处理非平面文件中的文本(提取信息如同平面文件)
我有一个通过计算机模拟生成的纵向数据集,可以用以下表格来表示('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 个回答
如果你只想在匹配特定主题时获取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'
最好的办法是修改计算机模拟,让它输出矩形格式的数据。如果你不能这样做,下面有一个方法可以尝试:
为了能在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";
}
这就是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.