Come posso definire una grammatica di Raku per analizzare il testo TSV?


13

Ho alcuni dati TSV

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

Vorrei analizzarlo in un elenco di hash

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

Sto riscontrando problemi con l'utilizzo del metacarattere newline per delimitare la riga di intestazione dalle righe dei valori. La mia definizione grammaticale:

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

Ma questo sta tornando Nil. Penso di aver frainteso qualcosa di fondamentale sui regex in raku.


1
Nil. È piuttosto sterile per quanto riguarda il feedback, giusto? Per il debug, scarica virgola se non lo hai già fatto e / o vedi Come è possibile migliorare la segnalazione degli errori nelle grammatiche? . Hai avuto Nilil motivo per cui il tuo modello ha assunto la semantica di backtracking. Vedi la mia risposta a riguardo. Ti consiglio di evitare il backtracking. Vedi la risposta di @ user0721090601 al riguardo. Per pura praticità e velocità, vedi la risposta di JJ. Inoltre, risposta generale introduttiva a "Voglio analizzare X con Raku. Qualcuno può aiutare?" .
Raiph

usa Grammar :: Tracer; #works for me
p6steve

Risposte:


12

Probabilmente la cosa principale che lo butta via è che \scorrisponde allo spazio orizzontale e verticale. Per abbinare spazio solo orizzontale, l'uso \h, e per abbinare lo spazio solo in verticale, \v.

Una piccola raccomandazione che vorrei fare è di evitare di includere le nuove righe nel token. Potresti anche voler utilizzare gli operatori di alternanza %o %%, poiché sono progettati per gestire questo tipo di lavoro:

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

Il risultato di Parser.parse($dat)questo è il seguente:

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

che ci mostra che la grammatica ha analizzato con successo tutto. Tuttavia, concentriamoci sulla seconda parte della tua domanda, che vuoi che sia disponibile in una variabile per te. Per fare ciò, dovrai fornire una classe di azioni molto semplice per questo progetto. Devi solo creare una classe i cui metodi corrispondono ai metodi della tua grammatica (anche se quelli molto semplici, come value/ headerche non richiedono un'elaborazione speciale oltre alla stringa, possono essere ignorati). Ci sono alcuni modi più creativi / compatti per gestire la tua elaborazione, ma seguirò un approccio abbastanza rudimentale per l'illustrazione. Ecco la nostra classe:

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

Ogni metodo ha la firma ($/)che è la variabile di corrispondenza regex. Quindi ora chiediamo quali informazioni vogliamo da ogni token. Nella riga di intestazione, vogliamo ciascuno dei valori di intestazione, in una riga. Così:

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

Ogni token con un quantificatore su di esso sarà trattata come una Positional, così abbiamo potuto anche accedere l'intestazione di ogni incontro individuale con $<header>[0], $<header>[1]ecc Ma quelli sono oggetti partita, quindi abbiamo rapidamente stringa i. Il makecomando consente ad altri token di accedere a questi dati speciali che abbiamo creato.

La nostra riga di valore avrà un aspetto identico, perché i $<value>token sono ciò che ci interessa.

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

Quando arriveremo all'ultimo metodo, vorremmo creare l'array con gli hash.

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

Qui puoi vedere come accediamo alle cose in cui abbiamo elaborato headerRow()e valueRow(): usi il .mademetodo. Perché ci sono più valueRows, per ottenere ciascuno dei loro madevalori, dobbiamo fare una mappa (questa è una situazione in cui tendo a scrivere la mia grammatica per avere semplicemente <header><data>nella grammatica, e definire i dati come più righe, ma questo è abbastanza semplice non è poi così male).

Ora che abbiamo le intestazioni e le righe in due array, è semplicemente una questione di renderli una serie di hash, cosa che facciamo nel forloop. Il flat @x Z @ygiusto intercollega gli elementi e l'assegnazione hash fa ciò che intendiamo, ma ci sono altri modi per ottenere l'array nell'hash desiderato.

Una volta terminato, basta makee quindi sarà disponibile nel madeparse:

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

È abbastanza comune avvolgerli in un metodo, come

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

In questo modo puoi solo dire

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

Penso che scriverei la classe di azioni in modo diverso. 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 }Ovviamente dovresti prima istanziarlo :actions(Actions.new).
Brad Gilbert,

@BradGilbert sì, tendo a scrivere le mie classi di azioni per evitare l'istanza, ma se ho un'istanza, probabilmente lo farei class Actions { has @!header; has %!entries … }e avrei solo il valoreRow aggiungere direttamente le voci in modo da finire con solo method TOP ($!) { make %!entries }. Ma dopo tutto questo è Raku e TIMTOWTDI :-)
user0721090601

Dalla lettura di queste informazioni ( docs.raku.org/language/regexes#Modified_quantifier:_%,_%% ), penso di aver capito <valueRow>+ %% \n(Cattura righe delimitate da newline), ma seguendo questa logica, <.ws>* %% <header>sarebbe "cattura opzionale spazio che è delimitato da un non-spazio ". Mi sto perdendo qualcosa?
Christopher Bottoms

@ChristopherBottoms quasi. Il <.ws>non cattura ( <ws>vorrebbe). L'OP ha osservato che il formato TSV può iniziare con uno spazio bianco opzionale. In realtà, questo sarebbe probabilmente ancora meglio definito con un token di interlinea definito come \h*\n\h*, che consentirebbe di definire il valoreRow in modo più logico come<header> % <.ws>
user0721090601

@ user0721090601 Non ricordo prima di aver letto %/ %%definito un'opzione di "alternanza". Ma è il nome giusto. (Considerando che l'utilizzazione di esso per |, ||e cugini mi ha sempre colpito come strano.). Non avevo mai pensato a questa tecnica "all'indietro" prima. Ma è un bel linguaggio per scrivere regex che abbinano un pattern ripetuto con un'asserzione di separatore non solo tra le corrispondenze del pattern ma anche permetterlo ad entrambe le estremità (usando %%), o all'inizio ma non alla fine (usando %), come, er, alternativa alla fine ma non avvia la logica di rulee :s. Bello. :)
raiph

11

TL; DR: non lo fai. Basta usare Text::CSV, che è in grado di gestire tutti i formati.

Mostrerò quanti anni Text::CSVprobabilmente sarà utile:

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;

La parte chiave qui è il munging dei dati che converte il file iniziale in un array o array (in @data). È necessario solo, tuttavia, perché il csvcomando non è in grado di gestire le stringhe; se i dati sono in un file, sei a posto.

L'ultima riga stamperà:

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

Il campo ID diventerà la chiave dell'hash e il tutto un array di hash.


2
Voto a causa della praticità. Non sono sicuro, tuttavia, se il PO punta di più a imparare le grammatiche (approccio della mia risposta) o deve solo analizzare (approccio della tua risposta). In entrambi i casi, dovrebbe andare bene :-)
user0721090601

2
Eseguito l'upgrade per lo stesso motivo. :) Avevo pensato che il PO potesse mirare a capire cosa avevano fatto di sbagliato in termini di semantica regex (da qui la mia risposta), mirare a imparare come farlo nel modo giusto (la tua risposta), o semplicemente a dover analizzare (la risposta di JJ ). Lavoro di squadra. :)
raiph

7

TL; regex backtrack di DR . tokens no. Ecco perché il tuo modello non corrisponde. Questa risposta si concentra sulla spiegazione di ciò e su come risolvere banalmente la tua grammatica. Tuttavia, dovresti probabilmente riscriverlo o utilizzare un parser esistente, che è quello che dovresti assolutamente fare se vuoi semplicemente analizzare TSV piuttosto che conoscere le regex di raku.

Un equivoco fondamentale?

Penso di aver frainteso qualcosa di fondamentale sui regex in raku.

(Se sai già che il termine "regexes" è molto ambiguo, considera di saltare questa sezione.)

Una cosa fondamentale che potresti fraintendere è il significato della parola "regexes". Ecco alcuni significati popolari che la gente assume:

  • Espressioni regolari formali.

  • Regex del Perl.

  • Espressioni regolari compatibili Perl (PCRE).

  • Espressioni di corrispondenza del modello di testo chiamate "regex" che assomigliano a una qualsiasi delle precedenti e fanno qualcosa di simile.

Nessuno di questi significati è compatibile tra loro.

Mentre le regex del Perl sono semanticamente un superset di espressioni formali regolari, sono molto più utili in molti modi ma anche più vulnerabili al backtracking patologico .

Mentre le espressioni regolari compatibili Perl sono compatibili con Perl nel senso che erano originariamente uguali alle regex Perl standard alla fine degli anni '90, e nel senso che Perl supporta motori regex collegabili incluso il motore PCRE, la sintassi regex PCRE non è identica allo standard Perl regex utilizzato di default da Perl nel 2020.

E mentre le espressioni di corrispondenza del modello di testo chiamate "regexes" generalmente si assomigliano in qualche modo l'una all'altra, e corrispondono tutte al testo, ci sono dozzine, forse centinaia, di variazioni nella sintassi e persino in semantica per la stessa sintassi.

Le espressioni di corrispondenza del modello di testo Raku sono in genere chiamate "regole" o "regex". L'uso del termine "regexes" esprime il fatto che sembrano in qualche modo simili ad altre regex (sebbene la sintassi sia stata ripulita). Il termine "regole" indica il fatto che fanno parte di un insieme molto più ampio di funzionalità e strumenti che si adattano all'analisi (e oltre).

La soluzione rapida

Con l'aspetto fondamentale sopra della parola "regexes" fuori mano, ora posso passare all'aspetto fondamentale del comportamento della tua "regex" .

Se passiamo tre dei modelli nella tua grammatica per il tokendichiaratore al regexdichiaratore, la tua grammatica funziona come previsto:

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

L'unica differenza tra a tokene a regexè che un regexbacktracks mentre a tokennon lo fa. Così:

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

Durante l'elaborazione dell'ultimo modello (che potrebbe essere e spesso viene chiamato "regex", ma il cui vero dichiaratore è token, non regex), lo \Singhiottirà 'b', proprio come avrebbe fatto temporaneamente durante l'elaborazione del regex nella riga precedente. Tuttavia, poiché il modello è dichiarato come a token, il motore delle regole (noto anche come "motore regex") non esegue il backtracking , pertanto la corrispondenza complessiva non riesce.

Questo è ciò che sta succedendo nel tuo PO.

La soluzione giusta

Una soluzione migliore in generale è evitare di assumere un comportamento di backtracking, perché può essere lento e persino catastroficamente lento (indistinguibile dal blocco del programma) se utilizzato in abbinamento a una stringa costruita in modo pericoloso o a una combinazione accidentale di caratteri.

A volte regexsono appropriati. Ad esempio, se stai scrivendo una tantum e una regex fa il lavoro, allora hai finito. Va bene. Questo fa parte del motivo per cui la / ... /sintassi in raku dichiara un modello di backtracking, proprio come regex. (Quindi puoi scrivere / :r ... /se vuoi attivare il cricchetto - "cricchetto" significa l'opposto di "backtrack", quindi :rpassa una regex alla tokensemantica.)

Occasionalmente il backtracking ha ancora un ruolo in un contesto di analisi. Per esempio, mentre la grammatica per raku rifugge generalmente backtracking, e invece ha centinaia di rules e tokens, essa ha comunque ancora 3 regexs.


Ho votato la risposta di @ user0721090601 ++ perché è utile. Affronta anche diverse cose che mi sono sembrate immediatamente idiomaticamente disattivate nel tuo codice e, cosa importante, si attengono a tokens. Potrebbe essere la risposta che preferisci, che sarà interessante.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.