Wie kann ich eine Raku-Grammatik definieren, um TSV-Text zu analysieren?

13

Ich habe einige TSV-Daten

ID     Name    Email
   1   test    [email protected]
 321   stan    [email protected]

Ich möchte dies in eine Liste von Hashes analysieren

@entities[0]<Name> eq "test";
@entities[1]<Email> eq "[email protected]";

Ich habe Probleme mit der Verwendung des Newline-Metazeichens, um die Kopfzeile von den Wertzeilen abzugrenzen. Meine Grammatikdefinition:

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    [email protected]
 321   stan    [email protected]
EOF
say Parser.parse($dat);

Aber das kehrt zurück Nil. Ich glaube, ich verstehe etwas Grundlegendes an Regexen in Raku falsch.

littlebenlittle
quelle
1
Nil. Was das Feedback angeht, ist es ziemlich unfruchtbar, oder? Laden Sie zum Debuggen Commaide herunter, falls Sie dies noch nicht getan haben, und / oder lesen Sie Wie kann die Fehlerberichterstattung in Grammatiken verbessert werden? . Sie haben, Nilweil Ihr Muster eine Backtracking-Semantik angenommen hat. Siehe meine Antwort dazu. Ich empfehle Ihnen, Backtracking zu vermeiden. Siehe dazu die Antwort von @ user0721090601. Informationen zur praktischen Anwendbarkeit und Geschwindigkeit finden Sie in der Antwort von JJ. Außerdem einführende allgemeine Antwort auf "Ich möchte X mit Raku analysieren. Kann jemand helfen?" .
Raiph
benutze Grammar :: Tracer; #works for me
p6steve

Antworten:

12

Wahrscheinlich ist die Hauptsache, die es abwirft, die \sÜbereinstimmung mit dem horizontalen und vertikalen Raum. Verwenden Sie \hund nur den vertikalen Raum \v.

Eine kleine Empfehlung, die ich aussprechen würde, ist zu vermeiden, die Zeilenumbrüche in den Token aufzunehmen. Möglicherweise möchten Sie auch die Wechseloperatoren verwenden %oder %%, da sie für die Ausführung dieser Art von Arbeit ausgelegt sind:

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

Das Ergebnis Parser.parse($dat)hierfür ist folgendes:

「ID     Name    Email
   1   test    [email protected]
 321   stan    [email protected]
」
 headerRow => 「ID     Name    Email」
  header => 「ID」
  header => 「Name」
  header => 「Email」
 valueRow => 「   1   test    [email protected]」
  value => 「1」
  value => 「test」
  value => 「[email protected]」
 valueRow => 「 321   stan    [email protected]」
  value => 「321」
  value => 「stan」
  value => 「[email protected]」
 valueRow => 「」

Das zeigt uns, dass die Grammatik alles erfolgreich analysiert hat. Konzentrieren wir uns jedoch auf den zweiten Teil Ihrer Frage, der in einer Variablen für Sie verfügbar sein soll. Dazu müssen Sie eine Aktionsklasse angeben, die für dieses Projekt sehr einfach ist. Sie erstellen einfach eine Klasse, deren Methoden mit den Methoden Ihrer Grammatik übereinstimmen (obwohl sehr einfache Methoden wie value/ header, die neben der Stringifizierung keine spezielle Verarbeitung erfordern, ignoriert werden können). Es gibt einige kreativere / kompaktere Möglichkeiten, Ihre Verarbeitung zu handhaben, aber ich werde zur Veranschaulichung einen ziemlich rudimentären Ansatz wählen. Hier ist unsere Klasse:

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

Jede Methode hat die Signatur, ($/)die die Regex-Übereinstimmungsvariable ist. Fragen wir nun, welche Informationen wir von jedem Token wünschen. In der Kopfzeile wollen wir jeden der Kopfwerte in einer Reihe. Damit:

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

Alle Token mit einem Quantor auf sie wird als behandelt werden Positional, so dass wir auch jedes einzelne Header Spiel zugreifen konnte mit $<header>[0], $<header>[1]etc. Aber das sind Match - Objekte, so dass wir nur schnell sie stringify. Mit dem makeBefehl können andere Token auf diese speziellen Daten zugreifen, die wir erstellt haben.

Unsere Wertzeile wird identisch aussehen, da $<value>uns die Token wichtig sind.

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

Wenn wir zur letzten Methode kommen, wollen wir das Array mit Hashes erstellen.

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

Hier können Sie sehen, wie wir auf die Inhalte zugreifen, in denen wir verarbeitet haben, headerRow()und valueRow(): Sie verwenden die .madeMethode. Da es mehrere valueRows gibt made, müssen wir eine Map erstellen, um jeden ihrer Werte zu erhalten (dies ist eine Situation, in der ich dazu neige, meine Grammatik einfach <header><data>in die Grammatik zu schreiben und die Daten als mehrere Zeilen zu definieren, aber das ist es einfach genug ist es nicht schlecht).

Nachdem wir die Überschriften und Zeilen in zwei Arrays haben, müssen wir sie nur noch zu einem Array von Hashes machen, was wir in der forSchleife tun . Das flat @x Z @yinterkolonisiert nur die Elemente und die Hash-Zuweisung macht, was wir meinen, aber es gibt andere Möglichkeiten, das Array in den gewünschten Hash zu bekommen.

Sobald Sie fertig sind, haben Sie es einfach makeund dann wird es in madeder Analyse verfügbar sein :

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

Es ist ziemlich üblich, diese in eine Methode wie zu wickeln

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

Auf diese Weise kann man einfach sagen

my @entries = parse-tsv($dat);
say @entries[0]<Name>;    # test
say @entries[1]<Email>;   # [email protected]
user0721090601
quelle
Ich denke, ich würde die Aktionsklasse anders schreiben. 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 }Sie müssten es natürlich zuerst instanziieren :actions(Actions.new).
Brad Gilbert
@BradGilbert Ja, ich neige dazu, meine Aktionsklassen zu schreiben, um eine Instanziierung zu vermeiden, aber wenn ich instanziiere, würde ich wahrscheinlich class Actions { has @!header; has %!entries … }die valueRow-Einträge direkt hinzufügen, sodass Sie am Ende nur noch erhalten method TOP ($!) { make %!entries }. Aber das ist doch Raku und TIMTOWTDI :-)
user0721090601
Wenn ich diese Informationen lese ( docs.raku.org/language/regexes#Modified_quantifier:_%,_%% ), denke ich, dass ich das verstehe <valueRow>+ %% \n(Zeilen erfassen, die durch Zeilenumbrüche begrenzt sind), aber nach dieser Logik <.ws>* %% <header>wäre "Erfassung optional" Leerzeichen, das durch Nicht-Leerzeichen begrenzt ist ". Vermisse ich etwas
Christopher Bottoms
@ ChristopherBottoms fast. Das <.ws>erfasst nicht ( <ws>würde). Das OP stellte fest, dass das TSV-Format möglicherweise mit einem optionalen Leerzeichen beginnt. In der Realität wäre dies wahrscheinlich noch besser definiert, wenn ein Token mit Zeilenabstand als definiert \h*\n\h*wäre, wodurch die valueRow logischer definiert werden könnte als<header> % <.ws>
user0721090601
@ user0721090601 Ich kann mich nicht erinnern, zuvor eine "Abwechslung" gelesen %oder %%bezeichnet zu haben. Aber es ist der richtige Name. (Die Verwendung von für |, ||und Vetter hat mir immer seltsam.). Ich hatte noch nie an diese "Rückwärts" -Technik gedacht. Aber es ist eine nette Redewendung, um Regexes zu schreiben, die einem wiederholten Muster mit einer Trennzeichen-Behauptung entsprechen, nicht nur zwischen Übereinstimmungen des Musters, sondern es auch an beiden Enden (mit %%) oder am Anfang, aber nicht am Ende (mit %) als, ähm, zuzulassen. Alternative zur am Ende aber nicht Startlogik von ruleund :s. Nett. :)
Raiph
11

TL; DR: Das tust du nicht. Verwenden Sie einfach Text::CSV, die mit jedem Format umgehen kann.

Ich werde zeigen, wie alt Text::CSVwahrscheinlich nützlich sein wird:

use Text::CSV;

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

say @data.perl;

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

print $csv.perl;

Der Schlüsselteil hier ist das Datenmunging, das die ursprüngliche Datei in ein Array oder Arrays (in @data) konvertiert . Es wird jedoch nur benötigt, weil der csvBefehl keine Zeichenfolgen verarbeiten kann. Wenn sich Daten in einer Datei befinden, können Sie loslegen.

Die letzte Zeile wird gedruckt:

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

Das ID-Feld wird zum Schlüssel für den Hash und das Ganze zu einer Reihe von Hashes.

jjmerelo
quelle
2
Upvoting aus praktischen Gründen. Ich bin mir jedoch nicht sicher, ob das OP mehr darauf abzielt, Grammatiken zu lernen (Ansatz meiner Antwort) oder nur analysieren muss (Ansatz Ihrer Antwort). In jedem Fall sollte er
bereit
2
Aus dem gleichen Grund upvoted. :) Ich hatte gedacht, das OP könnte darauf abzielen, zu erfahren, was sie in Bezug auf die Regex-Semantik falsch gemacht haben (daher meine Antwort), zu lernen, wie man es richtig macht (Ihre Antwort) oder einfach nur zu analysieren (JJs Antwort) ). Zusammenarbeit. :)
Raiph
7

TL; DRs regex Backtrack. tokens nicht. Deshalb stimmt Ihr Muster nicht überein. Diese Antwort konzentriert sich darauf, dies zu erklären und wie Sie Ihre Grammatik trivial korrigieren können. Sie sollten es jedoch wahrscheinlich neu schreiben oder einen vorhandenen Parser verwenden. Dies sollten Sie auf jeden Fall tun, wenn Sie nur TSV analysieren möchten, anstatt mehr über Raku-Regexe zu erfahren.

Ein grundlegendes Missverständnis?

Ich glaube, ich verstehe etwas Grundlegendes an Regexen in Raku falsch.

(Wenn Sie bereits wissen, dass der Begriff "Regexes" sehr vieldeutig ist, sollten Sie diesen Abschnitt überspringen.)

Eine grundlegende Sache, die Sie möglicherweise missverstehen, ist die Bedeutung des Wortes "Regexes". Hier sind einige populäre Bedeutungen, die die Leute annehmen:

  • Formale reguläre Ausdrücke.

  • Perl Regexes.

  • Perl-kompatible reguläre Ausdrücke (PCRE).

  • Textmuster-Matching-Ausdrücke, die als "Regexes" bezeichnet werden und wie die oben genannten aussehen und etwas Ähnliches tun.

Keine dieser Bedeutungen ist miteinander kompatibel.

Während Perl-Regexe semantisch eine Obermenge formaler regulärer Ausdrücke sind, sind sie in vielerlei Hinsicht weitaus nützlicher, aber auch anfälliger für pathologisches Backtracking .

Während Perl-kompatible reguläre Ausdrücke in dem Sinne mit Perl kompatibel sind, dass sie ursprünglich mit Standard-Perl-Regexen Ende der 90er Jahre identisch waren, und in dem Sinne, dass Perl steckbare Regex-Engines einschließlich der PCRE-Engine unterstützt, ist die PCRE-Regex-Syntax nicht mit dem Standard identisch Perl-Regex wird von Perl im Jahr 2020 standardmäßig verwendet.

Und während Textmuster-Matching-Ausdrücke, die als "Regexes" bezeichnet werden, im Allgemeinen einander ähneln und alle mit Text übereinstimmen, gibt es Dutzende, vielleicht Hunderte von Variationen in der Syntax und sogar in der Semantik für dieselbe Syntax.

Raku-Textmuster-Übereinstimmungsausdrücke werden normalerweise entweder als "Regeln" oder als "reguläre Ausdrücke" bezeichnet. Die Verwendung des Begriffs "Regexes" vermittelt die Tatsache, dass sie etwas wie andere Regexes aussehen (obwohl die Syntax bereinigt wurde). Der Begriff "Regeln" vermittelt die Tatsache, dass sie Teil eines viel breiteren Satzes von Funktionen und Werkzeugen sind , die bis zum Parsen (und darüber hinaus) skaliert werden können.

Die schnelle Lösung

Mit dem oben genannten grundlegenden Aspekt des Wortes "Regexes" aus dem Weg kann ich mich nun dem grundlegenden Aspekt des Verhaltens Ihres "Regex" zuwenden .

Wenn wir drei der Muster in Ihrer Grammatik für den tokenDeklarator auf den regexDeklarator umstellen , funktioniert Ihre Grammatik wie beabsichtigt:

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

Der einzige Unterschied zwischen a tokenund a regexbesteht darin, dass a regexzurückverfolgt, während a tokendies nicht tut. Somit:

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

Während der Verarbeitung des letzten Musters (das könnte und wird oft als "Regex" bezeichnet, dessen tatsächlicher Deklarator jedoch tokennicht ist regex), \Swird das Muster verschluckt 'b', so wie es vorübergehend während der Verarbeitung des Regex in der vorherigen Zeile geschehen ist. Da das Muster jedoch als deklariert ist token, wird die Regelengine (auch als "Regex-Engine" bezeichnet) nicht zurückverfolgt , sodass die Gesamtübereinstimmung fehlschlägt.

Das ist es, was in Ihrem OP vor sich geht.

Die richtige Lösung

Eine bessere Lösung im Allgemeinen besteht darin, sich von der Annahme eines Backtracking-Verhaltens zu entwöhnen , da es langsam und sogar katastrophal langsam sein kann (nicht vom hängenden Programm zu unterscheiden), wenn es beim Abgleich mit einer böswillig konstruierten Zeichenfolge oder einer Zeichenfolge mit einer versehentlich unglücklichen Kombination von Zeichen verwendet wird.

Manchmal sind regexs angemessen. Wenn Sie beispielsweise eine einmalige Ausgabe schreiben und eine Regex die Aufgabe übernimmt, sind Sie fertig. Das ist gut. Dies ist einer der Gründe, warum die / ... /Syntax in Raku genau wie ein Backtracking-Muster deklariert regex. (Andererseits können Sie schreiben, / :r ... /wenn Sie das Ratschen einschalten möchten - "Ratsche" bedeutet das Gegenteil von "Backtrack", also :rschaltet eine Regex auf tokenSemantik um.)

Gelegentlich spielt das Zurückverfolgen in einem Analysekontext immer noch eine Rolle. Während die Grammatik für Raku im Allgemeinen das Zurückverfolgen meidet und stattdessen Hunderte von rules und tokens hat, hat sie dennoch 3 regexs.


Ich habe die Antwort von @ user0721090601 ++ positiv bewertet, weil sie nützlich ist. Es werden auch einige Dinge angesprochen, die mir in Ihrem Code sofort als idiomatisch erschienen, und vor allem bei tokens bleiben . Es kann durchaus die Antwort sein, die Sie bevorzugen, die cool sein wird.

Raiph
quelle