从固定宽度的大文本中解析未排序的数据

Posted

技术标签:

【中文标题】从固定宽度的大文本中解析未排序的数据【英文标题】:Parsing unsorted data from large fixed width text 【发布时间】:2011-12-19 21:26:38 【问题描述】:

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

我的脚本的基本逻辑是:

    阅读直到 ---- 行以构建要提取的参数列表(始终具有“过滤器 =”)。 使用 --- 行来确定字段宽度。它被空格分隔。 对于每个参数的构建时间和数据数组(嵌套在 foreach 参数中) 在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);

所以我想问你们这些网络好人如下:

    我显然在搞砸什么?语法、在我不需要时声明变量等。 这可能很慢(猜测),因为嵌套循环和一遍又一遍地逐行搜索。有没有更好的方法来重组循环以一次提取多行? 您还有其他提高速度的建议吗?

编辑:我修改了示例文本文件以说明非整数时间戳,并且参数名称可能包含空格。

【问题讨论】:

您能否在上面的示例中显示您对 TOC 文件和 BIN 文件的期望? @SinanÜnür TOC 文件看起来像这样:注意偏移量是由组成的。 Param1,UI,0,10,20, Param2,D,20,30,40, Param3,TXT,40,50,60, 其中格式为 Name, type, offset to timeStart, offset to time end, offset to数据结束。因此,在 Matlab 中需要做的就是使用适当的数据类型从开始到结束偏移量读取二进制文件。 @SinanÜnür 我只会写出第一个参数,因为它会以二进制形式出现。我将使用十六进制表示法,尽管这将是一个二进制文件。此外,我将时间戳写为单打而不是双打作为空格。 0x3f800000 0x40400000 0x40a00000 0x00000005 0x0000000A 0x0000000F。所以 Param1 的 timestart、timeend 和 datastart 偏移量为 0,96,192(如果我正确添加的话) 一定要按参数名排序吗? @BradGilbert 如果在写入二进制时未按参数名称排序,则在 Matlab 中构建数据/时间数组很可能会很困难且速度很慢。 【参考方案1】:

首先,您应该始终拥有'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 - 对打印和理解哈希中的数据有很大帮助!

【讨论】:

这听起来很有希望。由于文件的大小,我对构建大型数组/结构等持怀疑态度。当我在Matlab中尝试这个时,它在管理内存方面并不那么热,我要么内存不足,要么开始无休止地分页?所以我试图一次不要过多地读入内存。我将阅读哈希并试一试。最后,我只需要时间数组,以及写入二进制的数据(按参数分组)......或多或少以我的代码示例的继续块中描述的格式。这样我就可以在 Matlab 中绘制数据与时间的关系【参考方案2】:

在我看来,嵌入的空格只能出现在最后一个字段中。这使得使用split ' ' 可以解决这个问题。

我假设您对标题不感兴趣。此外,我假设您需要每个参数的向量并且对时间戳不感兴趣。

要使用在命令行中指定或通过标准输入管道传输的数据文件名,请将&lt;DATA&gt; 替换为&lt;&gt;

#!/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 = 
          '参数2' => [
                        '2.1234',
                        '3.1234'
                      ],
          '参数1' => [
                        '5',
                        '10',
                        '15'
                      ],
          '参数3' => [
                        '一些文本 1',
                        '一些文字 2'
                      ]
        ;

【讨论】:

有些参数名确实有空格。所以分裂可能会导致问题。为了简单起见,我在示例中没有包括其他几个字段/列(参数描述、单位和状态),它们也经常包含空格。这就是为什么我在数据开头使用 --- 行。 --- 行有空格,表示每行的字段宽度。这就是我使用解包功能的原因。【参考方案3】:

首先,这段代码会为每个参数读取一次输入文件。这是相当低效的。

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 .= "\@$posA$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$paramtime;
  print $fh $convertD->($time);


sub print_data
  my($param,$format,$data) = @_;
  my $fh = $temp_files$paramdata;
  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$fieldsname //= $fieldstype;

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$paramdata;
  my $time_fh = $temp_files$paramtime;

  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;

【讨论】:

感谢您的意见!在看到您的答案之前,我添加了到目前为止所写的内容。我可能会尝试合并使用临时文件以保持较低的内存使用率。理论上,一些数据文件可能会变得非常庞大。我还更新了原始帖子以获得更好的示例数据文件。 @AeroEngy 我想知道这个程序在实际数据上的效果如何。如果您的数据很容易放入 RAM,那么这个示例可能有点过头了。【参考方案4】:

我修改了我的代码以按照建议构建一个哈希。由于时间限制,我还没有将输出合并到二进制文件中。另外,我需要弄清楚如何引用哈希来获取数据并将其打包成二进制文件。我不认为这部分应该很难......希望

在实际数据文件(约 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=>subpack 'A*', join "\n", @_, D=>subpack 'd*', @_, UI=>subpack '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 $convertD->(@$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'
                                   ]
                       
        ;

这是输出目录文件:

Param 1,UI,0,24,36,
Param 2,D,36,52,68,
Param 3,TXT,68,84,107,

感谢大家迄今为止的帮助!这是一个很好的资源!

编辑:添加了二进制和 TOC 文件编写代码。

【讨论】:

试试这个来写二进制文件,toc my %convert = (TXT=&gt;subpack 'A*', join "\n", @_ ,D=&gt;sub pack 'd*', @_,UI=&gt;subpack 'L*', @_); open my $binfile, '&gt;:raw', $lineArg1.'.bin'; open my $tocfile, '&gt;', $lineArg1.'.toc'; for my $param (@param_names) my $data = $dataHash$param;my @toc_line = ($param, $data-&gt;type, tell $binfile ); print $binfile $convertD-&gt;(@$data-&gt;time); push @toc_line, tell $binfile; print $binfile $convert$data-&gt;type-&gt;(@$data-&gt;data); push @toc_line, tell $binfile; print $tocfile join(',',@toc_line,''),"\n"; @BradGilbert 我添加了您的代码来编写二进制和 TOC 文件。它似乎工作正常。谢谢!

以上是关于从固定宽度的大文本中解析未排序的数据的主要内容,如果未能解决你的问题,请参考以下文章

如何在 PHP 中解析固定宽度的文本文件? [复制]

如何在 Pyspark 中以编程方式解析固定宽度的文本文件?

Spark-Scala:使用异常处理将固定宽度线解析为 Dataframe Api

CoreData:从字典数组插入到SQLite数据库中发生未排序 - 因此无法设置外键

使用 Powershell 从 SQL Server 2008 R2 导出到固定宽度的文本文件

WPF PropertyChanged 事件未触发/更新文本框