2011-12-19 57 views
4

我主要是Matlab用戶和Perl n00b。這是我的第一個Perl腳本。從大固定寬度的文本解析未分類的數據

我有一個大的固定寬度的數據文件,我想處理成一個二進制文件的目錄。我的問題是數據文件非常大,數據參數按時間排序。這使得解析Matlab很困難(至少對我來說)。所以看到Matlab如何不擅長解析文本,我想我會嘗試Perl。我寫了下面的代碼,至少在我的小測試文件中起作用。然而,當我在一個實際的大數據文件上嘗試它時,它的速度非常緩慢。它被拼湊在一起,有很多來自web/Perl文檔的各種任務的例子。

這是數據文件的一個小例子。注意:真實文件有大約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. 讀,直到----一線構築的參數列表中提取(總是有「過濾器=」)。
  2. 使用---行來確定字段寬度。它被空間打破。
  3. 對於每個參數構建時間和數據數組(同時嵌套在foreach參數中)
  4. 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); 

所以我的問題給你良好的網絡人如下:

  1. 我是什麼明顯搞砸了?語法,當我不需要時聲明變量等。
  2. 由於嵌套循環和一遍又一遍搜索行,這可能很慢(猜測)。有沒有更好的方法來重構循環以一次提取多行?
  3. 任何其他速度改進提示,你可以給?

編輯:我修改了示例文本文件來說明非整數時間戳,並且參數名可能包含空格。

+1

Perl在您的命令行上有文檔可用:'perldoc -q profile' – toolic 2011-12-19 21:47:23

+0

您可以在TOC文件和上述示例的BIN文件中顯示您期望的內容嗎? – 2011-12-19 23:23:52

+0

@SinanÜnürTOC文件看起來像這樣: 請注意,補償號碼是由組成的。 Param1,UI,0,10,20, Param2,D,20,30,40, Param3,TXT,40,50,60, 其中格式爲Name,type,offset to timeStart,offset to時間結束,抵消數據結束。所以在Matlab中需要的是使用適當的數據類型將二進制文件從開始到結束偏移量進行fread。 – 2011-12-20 00:33:37

回答

0

我修改代碼來構建一個哈希的建議。由於時間限制,我還沒有將輸出結合到二進制文件中。另外我需要弄清楚如何引用散列來獲取數據並將其打包爲二進制。我不認爲這部分應該是困難的...希望

在實際的數據文件(〜350MB & 2.0百萬行)下面的代碼大約需要3分鐘來構建散列。 CPU使用率是100%在我的核心(另3 nill)和Perl的內存使用量在1出突破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, 

感謝大家的幫助迄今爲止!這是一個很好的資源!

編輯:添加二進制文件& TOC文件編寫代碼。

+0

嘗試寫這個二進制文件,並且toc'my%convert =(TXT => sub {pack'A *',join「\ n」,@_},D => sub {pack'd *',@_ },UI => sub {pack'L *',@_}); 打開我的$ binfile,'>:raw',$ lineArg1。'。bin'; 打開我的$ tocfile,'>',$ lineArg1。'。TOC「; ($ param_names){ my $ data = $ dataHash {$ param}; my @toc_line =($ param,$ data - > {type},tell $ binfile);我的$ param名稱爲 。打印{$ binfile} $ convert {D} - >(@ {$ data - > {time}}); push @toc_line,告訴$ binfile;打印{$ binfile} $ convert {$ data - > {type}} - >(@ {$ data - > {data}}); push @toc_line,告訴$ binfile; print {$ tocfile} join(',',@ toc_line,''),「\ n」; }' – 2011-12-20 21:12:04

+0

@BradGilbert我添加了你的代碼來編寫二進制&TOC文件。它似乎工作正常。謝謝! – 2011-12-21 17:34:59

3

首先,您應該始終有'use strict;' and 'use warnings;' pragmas in your script

看來你需要一個簡單的數組(@param_name)作爲參考,所以加載這些值將是直截了當的,因爲你有它。 (再次,將上述編譯指示會開始顯示你的錯誤,包括$line = =~ s/^\s+//;線!)

我建議你閱讀本,瞭解如何加載數據文件到 Hash of Hashes。一旦設計了散列,您只需讀取並加載文件數據內容,然後遍歷散列的內容即可。

例如,使用時間作爲哈希

%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的內容看完之後,你開始處理之前。

所以最後,你迭代散列,並引用數組(而不是文件內容)爲你的輸出寫入 - 我會想象它會是更快做到這一點。

讓我知道你是否需要更多信息。

注意:如果你走這條路線,包括Data:Dumper - 打印一個顯著的幫助和理解你的哈希中的數據!

+0

聽起來很有希望。由於文件的大小,我很懷疑構建大型數組/結構等。當我在Matlab中嘗試這種方式時,在管理內存方面並不是那麼熱門,我會耗盡內存或無休止地開始分頁?所以我試圖不要一次性讀太多內存。 我會閱讀哈希,並給它一個鏡頭。最後,我只需要將時間數組和數據(按參數分組)寫入二進制文件......或多或少地採用我的代碼示例的繼續塊中描述的格式。這是我可以在Matlab中繪製數據與時間的關係 – 2011-12-20 00:01:36

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' 
         ] 
     };
+0

某些參數名稱有空格。所以拆分可能會導致問題。爲簡單起見,在示例中還沒有包含其他幾個字段/列(Param Description,Units,&Status),並且它們還經常包含空格。這就是爲什麼我使用數據開頭之上的---行。 ---行有空格,表示每行的字段寬度。這就是我使用解包函數的原因。 – 2011-12-20 13:58:06

1

首先,這一段代碼使得用於每PARAM被讀取一次的輸入文件。這是非常無效的。

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; 
+0

感謝您的輸入!在我看到您的答案之前,我添加了我寫的內容。我可能會嘗試使用臨時文件來保持內存使用率較低。理論上的一些數據文件可能會非常龐大​​。我還更新了原始帖子,以獲得稍微更好的示例數據文件。 – 2011-12-20 20:13:47

+0

@AeroEngy我想知道這個程序在實際數據上的效果如何。如果你的數據很容易適應RAM,那麼這個例子可能有點矯枉過正。 – 2011-12-20 20:47:41