TSVテキストを解析するためのRaku文法を定義するにはどうすればよいですか?


13

TSVデータがあります

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

これを解析してハッシュのリストにしたい

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

改行メタ文字を使用してヘッダー行と値行を区切るのに問題があります。私の文法定義:

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

しかし、これは戻ってきていNilます。楽の正規表現についての基本的なことを誤解していると思います。


1
Nil。フィードバックに関してはかなり不毛ですよね?デバッグするには、まだ行っていない場合は、commideをダウンロードしてください。または、「文法のエラー報告どのように改善できるか」を参照してください。あなたはNil、パターンがバックトラックのセマンティクスを想定していることを知った それについての私の答えを見てください。バックトラックを避けることをお勧めします。これについては@ user0721090601の回答を参照してください。実用性とスピードについては、JJの回答をご覧ください。また、「RakuでXを解析したいのですが、だれでも手伝ってくれますか?」に対する一般的な回答です。
レイフ

Grammar :: Tracerを使用します。#自分用に機能する
p6steve

回答:


12

おそらくそれを捨てている主なものは、\s水平方向垂直方向のスペースを一致させることです。水平方向のスペースだけを一致させるにはを使用し\h、垂直方向のスペースだけを一致させるにはを使用します\v

私が行う1つの小さな推奨事項は、トークンに改行を含めないことです。また、これらのタイプの作業を処理するように設計されているため、代替演算子%またはを使用することもでき%%ます。

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

この結果はParser.parse($dat)次のとおりです。

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

これは、文法がすべてを正常に解析したことを示しています。ただし、変数の中で利用できるようにしたいという質問の2番目の部分に焦点を当てましょう。そのためには、このプロジェクトで非常に簡単なアクションクラスを指定する必要があります。メソッドが文法のメソッドと一致するクラスを作成するだけです(ただし、value/ などの非常に単純なものは、header文字列化以外の特別な処理を必要とせず、無視できます)。あなたの処理を処理するためのより創造的/コンパクトな方法がいくつかありますが、私はかなり初歩的なアプローチで説明します。ここに私たちのクラスがあります:

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

各メソッド($/)には、正規表現一致変数であるシグネチャがあります。それでは、各トークンからどのような情報が必要かを尋ねましょう。ヘッダー行では、各ヘッダー値を続けて表示します。そう:

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

その上に数量詞を持つ任意のトークンは、として扱われますPositional、我々はまたして、個々のヘッダーの試合にアクセスできるよう、$<header>[0]$<header>[1]私たちはすぐにそれらを文字列化して、などしかし、それらが一致オブジェクトです。このmakeコマンドにより、他のトークンが、作成したこの特別なデータにアクセスできるようになります。

$<value>トークンが重要なので、値の行は同じに見えます。

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

最後のメソッドに到達したら、ハッシュを含む配列を作成します。

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

ここでは、我々は中に処理さのものへのアクセス方法を見ることができますheaderRow()valueRow():あなたが使用.madeする方法を。複数のvalueRowsがあるため、それぞれのmade値を取得するために、マップを実行する必要があります(これは、文法を単純<header><data>に持つように文法を記述し、データを複数の行として定義する傾向がある状況ですが、これは十分にシンプルで、それほど悪くはありません)。

ヘッダーと行が2つの配列になっているので、forループで行うように、それらをハッシュの配列にするだけです。flat @x Z @y単に要素をintercolates、ハッシュ割り当ては、私たちが何を意味するかんが、あなたがしたいハッシュの配列を取得するための他の方法があります。

完了したら、それだけmakeで、madeパースで利用できるようになります。

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

これらを次のようなメソッドにラップすることはかなり一般的です

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

そうすればあなたはただ言うことができます

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

アクションクラスを別の方法で書くと思います。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 }もちろん、最初にそれをインスタンス化する必要があります:actions(Actions.new)
Brad Gilbert

@BradGilbertはい、インスタンス化を回避するためにアクションクラスを作成する傾向がありますが、インスタンス化する場合はclass Actions { has @!header; has %!entries … }、おそらくvalueRowにエントリを直接追加させ、最終的にはだけになるようにしますmethod TOP ($!) { make %!entries }。でも結局これはRakuで
TIMTOWTDI

この情報(docs.raku.org/language/regexes#Modified_quantifier:_%,_%%)を読むと、<valueRow>+ %% \n(改行で区切られた行をキャプチャする)は理解できると思いますが、そのロジックに従うと、<.ws>* %% <header>「オプションのキャプチャ」になります非空白文字で区切られた空白文字」。何か不足していますか?
クリストファーボトムズ

@ChristopherBottomsほぼ。の<.ws>(捕捉しない<ws>でしょう)。OPは、TSV形式がオプションの空白で始まる場合があることを指摘しました。実際には、これはおそらく、より良いライン間隔トークンとして定義して定義されるだろう\h*\n\h*valueRowは、より論理的のように定義することを可能と思われる、<header> % <.ws>
user0721090601

@ user0721090601私は読んで覚えていません%/ %%の前に「交代」のOPと呼ばれます。しかし、それは正しい名前です。(のためのそれを使用するのに対し|||いとこいつも変なとして私を襲いました。)。私はこれまでこの「逆方向」のテクニックを考えていませんでした。しかし、これは、繰り返しパターンに一致する正規表現を、パターンの一致間だけでなく、両端で(%%またはを使用して)、または開始ではなく終了(を使用%)して、ruleおよびの開始ロジックではなく、最後のの代替:s。いいね。:)
レイフ

11

TL; DR:そうではありません。Text::CSVすべてのフォーマットを処理できるを使用してください。

私はText::CSVおそらく何歳になると便利かを示します。

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;

ここで重要な部分は、初期ファイルを1つまたは複数の配列に変換するデータ変更です(@data)。ただし、このcsvコマンドは文字列を処理できないため、必要なのはこれだけです。データがファイル内にある場合は、問題ありません。

最後の行が印刷されます:

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

IDフィールドはハッシュのキーとなり、全体がハッシュの配列になります。


2
実用性のために賛成票を投じます。しかし、OPが文法を学ぶことをもっと目指しているのか(私の答えのアプローチ)、解析する必要があるだけなのか(あなたの答えのアプローチ)はわかりません。どちらの場合でも、彼は行くべきです:-)
user0721090601

2
同じ理由で賛成。:)私は、OPが正規表現の意味で彼らが間違ったことを学ぶことを目的としているため(それが私の答えです)、正しい方法(あなたの答え)を学ぶことを目的としている、または単に解析する必要がある(JJの答え) )。チームワーク。:)
レイフ

7

TL; DR regexのバックトラック。tokenありません。そのため、パターンが一致しません。この回答では、その説明と、簡単に文法を修正する方法について説明します。ただし、おそらくそれを書き直すか、既存のパーサーを使用する必要があります。これは、raku正規表現について学ぶのではなく、TSVを解析したいだけなら確実に行うべきことです。

基本的な誤解?

楽の正規表現についての基本的なことを誤解していると思います。

(「正規表現」という用語が非常に曖昧なものであることをすでに知っている場合は、このセクションをスキップすることを検討してください。)

あなたが誤解しているかもしれない1つの基本的なことは、「正規表現」という言葉の意味です。ここでは、人々が想定するいくつかの一般的な意味を示します。

  • 正式な正規表現。

  • Perl正規表現。

  • Perl互換正規表現(PCRE)。

  • 上記のいずれかのように見え、同様のことを行う「正規表現」と呼ばれるテキストパターンマッチング式。

これらの意味は互いに互換性がありません。

Perlの正規表現は、意味的には正規の正規表現のスーパーセットですが、多くの点ではるかに有用ですが、病理学的なバックトラッキングに対して脆弱です。です。

Perl互換の正規表現は、もともとはPerlと互換性がありますが、、1990年代後半の標準のPerl正規表現と同じであったという意味で、およびPerlがPCREエンジンを含むプラグイン可能な正規表現エンジンをサポートするという意味で、Perlとますが、PCRE正規表現構文は標準と同じではありません2020年にPerlによってデフォルトで使用されるPerl正規表現。

また、「正規表現」と呼ばれるテキストパターンマッチング式は一般に互いに似ており、すべてテキストに一致しますが、構文には数十、おそらく数百のバリエーションがあり、同じ構文のセマンティクスにもあります。

Rakuテキストパターンマッチング式は、通常「ルール」または「正規表現」と呼ばれます。「正規表現」という用語の使用は、それらが他の正規表現のように見えるという事実を伝えます(構文はクリーンアップされていますが)。「ルール」という用語は、それらがはるかに広範な機能とツールのセットの一部であるという事実を伝えます構文解析(およびそれ以降)に拡張する。

クイックフィックス

上記の「正規表現」という言葉の基本的な側面を理解したところで、「正規表現」の動作の基本的な側面に目を向けることができます。

token宣言子の文法内の3つのパターンを宣言子に切り替えると、文法はregex意図したとおりに機能します。

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

a tokenとa の唯一の違いregexは、regexバックトラックするのに対してa tokenはしないということです。したがって:

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

最後のパターンの処理中(これは「正規表現」と呼ばれることもあり、多くの場合「正規表現」と呼ばれますが、その実際の宣言子はでありtoken、ではありませんregex)、前の行の正規表現の処理中に一時的に行ったように、は\Sを飲み込み'b'ます。ただし、パターンはとして宣言されているtokenため、ルールエンジン(別名「正規表現エンジン」)はバックトラックしません。ため、全体的な一致は失敗します。

それがあなたのOPで起こっていることです。

適切な修正

一般に、より良い解決策は、バックトラック動作を想定しないことです。悪意を持って作成された文字列または誤って文字の組み合わせを持つ文字列との照合に使用すると、速度が遅くなり、破壊的に遅くなることもあります(プログラムのハングと区別がつきません)。

regexsが適切な場合もあります。たとえば、1回限りの記述を行っていて、正規表現がその仕事をする場合は、これで完了です。それはいいです。これが/ ... /、rakuの構文がのようにバックトラックパターンを宣言する理由の1つですregex。(ラチェット/ :r ... /をオンにしたい場合は、もう一度書くことができます。「ラチェット」は「バックトラック」の反対を意味するため、正規表現をセマンティクスに切り替えます。):rtoken

時折、バックトラックが解析コンテキストで役割を果たします。たとえば、rakuの文法は一般的にバックトラックを避け、代わりに数百とrulesをtoken持っていますが、それでも3 regexsを持っています。


@ user0721090601 ++の回答は有用なので、私は賛成しています。また、すぐに私があなたのコードで慣用的にオフになっているように見え、そして重要なのはtokens に固執するいくつかの事柄にも対処しています。それはあなたが好む答えかもしれませんが、それはクールです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.