处理非平面文件中的文本(提取信息如同平面文件)

4 投票
5 回答
761 浏览
提问于 2025-04-15 19:17

我有一个通过计算机模拟生成的纵向数据集,可以用以下表格来表示('var'是变量):

time subject var1 var2 var3
t1   subjectA  ...
t2   subjectB  ...

还有

subject   name
subjectA  nameA
subjectB  nameB

不过,生成的文件写成了类似下面的格式:

time t1 
  description
subjectA nameA
  var1 var2 var3
subjectB nameB
  var1 var2 var3
time t2
  description
subjectA nameA
  var1 var2 var3
subjectB nameB
  var1 var2 var3
...(and so on)

我一直在用一个(python)脚本来处理这个输出数据,把它转成一个平面文本文件,这样我就可以把它导入到R、python、SQL,或者用awk/grep来提取信息——下面是一个从单个查询中想要的信息的例子(在数据转换成表格后用SQL表示):

SELECT var1, var2, var3 FROM datatable WHERE subject='subjectB'

我在想有没有更有效的解决方案,因为这些数据文件每个大约有100MB(而且我有好几百个),创建平面文本文件既耗时又占用额外的硬盘空间,还包含冗余信息。理想情况下,我希望能直接与原始数据集互动,提取我想要的信息,而不需要创建额外的平面文本文件……有没有更简单的awk/perl解决方案呢?我在python的文本处理方面很熟练,但对awk的了解很基础,对perl更是一无所知;我想知道这些或其他特定领域的工具是否能提供更好的解决方案。

谢谢!

附言:哇,感谢大家!很抱歉我不能选择每个人的回答。
@FM:谢谢。我的Python脚本和你的代码很像,只是没有过滤步骤。不过你的组织结构很清晰。
@PP:我以为我已经很熟练使用grep了,但显然还不够!这非常有帮助……不过我觉得当输出中混入“时间”时,grep会变得困难(我在例子中没有把这作为可能的提取场景提到!这是我的失误)。
@ghostdog74:这真是太棒了……但修改代码以获取“subjectA”并不简单……(不过我会在这段时间多读读awk,希望能理解)。
@weismat:说得很好。
@S.Lott:这非常优雅且灵活——我并不是在寻求python的解决方案,但这与PP建议的解析、过滤和输出框架完美契合,并且足够灵活,可以处理多种不同的查询,从这个层次结构文件中提取不同类型的信息。

再次感谢大家——非常感谢。

5 个回答

2

如果你只想在匹配特定主题时获取var1、var2和var3,你可以试试下面这个命令:


  grep -A 1 'subjectB'

这里的 -A 1 参数告诉grep打印出匹配的那一行以及匹配行后面的那一行(在这个情况下,变量是在主题后面的那一行)。

你可能还想使用 -E 选项,这样grep就可以搜索一个正则表达式,并把主题搜索固定在行的开头(比如 grep -A 1 -E '^subjectB')。

最后,输出的内容将包括你想要的主题行和变量行。如果你想隐藏主题行,可以这样做:


  grep -A 1 'subjectB' |grep -v 'subjectB'

而且你可能还想处理变量行:


  grep -A 1 'subjectB' |grep -v 'subjectB' |perl -pe 's/ /,/g'

2

最好的办法是修改计算机模拟,让它输出矩形格式的数据。如果你不能这样做,下面有一个方法可以尝试:

为了能在R、SQL等工具中使用这些数据,你需要把数据从层级格式转换成矩形格式。假如你已经有一个解析器,可以把整个文件转换成矩形数据集,那你就已经完成了大部分工作。接下来,你需要给你的解析器增加一些灵活性,这样它就能过滤掉不需要的数据记录。这样,你就不再只是一个文件转换工具,而是一个数据提取工具。

下面的例子是用Perl写的,但你也可以用Python来实现。总体思路是把(a)解析、(b)过滤和(c)输出这几个部分清晰分开。这样,你就能创建一个灵活的环境,根据你当前的数据处理需求,轻松添加不同的过滤或输出方法。你还可以设置过滤方法,让它接受参数(可以是命令行输入或配置文件),这样就更灵活了。

use strict;
use warnings;

read_file($ARGV[0], \&check_record);

sub read_file {
    my ($file_name, $check_record) = @_;
    open(my $file_handle, '<', $file_name) or die $!;
    # A data structure to hold an entire record.
    my $rec = {
        time => '',
        desc => '',
        subj => '',
        name => '',
        vars => [],
    };
    # A code reference to get the next line and do some cleanup.
    my $get_line = sub {
        my $line = <$file_handle>;
        return unless defined $line;
        chomp $line;
        $line =~ s/^\s+//;
        return $line;
    };
    # Start parsing the data file.
    while ( my $line = $get_line->() ){
        if ($line =~ /^time (\w+)/){
            $rec->{time} = $1;
            $rec->{desc} = $get_line->();
        }
        else {
            ($rec->{subj}, $rec->{name}) = $line =~ /(\w+) +(\w+)/;
            $rec->{vars} = [ split / +/, $get_line->() ];

            # OK, we have a complete record. Now invoke our filtering
            # code to decide whether to export record to rectangular format.
            $check_record->($rec);
        }
    }
}

sub check_record {
    my $rec = shift;
    # Just an illustration. You'll want to parameterize this, most likely.
    write_output($rec)
        if  $rec->{subj} eq 'subjectB'
        and $rec->{time} eq 't1'
    ;
}

sub write_output {
    my $rec = shift;
    print join("\t", 
        $rec->{time}, $rec->{subj}, $rec->{name},
        @{$rec->{vars}},
    ), "\n";
}
4

这就是Python生成器的全部内容。

def read_as_flat( someFile ):
    line_iter= iter(someFile)
    time_header= None
    for line in line_iter:
        words = line.split()
        if words[0] == 'time':
            time_header = [ words[1:] ] # the "time" line
            description= line_iter.next()
            time_header.append( description )
        elif words[0] in subjectNameSet:
            data = line_iter.next()
            yield time_header + data

你可以像使用普通的Python迭代器一样使用它。

for time, description, var1, var2, var3 in read_as_flat( someFile ):
    etc.

撰写回答