处理具有多种固定格式的文件的策略

时间:2010-09-03 19:28:23

标签: perl

这个问题特定于Perl,(尽管unpack函数很可能会影响我的实现。)

我必须处理存在多种格式的文件,以便将数据分层次地分解为有意义的部分。我希望能够做的是将文件数据解析为合适的数据结构。

以下是一个例子(关于RHS的评论):

                                       # | Format | Level | Comment
                                       # +--------+-------+---------
**DEVICE 109523.69142                  #        1       1   file-specific
  .981    561A                         #        2       1
10/MAY/2010    24.15.30,13.45.03       #        3       2   group of records
05:03:01   AB23X  15.67   101325.72    #        4       3   part of single record
*           14  31.30474 13        0   #        5       3   part of single record
05:03:15   CR22X  16.72   101325.42    #        4       3   new record
*           14  29.16264 11        0   #        5       3
06:23:51   AW41X  15.67    101323.9    #        4       3
*           14  31.26493219        0   #        5       3
11/MAY/2010    24.07.13,13.44.63       #        3       2   group of new records
15:57:14   AB23X  15.67   101327.23    #        4       3   part of single record
*           14  31.30474 13        0   #        5       3   part of single record
15:59:59   CR22X  16.72   101331.88    #        4       3   new record
*           14  29.16264 11        0   #        5

我现在的逻辑是脆弱的:

  • 我知道,例如,格式2总是在格式1之后,并且它们只跨越2行。
  • 我也知道格式4和格式5总是成对出现,因为它们对应于单个记录。记录数可以是变量
  • 我正在使用正则表达式来推断每行的格式。但是,这是有风险的,并且未来不会有灵活性(当有人决定更改输出的格式时)。

这里的一个重要问题是我可以采用什么策略来确定哪个格式需要用于哪一行。我很想知道其他人是否遇到过类似情况以及他们为解决这些问题所采取的措施。

6 个答案:

答案 0 :(得分:5)

在回答你的问题时,我找到了一个简洁的主循环的有趣解决方案:

while (<>) {
  given($_) {
    when (@{[ map $pattern{$_}, @expect]}) {}
    default {
      die "$0: line $.: expected " . join("|" => @expect) . "; got\n$_";
    }
  }
}

正如您将在下面看到的,%pattern是针对不同格式的命名模式的哈希值,given/when针对Regex个对象的数组执行短路搜索以查找第一场比赛。

由此,您可以推断出@expect是我们希望在当前行上找到的格式名称列表。

有一段时间,我被困在多种可能的预期格式的情况下以及如何知道匹配的格式,但后来我记得正则表达式中的(?{ code })

  

此零宽度断言评估任何嵌入的Perl代码。它总是成功,而且它的代码不是插值的。

这允许像穷人的yacc语法。例如,匹配和处理格式1的模式是

fmt1 => qr/^ \*\* DEVICE \s+ (\S+) \s*$
             (?{ $device->{attr1} = $1;
                 @expect = qw< fmt2 >;
               })
          /x,

处理完问题后的输入后,$device包含

{
  'attr1' => '109523.69142',
  'attr2' => '.981',
  'attr3' => '561A',
  'groups' => [
    {
      'date' => '10/MAY/2010',
      'nnn' => [ '24.15.30', '13.45.03' ],
      'records' => [
        [ '05:03:01', 'AB23X', '15.67', '101325.72', '14', '31.30474',  '13', '0' ],
        [ '05:03:15', 'CR22X', '16.72', '101325.42', '14', '29.16264',  '11', '0' ],
        [ '06:23:51', 'AW41X', '15.67', '101323.9',  '14', '31.264932', '19', '0' ],
      ],
    },
    {
      'date' => '11/MAY/2010',
      'nnn' => [ '24.07.13', '13.44.63' ],
      'records' => [
        [ '15:57:14', 'AB23X', '15.67', '101327.23', '14', '31.30474', '13', '0' ],
        [ '15:59:59', 'CR22X', '16.72', '101331.88', '14', '29.16264', '11', '0' ],
      ],
    }
  ],
}

我对结果感到很开心,但出于某种原因,Larry在perlstyle的建议浮现在脑海中:

  

仅仅因为你能以某种方式做某事并不意味着你应该这样做。


为了完整起见,下面将展示一个展示结果的工作程序。

#! /usr/bin/perl

use warnings;
use strict;
use feature ':5.10';
use re 'eval';

*ARGV = *DATA;

my $device;
my $record;
my @expect = qw/ fmt1 /;
my %pattern;
%pattern = (
  fmt1 => qr/^ \*\* DEVICE \s+ (\S+) \s*$
               (?{ $device->{attr1} = $1;
                   @expect = qw< fmt2 >;
                 })
            /x,

  fmt2 => qr/^ \s* (\S+) \s+ (\S+) \s*$
               (?{ @{$device}{qw< attr2 attr3 >} = ($1,$2);
                   @expect = qw< fmt3 >;
                 })
            /x,

  # e.g., 10/MAY/2010    24.15.30,13.45.03
  fmt3 => qr/^ (\d\d\/[A-Z]{3}\/\d{4}) \s+ (\S+) \s*$
               (?{ my($date,$nnns) = ($1,$2);
                   push @{ $device->{groups} } =>
                     { nnn  => [ split m|,| => $nnns ],
                       date => $date };
                   @expect = qw< fmt4 >;
                 })
            /x,

  # e.g., 05:03:01   AB23X  15.67   101325.72
  fmt4 => qr/^ (\d\d:\d\d:\d\d) \s+
               (\S+) \s+ (\S+) \s+ (\S+)
               \s*$
               (?{ push @{ $device->{groups}[-1]{records} } =>
                        [ $1, $2, $3, $4 ];
                   @expect = qw< fmt4 fmt5 >;
                 })
            /x,

  # e.g., *           14  31.30474 13        0
  fmt5 => qr/^\* \s+ (\d+) \s+
              # tricky: possibly no whitespace after 9-char float
              ((?=\d{1,7}\.\d+)[\d.]{1,9}) \s*
              (\d+) \s+ (\d+)
              \s*$
              (?{ push @{ $device->{groups}[-1]{records}[-1] } =>
                        $1, $2, $3, $4;
                  @expect = qw< fmt4 fmt3 fmt2 >;
                })
            /x,
);

while (<>) {
  given($_) {
    when (@{[ map $pattern{$_}, @expect]}) {}
    default {
      die "$0: line $.: expected " . join("|" => @expect) . "; got\n$_";
    }
  }
}

use Data::Dumper;
$Data::Dumper::Terse = $Data::Dumper::Indent = 1;
print Dumper $device;

__DATA__
**DEVICE 109523.69142
  .981    561A
10/MAY/2010    24.15.30,13.45.03
05:03:01   AB23X  15.67   101325.72
*           14  31.30474 13        0
05:03:15   CR22X  16.72   101325.42
*           14  29.16264 11        0
06:23:51   AW41X  15.67    101323.9
*           14  31.26493219        0
11/MAY/2010    24.07.13,13.44.63
15:57:14   AB23X  15.67   101327.23
*           14  31.30474 13        0
15:59:59   CR22X  16.72   101331.88
*           14  29.16264 11        0

答案 1 :(得分:3)

这是一个很好的问题。我有两个建议。

(1)第一个只是重申idea from cjm基于对象的状态机。这是执行复杂解析的灵活方式。我已经多次使用它,并且在大多数情况下对结果感到满意。

(2)第二个想法属于分而治之的Unix管道类别,用于预处理数据

首先观察一下您的数据:如果一组格式总是成对出现,它实际上代表了一种数据格式,可以在不丢失任何信息的情况下进行组合。这意味着您只有3种格式:1+234+5

这种想法导致了这一策略。编写一个或两个非常简单的脚本来预处理数据 - 有效地,重新格式化步骤,以便在真正的解析工作开始之前使数据成形。在这里,我将脚本显示为单独的工具。它们可以结合起来,但一般的哲学可能暗示它们仍然是截然不同的,狭义的工具。

在unbreak_records.pl。

省略她,并使用严格/警告。

while (<>){
    chomp;
    print /^\*?\s/ ? ' ' : "\n", $_;
}
print "\n";

在add_record_types.pl

while (<>){
    next unless /\S/;
    my $rt = /^\*/ ?   1 :
             /^..\// ? 2 : 3;
    print $rt, ' ', $_;
}

在命令行。

./unbreak_records.pl orig.dat | ./add_record_types.pl > reformatted.dat

输出:

1 **DEVICE 109523.69142   .981    561A
2 10/MAY/2010    24.15.30,13.45.03
3 05:03:01   AB23X  15.67   101325.72 *           14  31.30474 13        0
3 05:03:15   CR22X  16.72   101325.42 *           14  29.16264 11        0
3 06:23:51   AW41X  15.67    101323.9 *           14  31.26493219        0
2 11/MAY/2010    24.07.13,13.44.63
3 15:57:14   AB23X  15.67   101327.23 *           14  31.30474 13        0
3 15:59:59   CR22X  16.72   101331.88 *           14  29.16264 11        0

其余的解析很简单。如果您的数据提供者稍微修改了格式,您只需要编写一些不同的重新格式化脚本。

答案 2 :(得分:2)

根据您想要做的事情,例如,使用Parse::RecDescent实际编写正式语法可能是个好地方。这将允许您将整个文件提供给解析器,并从中获取数据结构。

答案 3 :(得分:2)

这听起来像是一台状态机擅长的东西。在Perl中执行状态机的一种方法是作为对象,其中每个状态都是一个方法。该对象为您提供了一个存储您正在构建的结构的位置,以及您需要的任何中间状态(如您正在阅读的文件句柄)。

my $state = 'expect_fmt1';
while (defined $state) {
  $state = $object->$state();
}
...
sub expect_fmt1 {
  my $self = shift;
  # read format 1, parse it, store it in object
  return 'expect_fmt2';
}

关于在决定如何处理之前处理必须查看的情况的一些想法:

如果文件足够小,您可以将其粘贴到对象中的arrayref中。这使得状态很容易检查一条线而不删除它。

如果文件太大而无法轻松啜饮,您可以使用方法读取下一行以及对象中的缓存,以便将其放回:

my get_line {
  my $self = shift;
  my $cache = $self->{line_cache};
  return shift @$cache if @$cache;
  return $self->{filehandle}->getline;
}
my unget_line { my $self = shift; unshift @{ $self->{line_cache} }, @_ }

或者,您可以将涉及此决策的状态分为两种状态。第一个状态读取该行,将其存储在$self->{current_line}中,确定它是什么格式,并返回解析&amp;的状态。格式化的商店(从$self->{current_line}获取要解析的行)。

答案 4 :(得分:0)

我会在一个或多个变量中保留一个额外的状态,并按行更新它。 那你呢? G。知道最后一行是1级,还是最后一行是格式4(你可以期望格式5),从而为你的处理提供更多的安全性。

答案 5 :(得分:0)

在这种情况下,我过去常常做的事情 - 如果可能的话 - 每行都有一个独特的正则表达式。如果格式#2遵循格式#1的1行,则可以在1之后立即应用正则表达式#2。但对于第一个#2之后的行,您要尝试#2或#3。

您还可以进行#2和#3组合的替换:

my ( $cap2_1, $cap2_2, $cap3_1, $cap3_2 ) = $line =~ /$regex2|regex3/;

如果#4紧跟在3之后,你将需要在#3和正则表达式#5之后应用正则表达式#4。之后,因为它可以是#3或#4,您可能想要重复多重匹配或重复#3 /#4。

while ( <> ) {
    given ( $state ) { 
         when ( 1 ) { my ( $device_num )  = m/$regex1/; $state++; }
         when ( 2 ) { my ( $cap1, $cap2 ) = m/$regex2/; $state++; }
         when ( 3 ) { 
             my ( $cap1, $cap2, $date, $nums ) = m/$regex2|$regex3/;
             $state += $cap1 ? 1 : 2;
         }
    }
}

这种方式为您提供了您可能想要做的事情的要点。或者请参阅FSA::Rules了解状态管理模块。