Как я могу определить грамматику Raku для анализа текста TSV? - PullRequest
13 голосов
/ 03 марта 2020

У меня есть некоторые данные TSV

ID     Name    Email
   1   test    test@email.com
 321   stan    stan@nowhere.net

Я хотел бы разобрать это в список хэшей

@entities[0]<Name> eq "test";
@entities[1]<Email> eq "stan@nowhere.net";

У меня возникли проблемы с использованием метасимвола новой строки для разграничения строка заголовка из строки значений. Мое грамматическое определение:

use v6;

grammar Parser {
    token TOP       { <headerRow><valueRow>+ }
    token headerRow { [\s*<header>]+\n }
    token header    { \S+ }
    token valueRow  { [\s*<value>]+\n? }
    token value     { \S+ }
}

my $dat = q:to/EOF/;
ID     Name    Email
   1   test    test@email.com
 321   stan    stan@nowhere.net
EOF
say Parser.parse($dat);

Но это возвращается Nil. Я думаю, что неправильно понимаю нечто фундаментальное в регулярных выражениях в raku.

Ответы [ 3 ]

12 голосов
/ 03 марта 2020

Вероятно, главное, что его отбрасывает, это то, что \s соответствует горизонтальному и вертикальному пространству. Чтобы соответствовать только горизонтальному пространству, используйте \h, а чтобы соответствовать только вертикальному пространству, \v.

Одна небольшая рекомендация, которую я хотел бы сделать, состоит в том, чтобы избегать включения новых строк в токене. Возможно, вы также захотите использовать операторы чередования % или %%, так как они предназначены для работы с этим типом работы:

grammar Parser {
    token TOP       { 
                      <headerRow>     \n
                      <valueRow>+ %%  \n
                    }
    token headerRow { <.ws>* %% <header> }
    token valueRow  { <.ws>* %% <value>  }
    token header    { \S+ }
    token value     { \S+ }
    token ws        { \h* }
} 

Результат Parser.parse($dat) для этого следующий:

「ID     Name    Email
   1   test    test@email.com
 321   stan    stan@nowhere.net
」
 headerRow => 「ID     Name    Email」
  header => 「ID」
  header => 「Name」
  header => 「Email」
 valueRow => 「   1   test    test@email.com」
  value => 「1」
  value => 「test」
  value => 「test@email.com」
 valueRow => 「 321   stan    stan@nowhere.net」
  value => 「321」
  value => 「stan」
  value => 「stan@nowhere.net」
 valueRow => 「」

, который показывает нам, что грамматика успешно все проанализировала. Однако давайте сосредоточимся на второй части вашего вопроса, которая заключается в том, что вы хотите, чтобы он был доступен в переменной для вас. Для этого вам нужно предоставить класс действий, который очень прост для этого проекта. Вы просто создаете класс, методы которого соответствуют методам вашей грамматики (хотя очень простые, такие как value / header, которые не требуют специальной обработки помимо строкового преобразования, могут игнорироваться). Есть несколько более креативных / компактных способов обработки ваших, но я приведу go с довольно элементарным подходом для иллюстрации. Вот наш класс:

class ParserActions {
  method headerRow ($/) { ... }
  method valueRow  ($/) { ... }
  method TOP       ($/) { ... }
}

Каждый метод имеет сигнатуру ($/), которая является переменной соответствия регулярного выражения. Итак, теперь давайте спросим, ​​какую информацию мы хотим получить от каждого токена. В строке заголовка мы хотим, чтобы каждое из значений заголовка было в строке. Итак:

  method headerRow ($/) { 
    my   @headers = $<header>.map: *.Str
    make @headers;
  }

Любой токен с квантификатором будет рассматриваться как Positional, поэтому мы также можем получить доступ к каждому отдельному совпадению заголовка с $<header>[0], $<header>[1] и т. Д. c. Но это совпадающие объекты, поэтому мы просто быстро их упорядочиваем. Команда make позволяет другим токенам получать доступ к созданным нами специальным данным.

Наша строка значений будет выглядеть одинаково, потому что токены $<value> - это то, что нас волнует.

  method valueRow ($/) { 
    my   @values = $<value>.map: *.Str
    make @values;
  }

Когда мы перейдем к последнему методу, мы захотим создать массив с хешами.

  method TOP ($/) {
    my @entries;
    my @headers = $<headerRow>.made;
    my @rows    = $<valueRow>.map: *.made;

    for @rows -> @values {
      my %entry = flat @headers Z @values;
      @entries.push: %entry;
    }

    make @entries;
  }

Здесь вы можете увидеть, как мы получаем доступ к материалам, которые мы обработали в headerRow() и valueRow(): вы используете метод .made. Поскольку существует несколько значений ValueRows, чтобы получить каждое из их значений made, нам нужно составить карту (в этой ситуации я склонен писать свою грамматику так, чтобы в грамматике было просто <header><data>, и определять данные как несколько строк, но это достаточно просто, это не так уж плохо).

Теперь, когда у нас есть заголовки и строки в двух массивах, просто нужно сделать их массивом хэшей, что мы делаем в for л oop. flat @x Z @y просто объединяет элементы, и присвоение ha sh делает то, что мы имеем в виду, но есть другие способы получить массив в ха sh, который вы хотите.

Как только вы закончите, Вы просто make это, и тогда он будет доступен в made анализа:

say Parser.parse($dat, :actions(ParserActions)).made
-> [{Email => test@email.com, ID => 1, Name => test} {Email => stan@nowhere.net, ID => 321, Name => stan} {}]

Это довольно распространено, чтобы обернуть их в метод, как

sub parse-tsv($tsv) {
  return Parser.parse($tsv, :actions(ParserActions)).made
}

Таким образом, вы можете просто сказать

my @entries = parse-tsv($dat);
say @entries[0]<Name>;    # test
say @entries[1]<Email>;   # stan@nowhere.net
11 голосов
/ 03 марта 2020

TL; DR: нет. Просто используйте Text::CSV, который может работать с любым форматом.

Я покажу, сколько лет Text::CSV, вероятно, будет полезно:

use Text::CSV;

my $text = q:to/EOF/;
ID  Name    Email
   1    test    test@email.com
 321    stan    stan@nowhere.net
EOF
my @data = $text.lines.map: *.split(/\t/).list;

say @data.perl;

my $csv = csv( in => @data, key => "ID");

print $csv.perl;

Ключевой частью здесь является обработка данных, которая преобразует исходный файл в массив или массивы (в @data). Однако это необходимо только потому, что команда csv не может работать со строками; если данные в файле, вы можете go.

Последняя строка напечатает:

${"   1" => ${:Email("test\@email.com"), :ID("   1"), :Name("test")}, " 321" => ${:Email("stan\@nowhere.net"), :ID(" 321"), :Name("stan")}}%

Поле ID станет ключом к ха sh и все это массив хешей.

7 голосов
/ 03 марта 2020

TL; DR regex s возврат. token нет. Вот почему ваш шаблон не совпадает. Этот ответ сфокусирован на объяснении этого и на том, как легко исправить вашу грамматику. Однако вам, вероятно, следует переписать его или использовать существующий синтаксический анализатор, что вы должны определенно делать, если вы просто хотите анализировать TSV, а не узнавать о регулярных выражениях raku.

Фундаментальное недоразумение?

Мне кажется, я неправильно понимаю что-то фундаментальное в регулярных выражениях в raku.

(Если вы уже знаете термин "регулярные выражения" «Это весьма неоднозначный вопрос, пропустите этот раздел.)

Одна фундаментальная вещь, которую вы можете неправильно понять, это значение слова« регулярные выражения ». Вот некоторые популярные значения, которые люди принимают:

  • Формальные регулярные выражения.

  • Perl регулярные выражения.

  • Perl Совместимые регулярные выражения (PCRE).

  • Выражения соответствия текстового шаблона, называемые "регулярными выражениями", которые выглядят как любые из вышеперечисленных и выполняют нечто подобное.

Ни одно из этих значений не совместимо друг с другом.

Хотя регулярные выражения Perl семантически являются надмножеством формальных регулярных выражений, они гораздо более полезны во многих отношениях, но также более уязвимы для патологическое возвращение назад .

Хотя Perl Совместимые регулярные выражения совместимы с Perl в том смысле, что они первоначально совпадают со стандартными Perl регулярными выражениями в в конце 1990-х годов и в том смысле, что Perl поддерживает сменные механизмы регулярных выражений, включая механизм PCRE, синтаксис регулярных выражений PCRE не идентичен стандартному регулярному выражению Perl, используемому по умолчанию Perl в 2020 году.

И хотя выражения сопоставления с текстовым шаблоном, называемые "регулярными выражениями", как правило, выглядят несколько похожими друг на друга и сопоставляют весь текст, существуют десятки, а может быть, сотни вариантов синтаксиса и даже семантики для одного и того же синтаксиса.

Выражения соответствия шаблону текста Raku обычно называются либо «правилами», либо «регулярными выражениями». Использование термина «регулярные выражения» передает тот факт, что они похожи на другие регулярные выражения (хотя синтаксис был очищен). Термин «правила» передает тот факт, что они являются частью более широкого набора функций и инструментов , которые масштабируются до синтаксического анализа (и далее).

Быстрое исправление

С учетом вышеупомянутого фундаментального аспекта слова "регулярные выражения" я могу теперь обратиться к фундаментальному аспекту поведения вашего "регулярного выражения" .

Если мы переключим три из шаблонов в вашей грамматике для декларатора token для декларатора regex ваша грамматика работает так, как вы планировали:

grammar Parser {
    regex TOP       { <headerRow><valueRow>+ }
    regex headerRow { [\s*<header>]+\n }
    token header    { \S+ }
    regex valueRow  { [\s*<value>]+\n? }
    token value     { \S+ }
}

Единственное отличие между token и regex состоит в том, что regex возвращается, тогда как token нет. Таким образом:

say 'ab' ~~ regex { [ \s* a  ]+ b } # 「ab」
say 'ab' ~~ token { [ \s* a  ]+ b } # 「ab」
say 'ab' ~~ regex { [ \s* \S ]+ b } # 「ab」
say 'ab' ~~ token { [ \s* \S ]+ b } # Nil

Во время обработки последнего шаблона (который может быть и часто называется "регулярным выражением", но фактическим декларатором которого является token, а не regex), \S будет проглотите 'b', как это было временно во время обработки регулярного выражения в предыдущей строке. Но, поскольку шаблон объявлен как token, механизм правил (он же «механизм регулярных выражений») не возвращает , поэтому общее совпадение не удается.

Вот что происходит в ваш ОП.

Правильное исправление

Лучшее решение в целом состоит в том, чтобы отучить себя от , предполагая поведение при возврате, поскольку оно может быть медленным и даже катастрофически медленным (неотличимым от зависание программы) при использовании в сопоставлении со строкой, созданной злонамеренно, или строкой со случайно неудачной комбинацией символов.

Иногда regex s являются подходящими. Например, если вы пишете одноразовый код, и регулярное выражение выполняет свою работу, то все готово. Хорошо. Это одна из причин того, что синтаксис / ... / в raku объявляет шаблон возврата, как и regex. (С другой стороны, вы можете написать / :r ... /, если хотите включить ratcheting - «храповик» означает противоположность «backtrack», поэтому :r переключает регулярное выражение в token семантику.)

Иногда откат назад все еще играет роль в контексте анализа. Например, в то время как грамматика для raku, как правило, избегает возврата назад и вместо этого имеет сотни rule s и token s, тем не менее, у нее все еще есть 3 regex s.


Я проголосовал @ user0721090601 ++ ответ, потому что это полезно. Он также затрагивает несколько вещей, которые мне сразу показались нелогичными в вашем коде, и, что важно, придерживаются token s. Это может быть ответ, который вы предпочитаете, который будет классным.

...