Стратегии обработки файла с несколькими фиксированными форматами - PullRequest
6 голосов
/ 03 сентября 2010

Этот вопрос не специфичен для 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 ]

5 голосов
/ 04 сентября 2010

Играя с ответом на ваш вопрос, я нашел интересное решение с кратким основным циклом:

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' ],
      ],
    }
  ],
}

Я удивлен результатом,но по какой-то причине приходит на ум совет Ларри в 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
3 голосов
/ 04 сентября 2010

Это хороший вопрос. Мне приходят в голову два предложения.

(1) Первое - просто повторить идею из cjm : конечного автомата на основе объектов . Это гибкий способ выполнения сложного анализа. Я использовал его несколько раз и был доволен результатами в большинстве случаев.

(2) Вторая идея относится к категории Unix-конвейера «разделяй и властвуй» для предварительной обработки данных .

Сначала сделаем замечание о ваших данных: если набор форматов всегда встречается в паре, он фактически представляет собой единый формат данных и может быть объединен без потери информации. Это означает, что у вас есть только 3 формата: 1+2, 3 и 4+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 голосов
/ 03 сентября 2010

Это звучит как то, в чем хорош конечный автомат. Одним из способов создания конечного автомата в 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';
}

Некоторые мысли о том, как обращаться со строкой, прежде чем решить, что с ней делать:

Если файл достаточно мал, вы можете поместить его в массив в объекте. Это позволяет государству легко исследовать линию, не удаляя ее.

Если файл слишком велик для простоты, вы можете иметь метод для чтения следующей строки вместе с кэшем в вашем объекте, который позволяет вам вернуть его обратно:

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}, решает, в каком формате она находится, и возвращает состояние, которое анализирует и сохраняет этот формат (который получает строку для анализа из $self->{current_line}).

2 голосов
/ 03 сентября 2010

В зависимости от того, что вы хотите с этим сделать, было бы неплохо написать формальную грамматику, например, используя Parse :: RecDescent . Это позволит вам передать весь файл анализатору и получить из него структуру данных.

0 голосов
/ 03 сентября 2010

То, что я использовал в этом случае - если это возможно - это наличие уникального регулярного выражения для каждой строки.Если формат # 2 следует за 1 строкой формата # 1, то вы можете применить регулярное выражение # 2 сразу после 1. Но для строки, следующей за первой # 2, вы хотите попробовать # 2 или # 3.

Вы также можете иметь чередование, которое объединяет # 2 и # 3:

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

Если # 4 следует сразу за 3, вы захотите применить регулярное выражение # 4 после # 3, ирегулярное выражение № 5.После этого, поскольку это может быть # 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 для модуля управления состоянием.

0 голосов
/ 03 сентября 2010

Я бы оставил дополнительное состояние в одной или нескольких переменных и обновил бы его для каждой строки.Затем вы, например, знаете, была ли последняя строка 1-го уровня или последняя строка была формата 4 (и вы можете ожидать формат 5), что обеспечивает большую безопасность вашей обработки.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...