Как я могу определить грамматику Raku для анализа текста TSV?

13

У меня есть некоторые данные 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.

littlebenlittle
источник
1
Nil, Это довольно бесплодно, что касается обратной связи, верно? Для отладки загрузите commaide, если вы еще этого не сделали, и / или посмотрите, как можно улучшить отчетность об ошибках в грамматиках? , Вы получили, Nilпотому что ваш шаблон принял семантику возврата. Смотрите мой ответ об этом. Я рекомендую вам отказаться от отслеживания. Смотрите ответ @ user0721090601 об этом. Для практичности и скорости, смотрите ответ JJ. Кроме того, вводный общий ответ на «Я хочу разобрать X с Раку. Кто-нибудь может помочь?» ,
raiph
использовать грамматику :: Tracer; # работает для меня
p6steve

Ответы:

12

Вероятно, главное, что его отбрасывает, - это \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которые не требуют специальной обработки помимо строкового преобразования, могут быть проигнорированы). Есть несколько более креативных / компактных способов обработки ваших, но я воспользуюсь довольно примитивным подходом для иллюстрации. Вот наш класс:

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

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

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

Любой маркер с квантором на нем будет рассматриваться как Positional, таким образом , мы могли бы также получить доступ к каждому отдельному матчу заголовка с $<header>[0], $<header>[1]и т.д. Но теми объектами матча, так что мы просто быстро stringify их. Команда 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цикле. Они flat @x Z @yпросто объединяют элементы, и присвоение хеша делает то, что мы имеем в виду, но есть и другие способы получить массив в нужном хеше.

Как только вы закончите, вы просто 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
user0721090601
источник
Я думаю, что я написал бы класс действий по-другому. class Actions { has @!header; method headerRow ($/) { @!header = @<header>.map(~*); make @!header.List; }; method valueRow ($/) {make (@!header Z=> @<value>.map: ~*).Map}; method TOP ($/) { make @<valueRow>.map(*.made).List }Вы, конечно, должны сначала создать его экземпляр :actions(Actions.new).
Брэд Гилберт
@BradGilbert Да, я склонен писать свои классы действий, чтобы избежать создания экземпляров, но в случае создания экземпляров, я бы, вероятно, сделал бы, class Actions { has @!header; has %!entries … }и просто бы значениеRow добавляло записи напрямую, чтобы вы в конечном итоге получили просто method TOP ($!) { make %!entries }. Но это ведь Раку и TIMTOWTDI :-)
user0721090601
После прочтения этой информации ( docs.raku.org/language/regexes#Modified_quantifier:_%,_%% ) я понял, что понимаю <valueRow>+ %% \n(захватывать строки, разделенные символами новой строки), но следуя этой логике, <.ws>* %% <header>можно было бы «захватывать необязательно» пробел, который разделен не пробелами ". Я что-то пропустил?
Кристофер Боттомс
@ChristopherBottoms почти. <.ws>Не захватывает ( <ws>будет). ОП отметил, что формат TSV может начинаться с необязательного пробела. В действительности это, вероятно, было бы еще лучше определить с помощью токена между строками, определенного как \h*\n\h*, который позволил бы определять valueRow более логично, как<header> % <.ws>
user0721090601
@ user0721090601 Я не помню, чтобы я читал %/ %%называл «чередование» оп ранее. Но это правильное имя. (В то время как использование этого для |, ||и кузены всегда казались мне странными.) Я не думал об этой "обратной" технике раньше. Но это хорошая идиома для написания регулярных выражений, сопоставляющих повторяющийся шаблон с некоторым утверждением-разделителем не только между совпадениями шаблона, но и позволяющими его на обоих концах (используя %%) или в начале, но не в конце (используя %), как, э-э, альтернатива в конце, но не начало логики ruleи :s. Ницца. :)
Раиф
11

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;

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

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

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

Поле идентификатора станет ключом к хешу, а все это массив хешей.

jjmerelo
источник
2
Голосование из-за практичности. Однако я не уверен, что ОП стремится больше изучать грамматику (подход моего ответа) или просто нуждается в анализе (подход вашего ответа). В любом случае, он должен быть хорошим, чтобы пойти :-)
user0721090601
2
Проголосовал по той же причине. :) Я думал, что ОП может быть нацелен на то, чтобы понять, что они сделали неправильно, с точки зрения семантики регулярных выражений (отсюда и мой ответ), на то, чтобы научиться делать это правильно (ваш ответ) или просто нужно разобрать (ответ JJ). ). Совместная деятельность. :)
Райф
7

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+ }
}

Единственное различие между a tokenи a regexсостоит в том, что regexотступает, а a 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', как это было временно во время обработки регулярного выражения в предыдущей строке. Но поскольку шаблон объявлен как a token, механизм правил (иначе называемый «механизм регулярных выражений») не возвращается , поэтому общее сопоставление не выполняется.

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

Правильное решение

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

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

Иногда обратное отслеживание все еще играет роль в контексте анализа. Например, в то время как грамматика для raku обычно избегает обратного отслеживания и вместо этого имеет сотни rules и tokens, тем не менее, у нее все еще есть 3 regexs.


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

raiph
источник