从大的固定宽度文本中解析未分类的数据

时间:2011-12-19 21:26:39

标签: perl

我主要是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 

我的脚本的基本逻辑是:

  1. 读取----行以构建要提取的参数列表(始终具有“filter =”)。
  2. 使用---线确定字段宽度。它被空格打破了。
  3. 对于每个参数构建时间和数据数组(虽然嵌套在foreach param中)
  4. continue块中写入时间和数据到二进制文件。然后在文本目录文件表中记录名称,类型和偏移量(用于稍后将文件读入Matlab)。
  5. 这是我的剧本:

    #!/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);
    

    所以我对网上好人的问题如下:

    1. 我明显搞砸了什么?语法,在我不需要时声明变量等等。
    2. 这可能很慢(猜测),因为嵌套循环并一遍又一遍地逐行搜索。有没有更好的方法来重构循环以一次提取多行?
    3. 您可以提供任何其他速度提升技巧吗?
    4. 编辑:我修改了示例文本文件以说明非整数时间戳,而Param名称可能包含空格。

4 个答案:

答案 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文件编写代码。