我主要是Matlab用户和Perl n00b。这是我的第一个Perl脚本。
我有一个大的固定宽度数据文件,我想将其处理成带有目录的二进制文件。我的问题是数据文件非常大,数据参数按时间排序。这使得解析Matlab变得困难(至少对我而言)。因此,看看Matlab在解析文本方面不是很好,我想我会尝试Perl。我编写了以下代码,至少在我的小测试文件中有效。但是,当我在实际的大型数据文件上尝试它时,它非常缓慢。它汇集在一起,其中包含许多来自Web / Perl文档的各种任务示例。
以下是数据文件的一小部分示例。注意:Real文件大约有2000个参数,是1-2GB。参数可以是文本,双精度或无符号整数。
Param 1 filter = ALL_VALUES
Param 2 filter = ALL_VALUES
Param 3 filter = ALL_VALUES
Time Name Ty Value
---------- ---------------------- --- ------------
1.1 Param 1 UI 5
2.23 Param 3 TXT Some Text 1
3.2 Param 1 UI 10
4.5 Param 2 D 2.1234
5.3 Param 1 UI 15
6.121 Param 2 D 3.1234
7.56 Param 3 TXT Some Text 2
我的脚本的基本逻辑是:
continue
块中写入时间和数据到二进制文件。然后在文本目录文件表中记录名称,类型和偏移量(用于稍后将文件读入Matlab)。这是我的剧本:
#!/usr/bin/perl
$lineArg1 = @ARGV[0];
open(INFILE, $lineArg1);
open BINOUT, '>:raw', $lineArg1.".bin";
open TOCOUT, '>', $lineArg1.".toc";
my $line;
my $data_start_pos;
my @param_name;
my @template;
while ($line = <INFILE>) {
chomp $line;
if ($line =~ s/\s+filter = ALL_VALUES//) {
$line = =~ s/^\s+//;
$line =~ s/\s+$//;
push @param_name, $line;
}
elsif ($line =~ /^------/) {
@template = map {'A'.length} $line =~ /(\S+\s*)/g;
$template[-1] = 'A*';
$data_start_pos = tell INFILE;
last; #Reached start of data exit loop
}
}
my $template = "@template";
my @lineData;
my @param_data;
my @param_time;
my $data_type;
foreach $current_param (@param_name) {
@param_time = ();
@param_data = ();
seek(INFILE,$data_start_pos,0); #Jump to data start
while ($line = <INFILE>) {
if($line =~ /$current_param/) {
chomp($line);
@lineData = unpack $template, $line;
push @param_time, @lineData[0];
push @param_data, @lineData[3];
}
} # END WHILE <INFILE>
} #END FOR EACH NAME
continue {
$data_type = @lineData[2];
print TOCOUT $current_param.",".$data_type.",".tell(BINOUT).","; #Write name,type,offset to start time
print BINOUT pack('d*', @param_time); #Write TimeStamps
print TOCOUT tell(BINOUT).","; #offset to end of time/data start
if ($data_type eq "TXT") {
print BINOUT pack 'A*', join("\n",@param_data);
}
elsif ($data_type eq "D") {
print BINOUT pack('d*', @param_data);
}
elsif ($data_type eq "UI") {
print BINOUT pack('L*', @param_data);
}
print TOCOUT tell(BINOUT).","."\n"; #Write memory loc to end data
}
close(INFILE);
close(BINOUT);
close(TOCOUT);
所以我对网上好人的问题如下:
编辑:我修改了示例文本文件以说明非整数时间戳,而Param名称可能包含空格。
答案 0 :(得分:3)
首先,您应始终拥有'use strict;' and 'use warnings;' pragmas in your script。
看起来您需要一个简单的数组(@param_name
)作为参考,因此加载这些值会直接得到它。 (再次,添加上面的pragma会开始显示错误,包括$line = =~ s/^\s+//;
行!)
我建议你阅读本文,了解如何将数据文件加载到 Hash of Hashes。设计完哈希后,只需读取并加载文件数据内容,然后遍历哈希的内容即可。
例如,使用time作为哈希
的键%HoH = (
1 => {
name => "Param1",
ty => "UI",
value => "5",
},
2 => {
name => "Param3",
ty => "TXT",
value => "Some Text 1",
},
3 => {
name => "Param1",
ty => "UI",
value => "10",
},
);
在开始处理之前,请确保在阅读完内容后关闭INFILE。
所以最后,你迭代哈希,并为你的输出写入引用数组(而不是文件内容) - 我认为这样做会更快更多。 / p>
如果您需要更多信息,请与我们联系。
注意:如果你走这条路,请加上Data:Dumper - 这是打印和理解哈希数据的重要帮助!
答案 1 :(得分:1)
在我看来,嵌入空间只能出现在最后一个字段中。这使得使用split ' '可以解决这个问题。
我假设你对标题不感兴趣。另外,我假设你想要一个每个参数的向量,并且对时间戳不感兴趣。
要使用在命令行中指定的数据文件名或通过标准输入管道传输,请将<DATA>
替换为<>
。
#!/usr/bin/env perl
use strict; use warnings;
my %data;
$_ = <DATA> until /^-+/; # skip header
while (my $line = <DATA>) {
$line =~ s/\s+\z//;
last unless $line =~ /\S/;
my (undef, $param, undef, $value) = split ' ', $line, 4;
push @{ $data{ $param } }, $value;
}
use Data::Dumper;
print Dumper \%data;
__DATA__
Param1 filter = ALL_VALUES
Param2 filter = ALL_VALUES
Param3 filter = ALL_VALUES
Time Name Ty Value
---------- ---------------------- --- ------------
1 Param1 UI 5
2 Param3 TXT Some Text 1
3 Param1 UI 10
4 Param2 D 2.1234
5 Param1 UI 15
6 Param2 D 3.1234
7 Param3 TXT Some Text 2
输出:
$VAR1 = { 'Param2' => [ '2.1234', '3.1234' ], 'Param1' => [ '5', '10', '15' ], 'Param3' => [ 'Some Text 1', 'Some Text 2' ] };
答案 2 :(得分:1)
首先,这段代码会导致输入文件为每个参数读取一次。这是非常无效的。
foreach $current_param (@param_name) {
...
seek(INFILE,$data_start_pos,0); #Jump to data start
while ($line = <INFILE>) { ... }
...
}
此外,很少有理由使用continue
块。这是更多的风格/可读性,然后是一个真正的问题。
现在开始使它更具性能。
我单独打包了这些部分,这样我就可以处理一行了。为了防止它耗尽大量的RAM,我使用File::Temp来存储数据,直到我准备好它为止。然后我使用File::Copy将这些部分附加到二进制文件中。
这是一个快速实施。如果我要添加更多内容,我会把它分开比现在更多。
#!/usr/bin/perl
use strict;
use warnings;
use File::Temp 'tempfile';
use File::Copy 'copy';
use autodie qw':default copy';
use 5.10.1;
my $input_filename = shift @ARGV;
open my $input, '<', $input_filename;
my @param_names;
my $template = ''; # stop uninitialized warning
my @field_names;
my $field_name_line;
while( <$input> ){
chomp;
next if /^\s*$/;
if( my ($param) = /^\s*(.+?)\s+filter = ALL_VALUES\s*$/ ){
push @param_names, $param;
}elsif( /^[\s-]+$/ ){
my @fields = split /(\s+)/;
my $pos = 0;
for my $field (@fields){
my $length = length $field;
if( substr($field, 0, 1) eq '-' ){
$template .= "\@${pos}A$length ";
}
$pos += $length;
}
last;
}else{
$field_name_line = $_;
}
}
@field_names = unpack $template, $field_name_line;
for( @field_names ){
s(^\s+){};
$_ = lc $_;
$_ = 'type' if substr('type', 0, length $_) eq $_;
}
my %temp_files;
for my $param ( @param_names ){
for(qw'time data'){
my $fh = tempfile 'temp_XXXX', UNLINK => 1;
binmode $fh, ':raw';
$temp_files{$param}{$_} = $fh;
}
}
my %convert = (
TXT => sub{ pack 'A*', join "\n", @_ },
D => sub{ pack 'd*', @_ },
UI => sub{ pack 'L*', @_ },
);
sub print_time{
my($param,$time) = @_;
my $fh = $temp_files{$param}{time};
print {$fh} $convert{D}->($time);
}
sub print_data{
my($param,$format,$data) = @_;
my $fh = $temp_files{$param}{data};
print {$fh} $convert{$format}->($data);
}
my %data_type;
while( my $line = <$input> ){
next if $line =~ /^\s*$/;
my %fields;
@fields{@field_names} = unpack $template, $line;
print_time( @fields{(qw'name time')} );
print_data( @fields{(qw'name type value')} );
$data_type{$fields{name}} //= $fields{type};
}
close $input;
open my $bin, '>:raw', $input_filename.".bin";
open my $toc, '>', $input_filename.".toc";
for my $param( @param_names ){
my $data_fh = $temp_files{$param}{data};
my $time_fh = $temp_files{$param}{time};
seek $data_fh, 0, 0;
seek $time_fh, 0, 0;
my @toc_line = ( $param, $data_type{$param}, 0+sysseek($bin, 0, 1) );
copy( $time_fh, $bin, 8*1024 );
close $time_fh;
push @toc_line, sysseek($bin, 0, 1);
copy( $data_fh, $bin, 8*1024 );
close $data_fh;
push @toc_line, sysseek($bin, 0, 1);
say {$toc} join ',', @toc_line, '';
}
close $bin;
close $toc;
答案 3 :(得分:0)
我修改了我的代码以按照建议构建哈希。由于时间限制,我还没有将输出合并到二进制文件中。另外,我需要弄清楚如何引用哈希来获取数据并将其打包成二进制文件。我不认为这部分应该是困难的...希望
在实际数据文件(~350MB和200万行)上,以下代码大约需要3分钟来构建哈希。我的1个内核的CPU使用率为100%(另外3个内核为nill),Perl内存使用率大约为325MB ......直到它向提示符转储了数百万行。但是,打印转储将被二进制包替换。
如果我犯了任何菜鸟错误,请告诉我。
#!/usr/bin/perl
use strict;
use warnings;
use Data::Dumper;
my $lineArg1 = $ARGV[0];
open(INFILE, $lineArg1);
my $line;
my @param_names;
my @template;
while ($line = <INFILE>) {
chomp $line; #Remove New Line
if ($line =~ s/\s+filter = ALL_VALUES//) { #Find parameters and build a list
push @param_names, trim($line);
}
elsif ($line =~ /^----/) {
@template = map {'A'.length} $line =~ /(\S+\s*)/g; #Make template for unpack
$template[-1] = 'A*';
my $data_start_pos = tell INFILE;
last; #Reached start of data exit loop
}
}
my $size = $#param_names+1;
my @getType = ((1) x $size);
my $template = "@template";
my @lineData;
my %dataHash;
my $lineCount = 0;
while ($line = <INFILE>) {
if ($lineCount % 100000 == 0){
print "On Line: ".$lineCount."\n";
}
if ($line =~ /^\d/) {
chomp($line);
@lineData = unpack $template, $line;
my ($inHeader, $headerIndex) = findStr($lineData[1], @param_names);
if ($inHeader) {
push @{$dataHash{$lineData[1]}{time} }, $lineData[0];
push @{$dataHash{$lineData[1]}{data} }, $lineData[3];
if ($getType[$headerIndex]){ # Things that only need written once
$dataHash{$lineData[1]}{type} = $lineData[2];
$getType[$headerIndex] = 0;
}
}
}
$lineCount ++;
} # END WHILE <INFILE>
close(INFILE);
print Dumper \%dataHash;
#WRITE BINARY FILE and TOC FILE
my %convert = (TXT=>sub{pack 'A*', join "\n", @_}, D=>sub{pack 'd*', @_}, UI=>sub{pack 'L*', @_});
open my $binfile, '>:raw', $lineArg1.'.bin';
open my $tocfile, '>', $lineArg1.'.toc';
for my $param (@param_names){
my $data = $dataHash{$param};
my @toc_line = ($param, $data->{type}, tell $binfile );
print {$binfile} $convert{D}->(@{$data->{time}});
push @toc_line, tell $binfile;
print {$binfile} $convert{$data->{type}}->(@{$data->{data}});
push @toc_line, tell $binfile;
print {$tocfile} join(',',@toc_line,''),"\n";
}
sub trim { #Trim leading and trailing white space
my (@strings) = @_;
foreach my $string (@strings) {
$string =~ s/^\s+//;
$string =~ s/\s+$//;
chomp ($string);
}
return wantarray ? @strings : $strings[0];
} # END SUB
sub findStr { #Return TRUE if string is contained in array.
my $searchStr = shift;
my $i = 0;
foreach ( @_ ) {
if ($_ eq $searchStr){
return (1,$i);
}
$i ++;
}
return (0,-1);
} # END SUB
输出如下:
$VAR1 = {
'Param 1' => {
'time' => [
'1.1',
'3.2',
'5.3'
],
'type' => 'UI',
'data' => [
'5',
'10',
'15'
]
},
'Param 2' => {
'time' => [
'4.5',
'6.121'
],
'type' => 'D',
'data' => [
'2.1234',
'3.1234'
]
},
'Param 3' => {
'time' => [
'2.23',
'7.56'
],
'type' => 'TXT',
'data' => [
'Some Text 1',
'Some Text 2'
]
}
};
这是输出TOC文件:
Param 1,UI,0,24,36,
Param 2,D,36,52,68,
Param 3,TXT,68,84,107,
感谢大家的帮助!这是一个很好的资源!
编辑:添加二进制&amp; TOC文件编写代码。