【问题标题】:Parsing multiline data in Perl在 Perl 中解析多行数据
【发布时间】:2010-11-04 08:47:58
【问题描述】:

我有一些数据需要分析。数据是多行的,每个块由换行符分隔。所以,是这样的

Property 1: 1234
Property 2: 34546
Property 3: ACBGD

Property 1: 1234
Property 4: 4567

Property 1: just
Property 3: an
Property 5: simple
Property 6: example

我需要过滤掉那些存在某些特定属性的数据块。例如,只有那些具有属性 4 的块,只有那些同时具有属性 3 和 6 的块等等。我可能还需要根据这些属性的值进行选择,例如,只有那些具有属性 3 且其值为 '一个'。

我将如何在 Perl 中执行此操作。我尝试将其拆分为“\n”,但似乎无法正常工作。我错过了什么吗?

【问题讨论】:

  • 你能发布你目前拥有的代码,以便我们看看你在尝试什么吗?

标签: perl split multiline text-parsing data-analysis


【解决方案1】:

使这项任务变得简单的秘诀是使用 $/ 变量将 Perl 置于“段落模式”。这使得一次处理一个记录变得容易。然后,您可以使用 grep 之类的内容过滤它们。

#!/usr/bin/perl

use strict;
use warnings;

my @data = do {
  local $/ = '';
  <DATA>;
};

my @with_4   = grep { /^Property 4:/m } @data;

my @with_3   = grep { /^Property 3:/m } @data;
my @with_3_6 = grep { /^Property 6:/m } @with_3;

print scalar @with_3_6;

__DATA__
Property 1: 1234
Property 2: 34546
Property 3: ACBGD

Property 1: 1234
Property 4: 4567

Property 1: just
Property 3: an
Property 5: simple
Property 6: example

在该示例中,我将每条记录作为纯文本处理。对于更复杂的工作,我可能会将每条记录转换为哈希。

#!/usr/bin/perl

use strict;
use warnings;

use Data::Dumper;

my @data;

{
  local $/ = '';

  while (<DATA>) {
    chomp;

    my @rec = split /\n/;
    my %prop;
    foreach my $r (@rec) {
      my ($k, $v) = split /:\s+/, $r;
      $prop{$k} = $v;
    }

    push @data, \%prop;
  }
}

my @with_4   = grep { exists $_->{'Property 4'} } @data;

my @with_3_6 = grep { exists $_->{'Property 3'} and
                      exists $_->{'Property 6'} } @data;

my @with_3an = grep { exists $_->{'Property 3'} and
                      $_->{'Property 3'} eq 'an' } @data;

print Dumper @with_3an;

__DATA__
Property 1: 1234
Property 2: 34546
Property 3: ACBGD

Property 1: 1234
Property 4: 4567

Property 1: just
Property 3: an
Property 5: simple
Property 6: example

【讨论】:

  • @davorgthanx 很多,这似乎真的有效。但我注意到的一件事是,在最终数组中,顺序没有像原始数据中那样维护,这不是问题,但想知道是否也可以以某种方式维护。
  • @sfactor:我的脚本将您的数据输入到一个列表中,该列表保留了顺序。
  • 如果您想以与原始数据相同的顺序打印散列,那么在这种情况下您可以简单地对键进行排序。在更复杂的示例中,您可以在定义顺序的数据结构中的某处存储另一个值。我不愿意切换到使用数组来存储单个记录,因为哈希查找在这里非常有用。
  • @Alex Reynolds:您的程序仍将单个记录存储为哈希值。所以你的数据也被打乱了——转储器的输出显示键不是按数字顺序排列的。
  • 对。您的数据结构与我的几乎相同。记录存储在一个数组中(因此它们保持原始顺序),但每条记录中的值存储在散列中,因此顺序丢失。这与我的解决方案完全相同。 OP 很清楚,在这种情况下,这些值的顺序并不重要。他只是在询问是否可以在必要时保留订单。我们的两个解决方案都需要非常相似的更改来支持这一点。
【解决方案2】:

取决于每个属性集的大小以及您拥有多少内存...

我会使用一个简单的状态机来按顺序扫描文件 - 使用逐行顺序扫描,而不是多行 - 将每个属性/id/值添加到以 id 为键的哈希中。当你得到一个空行或文件结尾时,确定是否应该过滤散列的元素,并在必要时发出它们,然后重置散列。

【讨论】:

    【解决方案3】:

    又快又脏:

    my $string = <<END;
    Property 1: 1234
    Property 2: 34546
    Property 3: ACBGD
    
    Property 1: 1234
    Property 4: 4567
    
    Property 1: just
    Property 3: an
    Property 5: simple
    Property 6: example
    END
    
    my @blocks = split /\n\n/, $string;
    
    my @desired_blocks = grep /Property 1: 1234/, @blocks;
    
    print join("\n----\n", @desired_blocks), "\n";
    

    【讨论】:

      【解决方案4】:
      #!/usr/bin/perl
      
      use strict;
      use warnings;
      use Data::Dumper;
      
      my $propertyRef;
      my $propertyRefIdx = 0;
      
      while (<>) {
          chomp($_);
          if ($_ =~ /Property (\d+): (.*)/) {
              my $propertyKey = $1;
              my $propertyValue = $2;
      
              $propertyRef->[$propertyRefIdx]->{$propertyKey} = $propertyValue;
          }
          else {
              $propertyRefIdx++;
          }
      }
      
      print Dumper $propertyRef;
      

      假设此脚本名为propertyParser.pl,并且您有一个包含名为properties.txt 的属性和值的文件。你可以这样称呼它:

      $ propertyParser.pl < properties.txt
      

      用所有数据填充$propertyRef 后,您可以循环遍历元素并根据您需要应用的任何规则过滤它们,例如某些键和/或值组合:

      foreach my $property (@{$propertyRef}) {
          if (defined $property->{1} && defined $property->{3} 
                                     && ! defined $property->{6}) {
              # do something for keys 1 and 3 but not 6, etc.
          }
      }
      

      【讨论】:

      • 我喜欢这个。在 CLI 上添加以指定要匹配的属性和值会很简单。干净整洁。
      【解决方案5】:

      您的记录分隔符应为"\n\n"。每一行都以一个结尾,你可以用双换行符来区分一个块。使用这个想法,很容易过滤掉具有属性 4 的块。

      use strict;
      use warnings;
      use English qw<$RS>;
      
      open( my $inh, ... ) or die "I'm dead!";
      
      local $RS = "\n\n";
      while ( my $block = <$inh> ) { 
          if ( my ( $prop4 ) = $block =~ m/^Property 4:\s+(.*)/m ) { 
              ...
          }
          if ( my ( $prop3, $prop6 ) 
                   = $block =~ m/
              ^Property \s+ 3: \s+ ([^\n]*)
              .*?
              ^Property \s+ 6: \s+ ([^\n]*)
              /smx 
             ) {
              ...
          }
      }
      

      两个表达式都使用多行 ('m') 标志,因此 ^ 适用于 任何 行开头。最后一个使用标志在 '.' 中包含换行符。表达式 ('s') 和扩展语法 ('x'),其中包括忽略表达式中的空格。

      如果数据比较小,可以一次性处理完:

      use strict;
      use warnings;
      use English qw<$RS>;
      
      local $RS = "\n\n";
      my @block
          = map { { m/^Property \s+ (\d+): \s+ (.*?\S) \s+/gmx } } <DATA>
          ;
      print Data::Dumper->Dump( [ \@block ], [ '*block' ] ), "\n";
      

      这表明结果是:

      @block = (
                 {
                   '1' => '1234',
                   '3' => 'ACBGD',
                   '2' => '34546'
                 },
                 {
                   '4' => '4567',
                   '1' => '1234'
                 },
                 {
                   '6' => 'example',
                   '1' => 'just',
                   '3' => 'an',
                   '5' => 'simple'
                 }
               );
      

      【讨论】:

        【解决方案6】:

        检查 $/ 变量将为您做什么,例如解释 here。您可以将“行尾”分隔符设置为您喜欢的任何内容。您可以尝试将其设置为 '\n\n'

        $/ = "\n\n";
        foreach my $property (<DATA>)
            {
            print "$property\n";
            }
        
        
        __DATA__
        Property 1: 1234
        Property 2: 34546
        Property 3: ACBGD
        
        Property 1: 1234
        Property 4: 4567
        
        Property 1: just
        Property 3: an
        Property 5: simple
        Property 6: example
        

        由于您的数据元素似乎被空行分隔,这将逐个读取每个属性组的行。

        您也可以将整个文件读入一个数组并从内存中处理它

        my(@lines) = &lt;DATA&gt;

        【讨论】:

        • 这里有几个cmets。 local ly 修改 $/ 比全局修改更安全。而所谓的'段落'slurp模式可以通过将$/设置为''来启用。
        【解决方案7】:

        假设您的数据存储在一个文件中(比如说 mydata.txt),您可以编写以下 perl 脚本(我们称他为 Bob.pl):

        my @currentBlock = ();
        my $displayCurrentBlock = 0;
        # This will iterate on each line of the file
        while (<>) {
          # We check the content of $_ (the current line)
          if ($_ =~ /^\s*$/) {
            # $_ is an empty line, so we display the current block if needed
            print @currentBlock if $displayCurrentBlock;
            # Current block and display status are resetted
            @currentBlock = ();
            $displayCurrentBlock = 0;
          } else{
            # $_ is not an empty line, we add it to the current block
            push @currentBlock, $_;
            # We set the display status to true if a certain condition is met
            $displayCurrentBlock = 1 if ($_ =~ /Property 3: an\s+$/);
          }
        }
        # A last check and print for the last block
        print @currentBlock if $displayCurrentBlock;
        

        接下来,您只需启动 perl Bob.pl &lt; mydata.txt,瞧!

        localhost> perl Bob.pl < mydata.txt
        Property 1: just
        Property 3: an
        Property 5: simple
        Property 6: example
        

        【讨论】:

        • 永远,永远,永远写$_ =~ /.../!!
        【解决方案8】:

        关于您问题的第一部分,您可以使用 perl 的 -00 命令行选项读取“paragraph mode”中的记录,例如:

        #!/usr/bin/perl -00
        
        my @data = <>;
        
        # Print the last block.
        print $data[-1], "\n"
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2012-12-11
          • 1970-01-01
          • 2019-03-08
          • 2013-12-20
          • 2014-03-24
          • 2011-05-16
          • 1970-01-01
          • 2011-08-15
          相关资源
          最近更新 更多