Comment puis-je définir une grammaire Raku pour analyser le texte TSV?

13

J'ai des données TSV

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

Je voudrais analyser ceci dans une liste de hachages

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

J'ai du mal à utiliser le métacaractère de nouvelle ligne pour délimiter la ligne d'en-tête des lignes de valeur. Ma définition 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    [email protected]
 321   stan    [email protected]
EOF
say Parser.parse($dat);

Mais cela revient Nil. Je pense que je ne comprends pas quelque chose de fondamental à propos des regex dans le raku.

littlebenlittle
la source
1
Nil. C'est assez stérile en ce qui concerne les commentaires, non? Pour le débogage, téléchargez la virgule si vous ne l'avez pas déjà fait et / ou consultez Comment améliorer le rapport d'erreurs dans les grammaires? . Vous avez obtenu Nilparce que votre modèle supposait une sémantique de retour arrière. Voir ma réponse à ce sujet. Je vous recommande d'éviter le retour en arrière. Voir la réponse de @ user0721090601 à ce sujet. Pour la praticité et la vitesse, voir la réponse de JJ. Aussi, réponse générale d'introduction à "Je veux analyser X avec Raku. Quelqu'un peut-il aider?" .
raiph
utilisez Grammar :: Tracer; #works for me
p6steve

Réponses:

12

La principale chose qui le jette est probablement celle qui \scorrespond à l'espace horizontal et vertical. Pour correspondre à l' espace horizontal juste, l' utilisation \h, et pour correspondre à l' espace juste vertical, \v.

Une petite recommandation que je ferais est d'éviter d'inclure les nouvelles lignes dans le jeton. Vous pouvez également utiliser les opérateurs d'alternance %ou %%, comme ils sont conçus pour gérer ce type de travail:

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

Le résultat de Parser.parse($dat)ceci est le suivant:

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

ce qui nous montre que la grammaire a tout analysé avec succès. Cependant, concentrons-nous sur la deuxième partie de votre question, que vous souhaitez qu'elle soit disponible dans une variable pour vous. Pour ce faire, vous devrez fournir une classe d'actions très simple pour ce projet. Vous créez simplement une classe dont les méthodes correspondent aux méthodes de votre grammaire (bien que des méthodes très simples, comme value/ headerqui ne nécessitent pas de traitement spécial en plus de la stringification, puissent être ignorées). Il existe des moyens plus créatifs / compacts de gérer le traitement de la vôtre, mais je vais suivre une approche assez rudimentaire pour l'illustration. Voici notre classe:

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

Chaque méthode a la signature ($/)qui est la variable de correspondance d'expression régulière. Alors maintenant, demandons quelles informations nous voulons de chaque jeton. Dans la ligne d'en-tête, nous voulons chacune des valeurs d'en-tête, dans une ligne. Donc:

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

Tout jeton avec un quantificateur sur elle sera traitée comme Positional, afin que nous puissions également accéder à chaque match d' en- tête individuel avec $<header>[0], $<header>[1]etc. Mais ce sont des objets de match, alors on les stringify rapidement. La makecommande permet à d'autres jetons d'accéder à ces données spéciales que nous avons créées.

Notre ligne de valeur sera identique, car les $<value>jetons sont ce dont nous nous soucions.

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

Lorsque nous arriverons à la dernière méthode, nous voudrons créer le tableau avec des hachages.

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

Ici, vous pouvez voir comment nous accédons aux trucs que nous avons traités headerRow()et valueRow(): Vous utilisez la .mademéthode. Parce qu'il y a plusieurs ValueRows, pour obtenir chacune de leurs madevaleurs, nous devons faire une carte (c'est une situation où j'ai tendance à écrire ma grammaire pour avoir simplement <header><data>dans la grammaire, et définir les données comme étant plusieurs lignes, mais c'est assez simple c'est pas trop mal).

Maintenant que nous avons les en-têtes et les lignes dans deux tableaux, il s'agit simplement d'en faire un tableau de hachages, ce que nous faisons dans la forboucle. Le flat @x Z @yjuste interconnecte les éléments, et l'affectation de hachage fait ce que nous voulons dire, mais il existe d'autres façons d'obtenir le tableau dans le hachage souhaité.

Une fois que vous avez terminé, vous venez de le makefaire, puis il sera disponible dans le madede l'analyse:

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

Il est assez courant de les intégrer dans une méthode, comme

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

De cette façon, vous pouvez simplement dire

my @entries = parse-tsv($dat);
say @entries[0]<Name>;    # test
say @entries[1]<Email>;   # [email protected]
user0721090601
la source
Je pense que j'écrirais la classe d'actions différemment. 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 }Vous devrez bien sûr l'instancier en premier :actions(Actions.new).
Brad Gilbert
@ BradGilbert oui, j'ai tendance à écrire mes classes d'actions pour éviter l'instanciation, mais si j'instancie, je le ferais probablement class Actions { has @!header; has %!entries … }et je demanderais simplement à valueRow d'ajouter directement les entrées pour que vous vous retrouviez avec juste method TOP ($!) { make %!entries }. Mais c'est Raku après tout et TIMTOWTDI :-)
user0721090601
En lisant ces informations ( docs.raku.org/language/regexes#Modified_quantifier:_%,_%% ), je pense que je comprends <valueRow>+ %% \n( <.ws>* %% <header>capturer des lignes délimitées par des sauts de ligne), mais en suivant cette logique, ce serait "capture facultative espaces blancs délimités par des espaces non blancs ". Suis-je en train de manquer quelque chose?
Christopher Bottoms
@ChristopherBottoms presque. Le <.ws>ne capture pas (le <ws>ferait). L'OP a noté que le format TSV peut commencer par un espace facultatif. En réalité, cela serait probablement encore mieux défini avec un jeton d'espacement de ligne défini comme \h*\n\h*, ce qui permettrait à valueRow d'être défini plus logiquement comme<header> % <.ws>
user0721090601
@ user0721090601 Je ne me souviens pas avoir lu %/ %%appelé une "alternance" op auparavant. Mais c'est le bon nom. (Considérant que l' utilisation de celui - ci pour |, ||et ses cousins m'a toujours frappé comme bizarre.). Je n'avais pas pensé à cette technique "à l'envers" auparavant. Mais c'est un joli idiome pour écrire des expressions rationnelles correspondant à un motif répété avec une assertion de séparateur non seulement entre les correspondances du motif mais aussi en le permettant aux deux extrémités (en utilisant %%), ou au début mais pas en fin (en utilisant %), comme un, euh, alternative à la logique de fin et non de début de ruleet :s. Agréable. :)
raiph
11

TL; DR: vous ne le faites pas. Utilisez simplement Text::CSV, qui est capable de gérer tous les formats.

Je vais montrer quel âge Text::CSVsera probablement utile:

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;

La partie clé ici est la fusion de données qui convertit le fichier initial en un ou plusieurs tableaux (en @data). Cependant, il n'est nécessaire que parce que la csvcommande n'est pas en mesure de traiter les chaînes; si les données sont dans un fichier, vous êtes prêt à partir.

La dernière ligne imprimera:

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

Le champ ID deviendra la clé du hachage, et le tout un tableau de hachages.

jjmerelo
la source
2
Vote en raison de l'aspect pratique. Je ne suis pas sûr, cependant, si l'OP vise plus à apprendre les grammaires (l'approche de ma réponse) ou a juste besoin d'analyser (l'approche de votre réponse). Dans les deux cas, il devrait être
prêt
2
A voté pour la même raison. :) J'avais pensé que l'OP visait à apprendre ce qu'ils avaient fait de mal en termes de sémantique d'expression rationnelle (d'où ma réponse), visant à apprendre comment le faire correctement (votre réponse), ou tout simplement besoin d'analyser (la réponse de JJ ). Travail en équipe. :)
raiph
7

Retour en regex arrière de TL; DR . tokens pas. C'est pourquoi votre motif ne correspond pas. Cette réponse se concentre sur l'explication de cela et sur la façon de corriger trivialement votre grammaire. Cependant, vous devriez probablement le réécrire, ou utiliser un analyseur existant, ce que vous devez absolument faire si vous voulez simplement analyser TSV plutôt que de vous renseigner sur les expressions rationnelles raku.

Un malentendu fondamental?

Je pense que je me méprends sur quelque chose de fondamental à propos des regex dans le raku.

(Si vous savez déjà que le terme "regexes" est très ambigu, pensez à sauter cette section.)

Une chose fondamentale que vous pourriez mal comprendre est le sens du mot «regexes». Voici quelques significations populaires que les gens supposent:

  • Expressions régulières formelles.

  • Perl regexes.

  • Expressions régulières compatibles Perl (PCRE).

  • Expressions de correspondance de modèle de texte appelées «expressions régulières» qui ressemblent à l'une des expressions ci-dessus et font quelque chose de similaire.

Aucune de ces significations n'est compatible les unes avec les autres.

Bien que les expressions rationnelles de Perl soient sémantiquement un surensemble d'expressions régulières formelles, elles sont beaucoup plus utiles à bien des égards, mais aussi plus vulnérables aux retours en arrière pathologiques .

Bien que les expressions régulières compatibles Perl soient compatibles avec Perl dans le sens où elles étaient à l' origine les mêmes que les expressions régulières Perl à la fin des années 1990, et dans le sens où Perl prend en charge les moteurs regex enfichables, y compris le moteur PCRE, la syntaxe des expressions régulières PCRE n'est pas identique à la norme Perl regex utilisé par défaut par Perl en 2020.

Et bien que les expressions d'appariement de motifs de texte appelées «expressions régulières» se ressemblent généralement un peu et correspondent toutes au texte, il existe des dizaines, voire des centaines, de variations de syntaxe, et même de sémantique pour la même syntaxe.

Les expressions de correspondance de motifs de texte Raku sont généralement appelées "règles" ou "expressions régulières". L'utilisation du terme «regexes» traduit le fait qu'ils ressemblent un peu à d'autres regexes (bien que la syntaxe ait été nettoyée). Le terme «règles» exprime le fait qu'elles font partie d'un ensemble beaucoup plus large de fonctionnalités et d'outils qui évoluent jusqu'à l'analyse (et au-delà).

La solution rapide

Avec l'aspect fondamental ci-dessus du mot «regexes», je peux maintenant passer à l'aspect fondamental du comportement de votre «regex» .

Si nous basculons trois des modèles de votre grammaire pour le tokendéclarant vers le regexdéclarant, votre grammaire fonctionne comme vous le vouliez:

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

La seule différence entre a tokenet a regexest qu'un a fait regexmarche arrière alors que ce tokenn'est pas le cas. Donc:

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

Pendant le traitement du dernier motif (qui pourrait être et est souvent appelé "regex", mais dont le déclarant réel ne l'est tokenpas regex), le \Savalera le 'b', tout comme il l'aura fait temporairement pendant le traitement du regex dans la ligne précédente. Mais, comme le modèle est déclaré en tant que token, le moteur de règles (alias "moteur regex") ne revient pas en arrière , donc la correspondance globale échoue.

C'est ce qui se passe dans votre PO.

La bonne solution

Une meilleure solution en général est de se sevrer de l' hypothèse d'un comportement de retour en arrière, car il peut être lent et même catastrophiquement lent (impossible à distinguer du blocage du programme) lorsqu'il est utilisé pour faire correspondre une chaîne construite de manière malveillante ou une chaîne avec une combinaison de caractères accidentellement malheureuse.

Parfois, les regexs sont appropriés. Par exemple, si vous écrivez une pièce unique et qu'une expression régulière fait le travail, alors vous avez terminé. C'est très bien. Cela fait partie de la raison pour laquelle la / ... /syntaxe dans raku déclare un modèle de retour arrière, tout comme regex. (Là encore, vous pouvez écrire / :r ... /si vous souhaitez activer le cliquet - "cliquet" signifie l'opposé de "backtrack", donc :rpasse une expression régulière à la tokensémantique.)

Parfois, le retour en arrière a toujours un rôle dans un contexte d'analyse. Par exemple, alors que la grammaire du raku évite généralement le retour en arrière et contient plutôt des centaines de rules et tokens, elle n'en a pas moins 3 regexs.


J'ai voté pour la réponse de @ user0721090601 ++ parce qu'elle est utile. Il aborde également plusieurs choses qui m'ont immédiatement semblé être idiomatiquement éteintes dans votre code, et, surtout, s'en tient à l' tokenart. C'est peut-être la réponse que vous préférez, qui sera cool.

raiph
la source