Jak zdefiniować gramatykę Raku do analizy tekstu TSV?


13

Mam trochę danych TSV

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

Chciałbym to parsować na listę skrótów

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

Mam problem z użyciem metaznaku nowej linii do oddzielenia wiersza nagłówka od wierszy wartości. Moja definicja gramatyki:

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);

Ale to wraca Nil. Myślę, że nie rozumiem czegoś fundamentalnego w wyrażeniach regularnych w raku.


1
Nil. Jeśli chodzi o opinie, to jest dość jałowe, prawda? W celu debugowania pobierz przecinek, jeśli jeszcze tego nie zrobiłeś i / lub zobacz Jak poprawić raportowanie błędów w gramatyce? . Masz Nilswój wzorzec zakładający semantykę cofania. Zobacz moją odpowiedź na ten temat. Polecam unikać powrotu. Zobacz odpowiedź @ user0721090601 na ten temat. Po prostu praktyczność i szybkość, patrz odpowiedź JJ. Również Wstępna ogólna odpowiedź na „Chcę przeanalizować X z Raku. Czy ktoś może pomóc?” .
raiph

użyj Grammar :: Tracer; #works for me
p6steve

Odpowiedzi:


12

Prawdopodobnie najważniejszą rzeczą, która go rzuca, jest \sdopasowanie do przestrzeni poziomej i pionowej. Aby dopasować tylko poziomą przestrzeń, wykorzystanie \hi dopasować tylko pionową przestrzeń, \v.

Jednym małym zaleceniem, które chciałbym zrobić, jest unikanie umieszczania nowego wiersza w tokenie. Możesz także użyć operatorów naprzemiennych %lub %%, ponieważ są one zaprojektowane do obsługi tego typu pracy:

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

Wynik Parser.parse($dat)tego jest następujący:

「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 => 「」

co pokazuje nam, że gramatyka z powodzeniem przeanalizowała wszystko. Skupmy się jednak na drugiej części pytania, która ma być dostępna w zmiennej dla Ciebie. Aby to zrobić, musisz podać klasę działań, która jest bardzo prosta dla tego projektu. Po prostu tworzysz klasę, której metody pasują do metod twojej gramatyki (chociaż te bardzo proste, takie jak value/ headerktóre nie wymagają specjalnego przetwarzania poza rygoryzacją, mogą zostać zignorowane). Istnieje kilka bardziej kreatywnych / kompaktowych sposobów radzenia sobie z przetwarzaniem, ale postaram się zastosować dość podstawowe podejście do ilustracji. Oto nasza klasa:

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

Każda metoda ma sygnaturę, ($/)która jest zmienną dopasowania wyrażenia regularnego. Więc teraz zapytajmy, jakie informacje chcemy od każdego tokena. W wierszu nagłówka chcemy każdej wartości nagłówka z rzędu. Więc:

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

Dowolny znak z kwantyfikatorem na nim będą traktowane jako Positional, więc mogliśmy również dostęp do każdego indywidualnego meczu z nagłówka $<header>[0], $<header>[1]itp Ale to są obiekty mecz, więc po prostu szybko stringify im. makeKomenda pozwala innym tokeny dostępu do tej szczególnej dane, które mamy utworzone.

Nasz wiersz wartości będzie wyglądał identycznie, ponieważ $<value>dbamy o tokeny.

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

Kiedy przejdziemy do ostatniej metody, będziemy chcieli utworzyć tablicę z skrótami.

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

Tutaj możesz zobaczyć, w jaki sposób uzyskujemy dostęp do rzeczy, w których przetwarzaliśmy headerRow()i valueRow(): Używasz .mademetody. Ponieważ istnieje wiele wierszy wartości, aby uzyskać każdą z ich madewartości, musimy zrobić mapę (jest to sytuacja, w której mam tendencję do pisania gramatyki po prostu <header><data>w gramatyce i definiowania danych jako wielu wierszy, ale jest to dość proste, nie jest tak źle).

Teraz, gdy mamy nagłówki i wiersze w dwóch tablicach, wystarczy po prostu zrobić z nich tablicę skrótów, co robimy w forpętli. Po flat @x Z @yprostu interpoluje elementy, a przypisanie skrótu robi to, co mamy na myśli, ale istnieją inne sposoby uzyskania tablicy w haszu, którą chcesz.

Gdy skończysz, po prostu maketo, a następnie będzie dostępny w madeparsowaniu:

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

Dość powszechne jest zawijanie ich w metodę, taką jak

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

W ten sposób możesz po prostu powiedzieć

my @entries = parse-tsv($dat);
say @entries[0]<Name>;    # test
say @entries[1]<Email>;   # stan@nowhere.net

Myślę, że inaczej napisałbym klasę działań. 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 }Oczywiście najpierw musisz to zrobić :actions(Actions.new).
Brad Gilbert

@BradGilbert tak, mam tendencję do pisania klas swoich działań, aby uniknąć tworzenia instancji, ale w przypadku tworzenia instancji prawdopodobnie zrobiłbym to class Actions { has @!header; has %!entries … }i po prostu miałbym wartośćRow dodaj wpisy bezpośrednio, abyś skończył method TOP ($!) { make %!entries }. Ale to w końcu Raku i TIMTOWTDI :-)
user0721090601

Po przeczytaniu tych informacji ( docs.raku.org/language/regexes#Modified_quantifier:_%,_%% ) wydaje mi się, że rozumiem <valueRow>+ %% \n(przechwytywanie wierszy oddzielonych znakami nowej linii), ale zgodnie z tą logiką <.ws>* %% <header>byłoby „przechwytywanie opcjonalne spacja, która jest oddzielona spacją ”. Czy coś brakuje?
Christopher Bottoms

@ChristopherBottoms prawie. <.ws>Nie uchwycić ( <ws>by). OP zauważył, że format TSV może zaczynać się od opcjonalnej białej spacji. W rzeczywistości byłoby to prawdopodobnie jeszcze lepiej zdefiniowane za pomocą tokena odstępu między wierszami zdefiniowanego jako \h*\n\h*, co pozwoliłoby na bardziej logiczne zdefiniowanie <header> % <.ws>
valueRow

@ user0721090601 Nie przypominam sobie wcześniej czytania %/ %%określania jako „alternacja”. Ale to właściwa nazwa. (Podczas gdy użycie go |, ||a kuzyni zawsze wydawali mi się dziwne.) Nie myślałem o tej „wstecznej” technice. Ale to fajny idiom do pisania wyrażeń regularnych pasujących do powtarzanego wzorca z pewną separacją nie tylko między dopasowaniami wzorca, ale także pozwalający na obu końcach (za pomocą %%) lub na początku, ale nie na końcu (za pomocą %), jako, er, alternatywa na końcu, ale nie logika początkowa rulei :s. Miły. :)
raiph

11

TL; DR: ty nie. Wystarczy użyć Text::CSV, który jest w stanie poradzić sobie z każdym formatem.

Pokażę, ile lat Text::CSVbędzie prawdopodobnie przydatne:

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;

Kluczową częścią tutaj jest munging danych, które przekształcają plik początkowy w tablicę lub tablice (in @data). Jest to potrzebne tylko dlatego, że csvpolecenie nie jest w stanie poradzić sobie z łańcuchami; jeśli dane są w pliku, możesz iść.

Ostatni wiersz zostanie wydrukowany:

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

Pole ID stanie się kluczem do skrótu, a całość będzie tablicą skrótów.


2
Zwiększenie popularności ze względu na praktyczność. Nie jestem jednak pewien, czy PO dąży bardziej do nauki gramatyki (podejście mojej odpowiedzi), czy po prostu wymaga analizy (podejście twojej odpowiedzi). W obu przypadkach powinien być
gotowy

2
Głosował z tego samego powodu. :) Pomyślałem, że OP może chcieć dowiedzieć się, co zrobili źle pod względem semantyki wyrażeń regularnych (stąd moja odpowiedź), starając się nauczyć, jak to zrobić poprawnie (Twoja odpowiedź), lub po prostu musi przeanalizować (odpowiedź JJ) ). Praca zespołowa. :)
raiph

7

regexBacktrack TL; DR . tokennie. Dlatego twój wzór nie pasuje. Ta odpowiedź skupia się na wyjaśnieniu tego i jak w prosty sposób naprawić gramatykę. Jednak prawdopodobnie powinieneś go przepisać lub użyć istniejącego parsera, co zdecydowanie powinieneś zrobić, jeśli chcesz po prostu parsować TSV, zamiast uczyć się o wyrażeniach regularnych raku.

Podstawowe nieporozumienie?

Myślę, że nie rozumiem czegoś fundamentalnego w wyrażeniach regularnych w raku.

(Jeśli znasz już termin „wyrażenia regularne” jest bardzo niejednoznaczny, rozważ pominięcie tej sekcji).

Jedną z podstawowych rzeczy, które mogą być nieporozumieniem, jest znaczenie słowa „wyrażenia regularne”. Oto kilka popularnych znaczeń, które lud zakłada:

  • Formalne wyrażenia regularne.

  • Perl wyrażenia regularne.

  • Wyrażenia regularne zgodne z Perlem (PCRE).

  • Wyrażenia pasujące do wzorca tekstowego zwane „wyrażeniami regularnymi”, które wyglądają jak wyżej, i robią coś podobnego.

Żadne z tych znaczeń nie jest ze sobą kompatybilne.

Chociaż wyrażenia regularne Perla są semantycznie nadzbiorem formalnych wyrażeń regularnych, są one o wiele bardziej przydatne na wiele sposobów, ale także bardziej podatne na patologiczne cofanie się .

Podczas gdy wyrażenia regularne zgodne z Perlem są kompatybilne z Perlem w tym sensie, że pierwotnie były takie same jak standardowe wyrażenia regularne Perla pod koniec lat 90. XX wieku oraz że Perl obsługuje wtykowe silniki wyrażeń regularnych , w tym silnik PCRE, składnia wyrażeń regularnych PCRE nie jest identyczna ze standardowym Wyrażenie regularne Perla używane domyślnie przez Perla w 2020 roku.

I chociaż wyrażenia pasujące do wzorca tekstowego zwane „wyrażeniami regularnymi” ogólnie wyglądają trochę podobnie do siebie i wszystkie pasują do tekstu, istnieją dziesiątki, być może setki odmian składni, a nawet semantyki dla tej samej składni.

Wyrażenia pasujące do wzorca tekstowego Raku są zwykle nazywane „regułami” lub „wyrażeniami regularnymi”. Użycie terminu „wyrażenia regularne” oznacza, że ​​wyglądają one podobnie jak inne wyrażenia regularne (chociaż składnia została oczyszczona). Termin „reguły” oznacza fakt, że są one częścią znacznie szerszego zestawu funkcji i narzędzi, które można skalować do analizy (i nie tylko).

Szybka poprawka

Z powyższej zasadniczej aspekcie słowo „regexes” out of the way, mogę teraz przejść do zasadniczego aspektu „regex” 's zachowanie .

Jeśli zmienimy trzy wzorce w gramatyce tokendeklaratora na regexdeklarator, gramatyka będzie działać zgodnie z zamierzeniami:

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

Jedyna różnica między a tokeni a regexpolega na tym, że regexścieżki powrotne tokennie występują. A zatem:

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

Podczas przetwarzania ostatniego wzorca (który może być i często nazywany jest „regex”, ale którego faktycznym deklaratorem tokennie jest regex), \Społknie go 'b', tak jak tymczasowo to zrobi podczas przetwarzania regex w poprzednim wierszu. Ponieważ wzorzec jest zadeklarowany jako a token, silnik reguł (inaczej „silnik wyrażenia regularnego”) nie cofa się , więc ogólne dopasowanie kończy się niepowodzeniem.

Tak dzieje się w twojej OP.

Właściwa poprawka

Ogólnie lepszym rozwiązaniem jest odejście od zakładania zachowania wstecznego, ponieważ może być ono powolne, a nawet katastrofalnie wolne (nie do odróżnienia od zawieszenia programu), gdy jest używane do dopasowywania do złośliwie skonstruowanego łańcucha lub łańcucha z przypadkowo niefortunną kombinacją znaków.

Czasami regexs są odpowiednie. Na przykład, jeśli piszesz jednorazowo, a wyrażenie regularne wykonuje zadanie, to gotowe. W porządku. Jest to jeden z powodów, dla których / ... /składnia w raku deklaruje wzorzec cofania, podobnie jak regex. (Potem znowu można pisać / :r ... /, jeśli chcesz, aby przełączyć się na wzmaganie - „zapadkowy” oznacza przeciwieństwo „BackTrack”, więc :rwłącza regex do tokensemantyki).

Czasami cofanie nadal odgrywa rolę w kontekście analizowania. Na przykład, podczas gdy gramatyka dla Raku generalnie unika Backtracking, a zamiast tego ma setki ruleS i tokenS, to jednak nadal ma 3 regexs.


Poprosiłem o odpowiedź @ user0721090601 ++, ponieważ jest przydatna. Odnosi się również do kilku rzeczy, które od razu wydawały mi się idiomatycznie wyłączone w twoim kodzie i, co ważne, trzymają się tokens. Może to być odpowiedź, którą wolisz, która będzie fajna.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.