Bagaimana saya bisa mendefinisikan tata bahasa Raku untuk mem-parsing teks TSV?

13

Saya punya beberapa data TSV

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

Saya ingin menguraikan ini ke dalam daftar hash

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

Saya mengalami masalah dengan menggunakan metacharacter baris baru untuk membatasi baris header dari baris nilai. Definisi tata bahasa saya:

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

Tapi ini kembali Nil. Saya pikir saya salah memahami sesuatu yang mendasar tentang regex di raku.

littlebenlittle
sumber
1
Nil. Sejauh ini umpan baliknya cukup mandul, bukan? Untuk debugging, unduh komaide jika Anda belum melakukannya, dan / atau lihat Bagaimana pelaporan kesalahan dalam tata bahasa ditingkatkan? . Anda punya Nilkarena pola Anda diasumsikan mundur semantik. Lihat jawaban saya tentang itu. Saya sarankan Anda menghindari mundur. Lihat jawaban @ user0721090601 tentang itu. Untuk kepraktisan dan kecepatan, lihat jawaban JJ. Juga, Pengantar jawaban umum untuk "Saya ingin mengurai X dengan Raku. Adakah yang bisa membantu?" .
raiph
gunakan Grammar :: Tracer; #bekerja untuk saya
p6steve

Jawaban:

12

Mungkin hal utama yang membuangnya adalah yang \scocok dengan ruang horizontal dan vertikal. Untuk mencocokkan ruang horizontal saja, gunakan \h, dan untuk mencocokkan ruang vertikal saja \v,.

Satu rekomendasi kecil yang saya buat adalah untuk menghindari memasukkan baris baru dalam token. Anda mungkin juga ingin menggunakan operator alternatif %atau %%, karena mereka dirancang untuk menangani pekerjaan jenis ini:

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

Hasil dari Parser.parse($dat)ini adalah sebagai berikut:

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

yang menunjukkan kepada kita bahwa tata bahasa telah berhasil mengurai segalanya. Namun, mari kita fokus pada bagian kedua dari pertanyaan Anda, yang Anda inginkan tersedia dalam variabel untuk Anda. Untuk melakukan itu, Anda harus menyediakan kelas tindakan yang sangat sederhana untuk proyek ini. Anda cukup membuat kelas yang metodenya cocok dengan metode tata bahasa Anda (meskipun yang sangat sederhana, seperti value/ headeryang tidak memerlukan pemrosesan khusus selain pengetatan, dapat diabaikan). Ada beberapa cara yang lebih kreatif / ringkas untuk menangani pemrosesan milik Anda, tetapi saya akan menggunakan pendekatan ilustrasi yang cukup sederhana. Inilah kelas kami:

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

Setiap metode memiliki tanda tangan ($/)yang merupakan variabel pencocokan regex. Jadi sekarang, mari kita bertanya informasi apa yang kita inginkan dari setiap token. Di baris tajuk, kami ingin setiap nilai tajuk, berturut-turut. Begitu:

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

Setiap tanda dengan quantifier di atasnya akan diperlakukan sebagai Positional, sehingga kami juga bisa mengakses setiap pertandingan sundulan individu dengan $<header>[0], $<header>[1], dll Tapi mereka adalah pertandingan benda, jadi kami hanya cepat stringify mereka. The makeperintah memungkinkan token lain untuk mengakses data khusus yang kami buat.

Baris nilai kami akan terlihat identik, karena $<value>token adalah hal yang kami pedulikan.

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

Ketika kita sampai pada metode terakhir, kita akan ingin membuat array dengan 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;
  }

Di sini Anda dapat melihat bagaimana kami mengakses hal-hal yang kami proses headerRow()dan valueRow(): Anda menggunakan .mademetode ini. Karena ada beberapa valueRows, untuk mendapatkan masing-masing madenilainya, kita perlu melakukan peta (ini adalah situasi di mana saya cenderung menulis tata bahasa saya hanya <header><data>di tata bahasa, dan mendefinisikan data sebagai beberapa baris, tetapi ini adalah cukup sederhana itu tidak terlalu buruk).

Sekarang kita memiliki header dan baris dalam dua array, itu hanya masalah membuat mereka menjadi array hash, yang kita lakukan dalam forloop. The flat @x Z @yjust interolates elemen, dan penugasan hash Melakukan Apa Yang Kami Maksud, tetapi ada cara lain untuk mendapatkan array dalam hash yang Anda inginkan.

Setelah selesai, Anda baru saja makemelakukannya, dan kemudian akan tersedia di madebagian parse:

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

Cukup umum untuk membungkus ini menjadi suatu metode, seperti

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

Dengan begitu Anda bisa mengatakannya

my @entries = parse-tsv($dat);
say @entries[0]<Name>;    # test
say @entries[1]<Email>;   # [email protected]
pengguna0721090601
sumber
Saya pikir saya akan menulis class action yang berbeda. 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 }Anda tentu harus instantiate dulu :actions(Actions.new).
Brad Gilbert
@BradGilbert ya, saya cenderung menulis class action saya untuk menghindari instantiation, tetapi jika instantiating, saya mungkin akan melakukannya class Actions { has @!header; has %!entries … }dan hanya memiliki valueRow tambahkan entri secara langsung sehingga Anda berakhir dengan adil method TOP ($!) { make %!entries }. Tapi bagaimanapun juga ini adalah Raku dan TIMTOWTDI :-)
user0721090601
Dari membaca info ini ( docs.raku.org/language/regexes#Modified_quantifier:_%,_%% ), saya pikir saya mengerti <valueRow>+ %% \n(Tangkap baris yang dibatasi oleh baris baru), tetapi mengikuti logika itu, <.ws>* %% <header>akan menjadi "tangkap opsional spasi putih yang dibatasi oleh non-spasi putih ". Apakah saya melewatkan sesuatu?
Christopher Bottoms
@ChristopherBottoms hampir. Tidak <.ws>menangkap ( <ws>akan). OP mencatat bahwa format TSV dapat dimulai dengan spasi kosong opsional. Pada kenyataannya, ini mungkin akan lebih baik didefinisikan dengan token spasi-garis didefinisikan sebagai \h*\n\h*, yang akan memungkinkan nilaiRow didefinisikan secara lebih logis sebagai<header> % <.ws>
user0721090601
@ user0721090601 Saya tidak ingat pernah membaca %/ %%diistilahkan sebagai "pergantian" op sebelumnya. Tapi itu nama yang tepat. (Padahal untuk menggunakannya |, ||dan sepupu selalu menganggapku aneh.). Saya tidak pernah memikirkan teknik "mundur" ini sebelumnya. Tapi itu adalah ungkapan yang bagus untuk menulis regex yang cocok dengan pola yang diulang dengan beberapa pernyataan pemisah tidak hanya antara kecocokan pola tetapi juga memungkinkannya di kedua ujungnya (menggunakan %%), atau di awal tetapi tidak berakhir (menggunakan %), sebagai, eh, alternatif untuk di akhir tetapi tidak memulai logika ruledan :s. Bagus. :)
raiph
11

TL; DR: Anda tidak. Cukup gunakan Text::CSV, yang mampu menangani setiap format.

Saya akan menunjukkan berapa umur yang Text::CSVmungkin berguna:

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;

Bagian kuncinya di sini adalah data munging yang mengubah file awal menjadi array atau array (in @data). Namun itu hanya diperlukan, karena csvperintah tidak dapat menangani string; jika data ada dalam file, Anda dapat melakukannya.

Baris terakhir akan mencetak:

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

Bidang ID akan menjadi kunci hash, dan semuanya array hash.

jjmerelo
sumber
2
Upvoting karena kepraktisannya. Namun, saya tidak yakin apakah OP bertujuan lebih untuk mempelajari tata bahasa (pendekatan jawaban saya) atau hanya perlu menguraikan (pendekatan jawaban Anda). Dalam kedua kasus, ia harus baik untuk pergi :-)
user0721090601
2
Terpilih karena alasan yang sama. :) Saya telah berpikir OP mungkin bertujuan untuk mempelajari kesalahan mereka dalam hal semantik regex (maka jawaban saya), bertujuan untuk belajar bagaimana melakukannya dengan benar (jawaban Anda), atau hanya perlu menguraikan (jawaban JJ) ). Kerja tim. :)
raiph
7

TL; DR regex backtrack. tokentidak. Itu sebabnya pola Anda tidak cocok. Jawaban ini berfokus pada menjelaskan hal itu, dan bagaimana memperbaiki tata bahasa Anda secara sepele. Namun, Anda mungkin harus menulis ulang, atau menggunakan parser yang sudah ada, yang pasti harus Anda lakukan jika Anda hanya ingin mengurai TSV daripada belajar tentang raku regex.

Kesalahpahaman mendasar?

Saya pikir saya salah memahami sesuatu yang mendasar tentang regex di raku.

(Jika Anda sudah tahu istilah "regex" adalah yang sangat ambigu, pertimbangkan untuk melewatkan bagian ini.)

Satu hal mendasar yang mungkin Anda salah pahami adalah arti kata "regex". Berikut adalah beberapa arti populer yang diasumsikan oleh rakyat:

  • Ekspresi reguler formal.

  • Perl regex.

  • Perl Regular Regular Expressions (PCRE).

  • Ekspresi pencocokan pola teks yang disebut "regex" yang terlihat seperti di atas dan melakukan sesuatu yang serupa.

Tak satu pun dari makna ini yang kompatibel satu sama lain.

Sementara Perl regex secara semantik merupakan superset dari ekspresi reguler formal, mereka jauh lebih berguna dalam banyak hal tetapi juga lebih rentan terhadap pengulangan patologis .

Sementara Perl Kompatibel Regular Expressions kompatibel dengan Perl dalam arti mereka awalnya sama dengan Perl regex standar pada akhir 1990-an, dan dalam arti Perl mendukung mesin regex pluggable termasuk mesin PCRE, sintaks PCRE regex tidak identik dengan standar. Perl regex digunakan secara default oleh Perl pada tahun 2020.

Dan sementara ekspresi pencocokan pola teks yang disebut "regex" umumnya memang terlihat agak mirip satu sama lain, dan melakukan semua teks yang cocok, ada puluhan, mungkin ratusan, variasi dalam sintaksis, dan bahkan dalam semantik untuk sintaksis yang sama.

Ekspresi pencocokan pola teks Raku biasanya disebut "aturan" atau "regex". Penggunaan istilah "regex" menyampaikan fakta bahwa mereka terlihat agak seperti regex lain (meskipun sintaks telah dibersihkan). Istilah "aturan" menyampaikan fakta bahwa mereka adalah bagian dari serangkaian fitur dan alat yang jauh lebih luas yang meningkatkan hingga penguraian (dan seterusnya).

Perbaikan cepat

Dengan keluarnya aspek fundamental dari kata "regex", saya sekarang dapat beralih ke aspek mendasar dari perilaku "regex" Anda .

Jika kami mengalihkan tiga pola dalam tata bahasa Anda untuk tokendeklarator ke regexdeklarator, tata bahasa Anda berfungsi seperti yang Anda inginkan:

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

Satu-satunya perbedaan antara a tokendan a regexadalah regexbacktracks sedangkan a tokentidak. Jadi:

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

Selama pengolahan dari pola terakhir (yang bisa dan sering disebut "regex", tetapi yang sebenarnya deklarator adalah token, tidak regex), yang \Sakan menelan 'b', hanya karena sementara akan dilakukan selama pemrosesan dari regex di garis sebelumnya. Tapi, karena polanya dinyatakan sebagai a token, mesin aturan (alias "mesin regex") tidak mundur , sehingga kecocokan keseluruhan gagal.

Itulah yang terjadi di OP Anda.

Perbaikan yang tepat

Solusi yang lebih baik secara umum adalah menyapih diri Anda dari asumsi perilaku backtracking, karena itu bisa lambat dan bahkan sangat lambat (tidak dapat dibedakan dari program yang digantung) ketika digunakan dalam pencocokan dengan string yang dibuat dengan berbahaya atau yang dengan kombinasi karakter yang tidak disengaja secara kebetulan.

Terkadang regextepat. Misalnya, jika Anda menulis satu kali dan regex melakukan pekerjaan, maka Anda selesai. Tidak apa-apa. Itulah bagian dari alasan bahwa / ... /sintaks dalam raku menyatakan pola backtracking, sama seperti regex. (Kemudian lagi Anda dapat menulis / :r ... /jika Anda ingin mengaktifkan ratcheting - "ratchet" berarti kebalikan dari "mundur", jadi alihkan :rregex ke tokensemantik.)

Kadang-kadang mundur masih memiliki peran dalam konteks parsing. Sebagai contoh, walaupun tata bahasa untuk raku umumnya menghindari backtracking, dan sebaliknya memiliki ratusan rules dan tokens, namun tetap memiliki 3 regexs.


Saya telah memutakhirkan jawaban @ user0721090601 ++ karena ini berguna. Ini juga membahas beberapa hal yang menurut saya secara idiomatis mematikan kode Anda, dan, yang terpenting, melekat pada tokens. Mungkin itu jawaban yang Anda inginkan, yang akan keren.

raiph
sumber