Como posso definir uma gramática Raku para analisar o texto TSV?

13

Eu tenho alguns dados TSV

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

Eu gostaria de analisar isso em uma lista de hashes

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

Estou tendo problemas com o uso do metacaractere de nova linha para delimitar a linha do cabeçalho das linhas de valor. Minha definição 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    [email protected]
 321   stan    [email protected]
EOF
say Parser.parse($dat);

Mas isso está voltando Nil. Acho que estou entendendo algo fundamental sobre as expressões regulares no raku.

littlebenlittle
fonte
11
Nil. É muito estéril no que diz respeito ao feedback, certo? Para depuração, faça o download commaide se você ainda não o fez e / ou consulte Como é possível melhorar o relatório de erros nas gramáticas? . Você assumiu Nilporque seu padrão assumiu a semântica de retrocesso. Veja minha resposta sobre isso. Eu recomendo que você evite voltar atrás. Veja a resposta de @ user0721090601 sobre isso. Para pura praticidade e velocidade, consulte a resposta de JJ. Além disso, a resposta geral introdutória a "Quero analisar X com Raku. Alguém pode ajudar?" .
raiph 3/03
use Grammar :: Tracer; #works for me
p6steve 21/03

Respostas:

12

Provavelmente a principal coisa que está jogando fora é que \scoincide com o espaço horizontal e vertical. Para corresponder apenas ao espaço horizontal, use \he para corresponder apenas ao espaço vertical \v,.

Uma pequena recomendação que eu faria é evitar incluir as novas linhas no token. Você também pode usar os operadores de alternância %ou %%, como eles foram projetados para lidar com este tipo de trabalho:

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

O resultado Parser.parse($dat)disso é o seguinte:

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

o que mostra que a gramática analisou tudo com êxito. No entanto, vamos nos concentrar na segunda parte da sua pergunta, que você deseja que ela esteja disponível em uma variável para você. Para fazer isso, você precisará fornecer uma classe de ações que seja muito simples para este projeto. Você acabou de criar uma classe cujos métodos correspondem aos métodos da sua gramática (embora os muito simples, como value/ headerque não exijam processamento especial além da stringificação, possam ser ignorados). Existem algumas maneiras mais criativas / compactas de lidar com o processamento, mas seguirei com uma abordagem bastante rudimentar para ilustração. Aqui está a nossa turma:

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

Cada método possui a assinatura ($/)que é a variável de correspondência de regex. Então agora, vamos perguntar quais informações queremos de cada token. Na linha do cabeçalho, queremos cada um dos valores do cabeçalho, em uma linha. Assim:

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

Qualquer token com um quantificador sobre ele será tratado como um Positional, por isso também pode acessar cada partida cabeçalho indivíduo com $<header>[0], $<header>[1]etc. Mas esses são objetos jogo, então nós rapidamente stringify eles. O makecomando permite que outros tokens acessem esses dados especiais que criamos.

Nossa linha de valor será idêntica, porque os $<value>tokens são o que nos interessa.

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

Quando chegarmos ao último método, queremos criar a matriz com 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;
  }

Aqui você pode ver como acessamos as coisas em que processamos headerRow()e valueRow(): Você usa o .mademétodo Como existem várias valueRows, para obter cada um de seus madevalores, precisamos fazer um mapa (essa é uma situação em que costumo escrever minha gramática simplesmente <header><data>na gramática e defino os dados como sendo várias linhas, mas isso é simples o suficiente, não é tão ruim).

Agora que temos os cabeçalhos e as linhas em duas matrizes, é simplesmente uma questão de torná-las uma matriz de hashes, o que fazemos no forloop. O flat @x Z @yjust intercolates os elementos, e a atribuição de hash faz o que queremos dizer, mas existem outras maneiras de obter a matriz no hash desejado.

Quando terminar, você apenas makeo fará e estará disponível na madeanálise:

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

É bastante comum agrupá-los em um método, como

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

Dessa forma, você pode apenas dizer

my @entries = parse-tsv($dat);
say @entries[0]<Name>;    # test
say @entries[1]<Email>;   # [email protected]
user0721090601
fonte
Eu acho que escreveria a classe de ações 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 }É claro que você teria que instanciar primeiro :actions(Actions.new).
Brad Gilbert
@BradGilbert sim, eu costumo escrever minhas classes de ações para evitar a instanciação, mas se instanciar, provavelmente faria class Actions { has @!header; has %!entries … }e apenas o valueRow adicionaria as entradas diretamente para que você acabasse com apenas method TOP ($!) { make %!entries }. Mas este é Raku afinal e TIMTOWTDI :-)
user0721090601
Ao ler essas informações ( docs.raku.org/language/regexes#Modified_quantifier:_%,_%% ), acho que entendi <valueRow>+ %% \n(Capturar linhas delimitadas por novas linhas), mas seguir essa lógica <.ws>* %% <header>seria "capturar opcional espaço em branco delimitado por não-espaço em branco ". Estou esquecendo de algo?
Christopher Bottoms
@ChristopherBottoms quase. O <.ws>não captura ( <ws>seria). O OP observou que o formato TSV pode começar com um espaço em branco opcional. Na realidade, isso provavelmente seria ainda melhor definido com um token de espaçamento definido como \h*\n\h*, o que permitiria que o valueRow fosse definido mais logicamente como<header> % <.ws>
user0721090601
@ user0721090601 Não me lembro de ter lido %/ %%denominado uma opção de "alternação" antes. Mas é o nome certo. (Considerando que a utilização do mesmo para |, ||e primos sempre me pareceu estranho.). Eu nunca tinha pensado nessa técnica "inversa" antes. Mas é um bom idioma para escrever expressões regulares que correspondam a um padrão repetido com alguma afirmação separadora, não apenas entre correspondências do padrão, mas também permitindo nas duas extremidades (usando %%) ou no início, mas não no final (usando %), como um, er, alternativa ao no final, mas não iniciar a lógica de rulee :s. Agradável. :)
raiph 12/03
11

TL; DR: você não. Basta usar Text::CSV, que é capaz de lidar com todos os formatos.

Vou mostrar quantos anos Text::CSVprovavelmente será útil:

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;

A parte principal aqui é a transferência de dados que converte o arquivo inicial em uma matriz ou matrizes (pol @data). Só é necessário, no entanto, porque o csvcomando não é capaz de lidar com strings; se os dados estiverem em um arquivo, você estará pronto.

A última linha será impressa:

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

O campo ID se tornará a chave do hash, e a coisa toda será uma matriz de hashes.

jjmerelo
fonte
2
Voto positivo devido à praticidade. Não tenho certeza, no entanto, se o OP está buscando mais aprender gramáticas (a abordagem da minha resposta) ou apenas precisando analisar (a abordagem da sua resposta). Em ambos os casos, ele deve estar
pronto
2
Votado pelo mesmo motivo. :) Eu achava que o OP poderia ter como objetivo aprender o que eles fizeram de errado em termos de semântica de expressões regulares (daí a minha resposta), com o objetivo de aprender como fazê-lo corretamente (sua resposta) ou apenas precisar analisar (a resposta de JJ ) Trabalho em equipe. :)
raiph 03/03
7

Backtrack de TL; DR regex . tokens não. É por isso que seu padrão não corresponde. Esta resposta se concentra em explicar isso e em como corrigir sua gramática trivialmente. No entanto, você provavelmente deve reescrevê-lo ou usar um analisador existente, que é o que você definitivamente deve fazer se quiser apenas analisar o TSV em vez de aprender sobre as expressões regulares raku.

Um mal-entendido fundamental?

Acho que estou entendendo mal algo fundamental sobre as expressões regulares no raku.

(Se você já sabe que o termo "regexes" é altamente ambíguo, pule esta seção.)

Uma coisa fundamental que você pode estar entendendo mal é o significado da palavra "regexes". Aqui estão alguns significados populares que as pessoas assumem:

  • Expressões regulares formais.

  • Regexes Perl.

  • Expressões regulares compatíveis com Perl (PCRE).

  • Expressões de correspondência de padrão de texto chamadas "regexes" que se parecem com qualquer uma das opções acima e fazem algo semelhante.

Nenhum desses significados é compatível um com o outro.

Embora as expressões regulares Perl sejam semanticamente um superconjunto de expressões regulares formais, elas são muito mais úteis de várias maneiras, mas também mais vulneráveis ​​ao retrocesso patológico .

Embora as expressões regulares compatíveis com Perl sejam compatíveis com Perl no sentido em que eram originalmente iguais às regexes padrão do Perl no final dos anos 90, e no sentido de que o Perl suporta mecanismos regex conectáveis, incluindo o mecanismo PCRE, a sintaxe do regex PCRE não é idêntica ao padrão Regex Perl usado por padrão pelo Perl em 2020.

E, embora as expressões correspondentes ao padrão de texto chamadas "regexes" geralmente se pareçam um com o outro e correspondam ao texto, existem dezenas, talvez centenas, de variações na sintaxe e até na semântica para a mesma sintaxe.

As expressões correspondentes de padrão de texto Raku são normalmente chamadas de "regras" ou "regexes". O uso do termo "regexes" transmite o fato de que eles se parecem com outros regexes (embora a sintaxe tenha sido limpa). O termo "regras" transmite o fato de que elas fazem parte de um conjunto muito mais amplo de recursos e ferramentas que se adaptam à análise (e além).

A solução rápida

Com o aspecto fundamental acima da palavra "regexes" fora do caminho, agora posso voltar ao aspecto fundamental do comportamento do seu "regex" .

Se alternarmos três dos padrões em sua gramática do tokendeclarador para o regexdeclarador, sua gramática funcionará como você deseja:

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

A única diferença entre a tokene a regexé que um regexrecua enquanto que um tokennão. Portanto:

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 o processamento do último padrão (que pode ser e costuma ser chamado de "regex", mas cujo declarador real tokennão é regex), o elemento \Sserá engolido 'b', exatamente como o fez temporariamente durante o processamento do regex na linha anterior. Mas, como o padrão é declarado como token, o mecanismo de regras (também conhecido como "mecanismo de expressão regular") não retorna , portanto a correspondência geral falha.

É isso que está acontecendo no seu OP.

A correção certa

Uma solução melhor em geral é afastar-se de assumir um comportamento de retorno, porque pode ser lento e até catastroficamente lento (indistinguível da interrupção do programa) quando usado na correspondência com uma string mal-intencionada ou com uma combinação acidentalmente infeliz de caracteres.

Às vezes, regexs são apropriados. Por exemplo, se você estiver escrevendo um registro único e uma regex fizer o trabalho, estará pronto. Isso é bom. Isso é parte da razão pela qual a / ... /sintaxe no raku declara um padrão de retorno, assim como regex. (Então, novamente, você pode escrever / :r ... /se desejar ativar a catraca - "catraca" significa o oposto de "retorno", portanto, :ralterna um regex para tokensemântica.)

Ocasionalmente, o retorno ainda desempenha um papel em um contexto de análise. Por exemplo, enquanto que a gramática para raku geralmente evita o retrocesso, e em vez disso tem centenas de rules e tokens, no entanto ele ainda tem 3 regexs.


Promovi a resposta a @ user0721090601 ++ porque é útil. Ele também aborda várias coisas que imediatamente me pareciam estar linguisticamente fora do seu código e, o que é mais importante, adere a tokens. Pode ser a resposta que você preferir, que será legal.

raiph
fonte