¿Cómo puedo definir una gramática Raku para analizar el texto TSV?


13

Tengo algunos datos de TSV

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

Me gustaría analizar esto en una lista de hashes

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

Tengo problemas para usar el metacarácter de nueva línea para delimitar la fila de encabezado de las filas de valor. Mi definición gramatical:

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

Pero esto está volviendo Nil. Creo que estoy malinterpretando algo fundamental sobre las expresiones regulares en raku.


1
Nil. Es bastante árido en lo que respecta a los comentarios, ¿verdad? Para la depuración, descargue Commaide si aún no lo ha hecho, y / o vea ¿Cómo se pueden mejorar los informes de errores en las gramáticas? . Tienes Nilporque tu patrón asumió una semántica de retroceso. Mira mi respuesta al respecto. Te recomiendo que evites el retroceso. Ver la respuesta de @ user0721090601 al respecto. Para mayor practicidad y velocidad, vea la respuesta de JJ. Además, la respuesta general introductoria a "Quiero analizar X con Raku. ¿Alguien puede ayudar?" .
raiph

use Grammar :: Tracer; # funciona para mí
p6steve

Respuestas:


12

Probablemente lo principal que lo está descartando es que \scoincide con el espacio horizontal y vertical. Para que coincida con el espacio justo horizontal, uso \h, y para que coincida con el espacio justo vertical, \v.

Una pequeña recomendación que haría es evitar incluir las nuevas líneas en el token. También es posible que desee utilizar los operadores de alternancia %o %%, como están diseñados para manejar este tipo de trabajo:

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

El resultado de Parser.parse($dat)esto es el siguiente:

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

lo que nos muestra que la gramática ha analizado todo con éxito. Sin embargo, centrémonos en la segunda parte de su pregunta, que desea que esté disponible en una variable para usted. Para hacer eso, deberá proporcionar una clase de acciones que sea muy simple para este proyecto. Simplemente crea una clase cuyos métodos coinciden con los métodos de su gramática (aunque se pueden ignorar los muy simples, como value/ headerque no requieren un procesamiento especial además de la stringificación). Hay algunas formas más creativas / compactas de manejar el procesamiento de los suyos, pero seguiré con un enfoque bastante rudimentario para la ilustración. Aquí está nuestra clase:

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

Cada método tiene la firma, ($/)que es la variable de coincidencia de expresiones regulares. Ahora, preguntemos qué información queremos de cada token. En la fila del encabezado, queremos cada uno de los valores del encabezado, en una fila. Entonces:

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

Cualquier señal con un cuantificador en ella será tratada como una Positional, por lo que también podría tener acceso a cada partido encabezado individuo con $<header>[0], $<header>[1], etc, pero esos son los objetos de los partidos, por lo que sólo stringify rápidamente. El makecomando permite que otros tokens accedan a estos datos especiales que hemos creado.

Nuestra fila de valor se verá idénticamente, porque los $<value>tokens son lo que nos importa.

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

Cuando lleguemos al último método, querremos crear la matriz con hashes.

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

Aquí puede ver cómo accedemos a las cosas que procesamos headerRow()y valueRow(): Usted usa el .mademétodo. Debido a que hay múltiples valueRows, para obtener cada uno de sus madevalores, necesitamos hacer un mapa (esta es una situación en la que tiendo a escribir mi gramática para tenerla simplemente <header><data>en la gramática, y destruir los datos como filas múltiples, pero esto es lo suficientemente simple no es tan malo).

Ahora que tenemos los encabezados y las filas en dos matrices, simplemente se trata de convertirlos en una matriz de hashes, lo que hacemos en el forbucle. El flat @x Z @ysimplemente intercolates los elementos, y la asignación de hash hace lo que queremos decir, pero hay otras maneras de obtener la matriz de hash que desea.

Una vez que haya terminado, solo makelo hará, y luego estará disponible en madeel análisis:

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

Es bastante común envolverlos en un método, como

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

De esa manera solo puedes decir

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

Creo que escribiría la clase de acciones diferente. 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 }Por supuesto, primero deberías instanciarlo :actions(Actions.new).
Brad Gilbert

@BradGilbert, sí, tiendo a escribir mis clases de acciones para evitar class Actions { has @!header; has %!entries … }la creación de instancias, pero si crea una instancia, probablemente lo haga y solo tenga el valueRow agregue las entradas directamente para que termine con solo method TOP ($!) { make %!entries }. Pero esto es Raku después de todo y TIMTOWTDI :-)
user0721090601

Al leer esta información ( docs.raku.org/language/regexes#Modified_quantifier:_%,_%% ), creo que entiendo <valueRow>+ %% \n(Capturar filas que están delimitadas por nuevas líneas), pero siguiendo esa lógica, <.ws>* %% <header>sería "captura opcional espacios en blanco delimitados por espacios en blanco ". ¿Me estoy perdiendo de algo?
Christopher Bottoms

@ChristopherBottoms casi. El <.ws>no captura (lo <ws>haría). El OP señaló que el formato TSV puede comenzar con un espacio en blanco opcional. En realidad, esto probablemente se definiría aún mejor con un token de espacio entre líneas definido como \h*\n\h*, lo que permitiría que el valueRow se defina más lógicamente como<header> % <.ws>
user0721090601

@ user0721090601 No recuerdo haber leído %/ %%llamado una operación de "alternancia" antes. Pero es el nombre correcto. (Considerando que el uso de la misma para |, ||y primos siempre me ha parecido raro.). No había pensado en esta técnica "al revés" antes. Pero es un buen lenguaje para escribir expresiones regulares que coinciden con un patrón repetido con alguna afirmación de separador no solo entre coincidencias del patrón, sino que también lo permite en ambos extremos (usando %%) o al principio pero no al final (usando %), como, er, alternativa al final pero no iniciar la lógica de ruley :s. Agradable. :)
raiph

11

TL; DR: no lo haces. Simplemente use Text::CSV, que es capaz de manejar todos los formatos.

Mostraré cuántos años Text::CSVprobablemente será útil:

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 clave aquí es la mezcla de datos que convierte el archivo inicial en una matriz o matrices (en @data). Sin embargo, solo es necesario porque el csvcomando no puede manejar cadenas; si los datos están en un archivo, está listo para comenzar.

Se imprimirá la última línea:

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

El campo ID se convertirá en la clave del hash, y todo en una matriz de hash.


2
Votación a favor de la practicidad. Sin embargo, no estoy seguro de si el OP apunta más a aprender gramáticas (el enfoque de mi respuesta) o simplemente necesita analizar (el enfoque de su respuesta). En cualquier caso, debería estar listo :-)
user0721090601

2
Votaron por la misma razón. :) Pensé que el OP podría tener como objetivo aprender lo que habían hecho mal en términos de semántica de expresiones regulares (de ahí mi respuesta), con el objetivo de aprender cómo hacerlo bien (su respuesta), o simplemente tener que analizar (la respuesta de JJ ) Trabajo en equipo. :)
raiph

7

TL; DR regex s retroceso. tokens no. Es por eso que tu patrón no coincide. Esta respuesta se centra en explicar eso y cómo arreglar trivialmente su gramática. Sin embargo, probablemente debería reescribirlo, o usar un analizador existente, que es lo que definitivamente debe hacer si solo desea analizar TSV en lugar de aprender sobre expresiones regulares de raku.

¿Un malentendido fundamental?

Creo que estoy malinterpretando algo fundamental sobre las expresiones regulares en raku.

(Si ya sabe que el término "expresiones regulares" es muy ambiguo, considere omitir esta sección).

Una cosa fundamental que puede estar malentendiendo es el significado de la palabra "expresiones regulares". Aquí hay algunos significados populares que la gente asume:

  • Expresiones regulares formales.

  • Perl regexes.

  • Expresiones regulares compatibles con Perl (PCRE).

  • Expresiones de coincidencia de patrones de texto llamadas "expresiones regulares" que se parecen a cualquiera de las anteriores y hacen algo similar.

Ninguno de estos significados son compatibles entre sí.

Si bien las expresiones regulares de Perl son semánticamente un superconjunto de expresiones regulares formales, son mucho más útiles en muchos aspectos, pero también son más vulnerables a la regresión patológica .

Si bien las expresiones regulares compatibles con Perl son compatibles con Perl en el sentido de que originalmente eran las mismas que las expresiones regulares de Perl a fines de la década de 1990, y en el sentido de que Perl admite motores de expresiones regulares enchufables, incluido el motor PCRE, la sintaxis de expresión regular de PCRE no es idéntica al estándar Perl regex utilizado por defecto por Perl en 2020.

Y aunque las expresiones de coincidencia de patrones de texto llamadas "expresiones regulares" generalmente se parecen un poco entre sí y coinciden con todo el texto, hay docenas, quizás cientos, de variaciones en la sintaxis, e incluso en semántica para la misma sintaxis.

Las expresiones de coincidencia de patrones de texto Raku generalmente se denominan "reglas" o "expresiones regulares". El uso del término "expresiones regulares" transmite el hecho de que se parecen a otras expresiones regulares (aunque la sintaxis se ha limpiado). El término "reglas" transmite el hecho de que son parte de un conjunto mucho más amplio de características y herramientas que se amplían al análisis (y más allá).

La solución rápida

Con el aspecto fundamental anterior de la palabra "expresiones regulares" fuera del camino, ahora puedo pasar al aspecto fundamental del comportamiento de su "expresión regular" .

Si cambiamos tres de los patrones en su gramática para el tokendeclarador al regexdeclarador, su gramática funciona como usted pretendía:

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

La única diferencia entre a tokeny a regexes que a regexretrocede mientras tokenque a no. Así:

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 el procesamiento del último patrón (que podría llamarse "expresión regular", pero cuyo declarante real es token, no regex), \Sse tragará 'b', tal como lo hizo temporalmente durante el procesamiento de la expresión regular en la línea anterior. Pero, debido a que el patrón se declara como a token, el motor de reglas (también conocido como "motor de expresiones regulares") no retrocede , por lo que la coincidencia general falla.

Eso es lo que está sucediendo en su OP.

La solución correcta

Una mejor solución en general es dejar de asumir un comportamiento de retroceso, porque puede ser lento e incluso catastróficamente lento (indistinguible del programa que se cuelga) cuando se usa para hacer coincidir una cadena construida maliciosamente o una con una combinación de caracteres accidentalmente desafortunada.

A veces los regexs son apropiados. Por ejemplo, si está escribiendo una única y una expresión regular hace el trabajo, entonces ya está. Esta bien. Esa es parte de la razón por la que la / ... /sintaxis en raku declara un patrón de retroceso, al igual que regex. (De nuevo, puede escribir / :r ... /si desea activar el trinquete : "trinquete" significa lo contrario de "retroceso", por lo que :rcambia una expresión regular a tokensemántica).

Ocasionalmente, el retroceso todavía tiene un papel en un contexto de análisis. Por ejemplo, mientras que la gramática para raku generalmente evita el retroceso, y en cambio tiene cientos de rulesy tokens, sin embargo, todavía tiene 3 regexs.


He votado a favor de la respuesta de @ user0721090601 ++ porque es útil. También aborda varias cosas que inmediatamente me parecieron idiomáticamente fuera de su código, y, lo que es más importante, se adhiere a tokens. Bien puede ser la respuesta que prefiera, que será genial.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.