Як я можу визначити граматику Раку для розбору тексту ТСВ?


13

У мене є деякі дані ТСВ

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. Я думаю, що я нерозумію щось принципове щодо виразів у раку.


1
Nil. Це досить безплідно, що стосується відгуків, правда? Для налагодження завантажте командний файл, якщо ви ще цього не зробили, та / або див. Як можна покращити звітність про помилки у граматиках? . Ви отримали NilТому що ваш шаблон передбачається відстеження семантики. Дивіться мою відповідь з цього приводу. Рекомендую відмовитися від зворотного огляду. Дивіться відповідь @ user0721090601 про це. Про повну практичність та швидкість дивіться відповідь JJ. Також вступна загальна відповідь на "Я хочу розібрати X з Раку. Чи хтось може допомогти?" .
raiph

використовувати граматику :: Tracer; #works для мене
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Просто intercolates елементи і призначення хеш - чи , що ми маємо на увазі, але є й інші способи , щоб отримати масив в хеш ви хочете.

Після того, як ви закінчите, ви просто 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

Думаю, я писав би клас дій по-різному. 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 … }і просто отримав valueRow додати записи безпосередньо, щоб у вас вийшло просто method TOP ($!) { make %!entries }. Але це Раку і все-таки TIMTOWTDI :-)
user0721090601

Читаючи цю інформацію ( docs.raku.org/language/regexes#Modified_quantifier:_%,_%% ), я думаю, що я розумію <valueRow>+ %% \n(Захоплення рядків, які розмежовані новими рядками), але, дотримуючись цієї логіки, <.ws>* %% <header>було б "захоплення необов'язковим пробіл, який розмежований непробільним ". Я щось пропускаю?
Крістофер Ботс

@ChristopherBottoms майже. <.ws>Чи не захоплює ( <ws>буде). ОП зазначила, що формат TSV може починатися з додаткового простору пробілів. Насправді це, мабуть, ще краще визначиться за допомогою маркера з інтервалом між рядками, визначеного як \h*\n\h*, що дозволить визначити значенняRow більш логічно як<header> % <.ws>
user0721090601

@ user0721090601 Я не пригадую, щоб раніше читав %/ %%називав "чергування". Але це правильна назва. (В той час, як використовувати його для |, ||і двоюрідні брати завжди вражали мене як дивно.) Я раніше не думав про цю "назад" техніку. Але це приємна ідіома для написання реджексів, що відповідають повторюваному шаблону з деяким твердженням роздільника не лише між збігами шаблону, але і дозволяють йому в обох кінцях (за допомогою %%) або на початку, але не в кінці (використовуючи %), як, er, альтернатива в кінці, але не запускається логіка ruleі :s. Приємно. :)
raiph

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;

Ключова частина тут - це обробка даних, яка перетворює початковий файл у масив чи масиви (в @data). Однак це потрібно лише тому, що csvкоманда не в змозі обробляти рядки; якщо дані є у файлі, ви можете піти.

Останній рядок буде надруковано:

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

Поле ID стане ключем до хешу, а все це - масив хешів.


2
Оновлення через практичність. Я не впевнений, якщо ОП має на меті більше вивчити граматики (підхід моєї відповіді) або просто потрібно розібратися (підхід вашої відповіді). У будь-якому випадку йому слід добре поїхати :-)
user0721090601

2
Оголошено з тієї ж причини. :) Я думав, що ОП може ставити собі за мету дізнатися, що вони зробили неправильно з точки зору семантики регулярних виразів (звідси моя відповідь), спрямовані на те, щоб навчитися робити це правильно (ваша відповідь) або просто потрібно розібратися (відповідь JJ ). Робота в команді. :)
raiph

7

TL; DR regex s зворотний трек. tokens ні. Ось чому ваш шаблон не відповідає. Ця відповідь зосереджена на тому, щоб пояснити це та як тривільно виправити свою граматику. Однак вам, ймовірно, слід переписати його або використати існуючий аналізатор, що саме ви повинні обов'язково зробити, якщо ви просто хочете проаналізувати TSV, а не дізнатися про регексери raku.

Фундаментальне непорозуміння?

Я думаю, що я нерозумію щось принципове щодо виразів у раку.

(Якщо ви вже знаєте, термін "регулярні вирази" є дуже неоднозначним, спробуйте пропустити цей розділ.)

Одне фундаментальне, що ви можете нерозуміти - це значення слова "регулярні виразки". Ось деякі популярні значення, які народ вважає:

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

  • Регекси Perl.

  • Регулярні регулярні вирази (PCRE), сумісні з Perl.

  • Текстовий шаблон, що відповідає виразам під назвою "регулярні виразки", схожий на будь-яке з перерахованих вище і робить щось подібне.

Жодне з цих значень не сумісне між собою.

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

Хоча регулярні вирази, сумісні з Perl, сумісні з Perl в тому сенсі, що вони спочатку були такими ж, як і стандартні регексели Perl наприкінці 90-х, і в тому сенсі, що Perl підтримує підключаються двигуни регулярного виразів, включаючи двигун PCRE, синтаксис регулярного вираження PCRE не ідентичний стандартному Регекс Perl, який за замовчуванням використовувався Perl у 2020 році.

І хоча вирази тексту, що відповідають тексту, під назвою "регулярні вирази", як правило, схожі на один одного, і всі вони відповідають тексту, існує кілька десятків, можливо сотень, варіацій синтаксису і навіть у семантиці для одного і того ж синтаксису.

Висловлювання тексту тексту Раку, як правило, називають або "правилами", або "регулярними виразами". Використання терміна "регулярні вирази" передає той факт, що вони виглядають дещо як інші регекси (хоча синтаксис був очищений). Термін "правила" повідомляє той факт, що вони є частиною набагато більш широкого набору функцій та інструментів, які масштабуються до розбору (і далі).

Швидке виправлення

З урахуванням зазначених вище фундаментального аспекту слова «регулярні вирази» з шляху, тепер я можу звернутися до фундаментального аспекту вашого «регулярні вирази» 's поведінка .

Якщо ми переключимо три зразки у вашій граматиці для 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зворотний трек, тоді як 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, двигун правил (він же «движок-генекс») не відхиляє , тому загальна відповідність не вдається.

Ось що відбувається у вашому ОП.

Правильне виправлення

Краще рішення в цілому - відучити себе від того, щоб поводитись із зворотним відслідковуванням, оскільки це може бути повільним і навіть катастрофічно повільним (невідрізним від висячої програми), коли використовується для зіставлення проти зловмисно сконструйованого рядка або одного з випадково невдалим поєднанням символів.

Іноді regexs доречні. Наприклад, якщо ви пишете одноразовий і регулярний вираз виконує роботу, то ви закінчили. Це добре. Це частина причини, що / ... /синтаксис у raku оголошує шаблон зворотного відстеження, як і він regex. (З іншого боку, ви можете написати , / :r ... /якщо ви хочете перейти на Храпова - «тріскачка» означає протилежне «BackTrack», тому :rперемикається регулярний вираз для tokenсемантики.)

Іноді зворотний трек все ще грає роль у контексті розбору. Наприклад, хоча граматика для raku, як правило, відхиляється від зворотного відстеження, а замість цього має сотні rules і tokens, вона все ж має 3 regexs.


Я підтримав відповідь @ user0721090601 ++, тому що це корисно. Він також стосується декількох речей, які мені відразу здалися ідіоматичними у вашому коді, і, що важливо, дотримуються tokens. Це може бути відповідь, яку ви віддаєте перевагу, яка буде класною.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.