ฉันจะกำหนดไวยากรณ์ Raku เพื่อแยกวิเคราะห์ข้อความ TSV ได้อย่างไร


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แล้ว ฉันคิดว่าฉันเข้าใจผิดบางอย่างเกี่ยวกับพื้นฐานของ regexes ใน raku


1
Nil. มันค่อนข้างแห้งแล้งหากมีความคิดเห็นเกิดขึ้นใช่ไหม สำหรับการดีบักให้ดาวน์โหลดคอมม่าถ้าคุณยังไม่ได้ทำและ / หรือดูการรายงานข้อผิดพลาดในแกรมม่าสามารถปรับปรุงได้อย่างไร . คุณได้Nilเพราะรูปแบบของคุณคิดว่าความหมายย้อนรอย ดูคำตอบของฉันเกี่ยวกับเรื่องนั้น ฉันแนะนำให้คุณหลีกเลี่ยงการย้อนรอย ดูคำตอบของ @ user0721090601 เกี่ยวกับสิ่งนั้น สำหรับการปฏิบัติจริงและความเร็วที่แท้จริงดูคำตอบของ JJ นอกจากนี้คำตอบทั่วไปเบื้องต้นเกี่ยวกับ "ฉันต้องการแยก X กับ Raku ใครช่วยได้บ้าง" .
raiph

ใช้ไวยากรณ์ :: ผู้ติดตาม; #works สำหรับฉัน
p6steve

คำตอบ:


12

อาจเป็นสิ่งสำคัญที่ทิ้งมันคือ\sตรงกับพื้นที่แนวนอนและแนวตั้ง เพื่อให้ตรงกับพื้นที่แนวนอนให้ใช้\hและเพื่อให้ตรงกับพื้นที่แนวตั้ง\vเท่านั้น

ข้อเสนอแนะเล็ก ๆ ที่ฉันควรทำก็คือหลีกเลี่ยงการรวมบรรทัดใหม่ในโทเค็น คุณอาจต้องการใช้ตัวดำเนินการสำรอง%หรือ%%เนื่องจากพวกมันถูกออกแบบมาเพื่อจัดการงานประเภทนี้:

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

ซึ่งแสดงให้เราเห็นว่าไวยากรณ์แยกวิเคราะห์ทุกอย่างเรียบร้อยแล้ว อย่างไรก็ตามขอให้คุณมุ่งเน้นไปที่ส่วนที่สองของคำถามที่คุณต้องการให้มีอยู่ในตัวแปรให้คุณ ในการทำเช่นนั้นคุณจะต้องระบุคลาสการกระทำที่ง่ายมากสำหรับโครงการนี้ คุณเพียงแค่สร้างคลาสที่เมธอดตรงกับเมธอดของไวยากรณ์ของคุณ (แม้ว่าคลาสที่ง่ายมากเช่นvalue/ headerที่ไม่ต้องการการประมวลผลพิเศษนอกเหนือจากการทำให้เป็นสตริงสามารถละเว้นได้) มีวิธีที่สร้างสรรค์ / กะทัดรัดในการจัดการการประมวลผลของคุณ แต่ฉันจะใช้วิธีการพื้นฐานในการแสดงตัวอย่าง นี่คือคลาสของเรา:

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

แต่ละวิธีมีลายเซ็น($/)ซึ่งเป็นตัวแปรการจับคู่ regex ดังนั้นตอนนี้มาถามข้อมูลที่เราต้องการจากโทเค็นแต่ละอัน ในแถวส่วนหัวเราต้องการค่าส่วนหัวแต่ละค่าในแถว ดังนั้น:

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

โทเค็นใด ๆ กับปริมาณที่มันจะได้รับการปฏิบัติเป็นPositionalดังนั้นเรายังสามารถเข้าถึงการแข่งขันแต่ละส่วนหัวของบุคคลที่มี$<header>[0], $<header>[1]ฯลฯ แต่ผู้ที่มีวัตถุการแข่งขันเพื่อให้เราได้อย่างรวดเร็วเพียงแค่ stringify พวกเขา 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วิธีการ เนื่องจากมีค่าหลายค่าในการรับค่าแต่ละmadeค่าเราต้องทำแผนที่ (นี่คือสถานการณ์ที่ฉันมักจะเขียนไวยากรณ์ของฉันให้มีเพียงแค่<header><data>ในไวยากรณ์และกำหนดข้อมูลเป็นหลายแถว แต่นี่คือ ง่ายพอมันไม่ได้แย่เกินไป)

ตอนนี้เรามีส่วนหัวและแถวในสองอาร์เรย์มันเป็นเรื่องของการทำให้พวกเขาเป็นชุดของแฮชซึ่งเราทำในforวง การรวมกันflat @x Z @yขององค์ประกอบเพียงอย่างเดียวและการมอบหมายแฮชทำในสิ่งที่เราหมายถึง แต่มีวิธีอื่นในการรับอาร์เรย์ในแฮชที่คุณต้องการ

เมื่อคุณทำเสร็จแล้วคุณ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 ใช่ฉันมักจะเขียนเรียนเพื่อหลีกเลี่ยงการเริ่มการกระทำของฉัน แต่ถ้า instantiating ผมอาจจะทำclass Actions { has @!header; has %!entries … }และเพียงแค่มี valueRow method TOP ($!) { make %!entries }เพิ่มรายการโดยตรงเพื่อที่คุณจะจบลงด้วยเพียง แต่นี่คือ Raku หลังจากทั้งหมดและ TIMTOWTDI :-)
user0721090601

จากการอ่านข้อมูลนี้ ( docs.raku.org/language/regexes#Modified_quantifier:_%,_%% ) ฉันคิดว่าฉันเข้าใจ<valueRow>+ %% \n(จับแถวที่คั่นด้วยบรรทัดใหม่) แต่ตามตรรกะนั้น<.ws>* %% <header>จะเป็น "ตัวเลือก ช่องว่างที่คั่นด้วยที่ไม่ใช่ช่องว่าง " ฉันพลาดอะไรไปรึเปล่า?
Christopher Bottoms

@ChristopherBottoms เกือบ <.ws>ไม่จับ ( <ws>จะ) OP ระบุว่ารูปแบบ TSV อาจเริ่มต้นด้วยช่องว่างทางเลือก ในความเป็นจริงนี้อาจจะถูกกำหนดให้ดียิ่งขึ้นด้วยโทเค็นระยะห่างบรรทัดที่กำหนดเป็น\h*\n\h*ซึ่งจะช่วยให้ค่า Row ที่จะกำหนดมากขึ้นในเชิงตรรกะเป็น<header> % <.ws>
user0721090601

@ user0721090601 ฉันจำไม่ได้ว่าอ่าน%/ %%เรียกคำว่า "alternation" op มาก่อน แต่มันเป็นชื่อที่ถูกต้อง (ในขณะที่การใช้งานของมัน|, ||และญาติได้หลงเสมอฉันเป็นแปลก.) ฉันไม่เคยคิดถึงเทคนิค "ย้อนกลับ" นี้มาก่อน แต่มันเป็นสำนวนที่ดีสำหรับการเขียน regexes ที่จับคู่รูปแบบซ้ำกับการยืนยันตัวคั่นบางอย่างไม่เพียง แต่ระหว่างการจับคู่ของรูปแบบ แต่ยังอนุญาตให้มันที่ปลายทั้งสอง (ใช้%%) หรือที่จุดเริ่มต้น แต่ไม่สิ้นสุด (ใช้%) เป็นเอ่อ ทางเลือกในการที่สิ้นสุด แต่ไม่เริ่มต้นตรรกะของและrule :sดี :)
raiph

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;

ส่วนที่สำคัญที่นี่คือข้อมูล munging ที่แปลงไฟล์เริ่มต้นเป็นอาร์เรย์หรืออาร์เรย์ (ใน@data) อย่างไรก็ตามต้องการเพียงเพราะcsvคำสั่งไม่สามารถจัดการกับสตริงได้ ถ้าข้อมูลอยู่ในไฟล์คุณก็พร้อมใช้งาน

บรรทัดสุดท้ายจะพิมพ์:

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

เขตข้อมูล ID จะกลายเป็นกุญแจสำคัญในการแฮชและสิ่งทั้งหมดคืออาร์เรย์ของแฮช


2
Upvoting เนื่องจากการปฏิบัติจริง ฉันไม่แน่ใจว่าถ้า OP มีจุดมุ่งหมายที่จะเรียนรู้ไวยากรณ์มากขึ้น (วิธีตอบของฉัน) หรือเพียงแค่ต้องการแยกวิเคราะห์ (แนวทางของคำตอบของคุณ) ไม่ว่าในกรณีใดเขาควรจะดีไป :-)
user0721090601

2
โหวตขึ้นด้วยเหตุผลเดียวกัน :) ฉันคิดว่า OP อาจต้องการเรียนรู้สิ่งที่พวกเขาทำผิดในแง่ของความหมายของ regex (ด้วยเหตุนี้คำตอบของฉัน) โดยมีจุดประสงค์ที่จะเรียนรู้วิธีการทำสิ่งที่ถูกต้อง (คำตอบของคุณ) หรือเพียงต้องการแยกคำตอบ ) การทำงานเป็นทีม :)
raiph

7

regexbacktrack ของTL; tokenไม่ทำ นั่นเป็นสาเหตุที่รูปแบบของคุณไม่ตรงกัน คำตอบนี้มุ่งเน้นไปที่การอธิบายเรื่องนั้นและวิธีแก้ไขไวยากรณ์ของคุณอย่างไม่สำคัญ อย่างไรก็ตามคุณควรเขียนใหม่หรือใช้ parser ที่มีอยู่ซึ่งเป็นสิ่งที่คุณควรทำแน่นอนถ้าคุณต้องการแยก TSV แทนที่จะเรียนรู้เกี่ยวกับ raku regexes

ความเข้าใจผิดขั้นพื้นฐาน?

ฉันคิดว่าฉันเข้าใจผิดบางอย่างเกี่ยวกับพื้นฐานของ regexes ใน raku

(ถ้าคุณรู้แล้วว่าคำว่า "regexes" เป็นคำที่คลุมเครือมากให้ข้ามหัวข้อนี้ไป)

สิ่งหนึ่งที่พื้นฐานที่คุณอาจเข้าใจผิดคือความหมายของคำว่า "regexes" นี่คือความหมายบางอย่างที่ชาวบ้านถือว่า:

  • นิพจน์ทั่วไปที่เป็นทางการ

  • Perl regexes

  • นิพจน์ปกติที่เข้ากันได้กับ Perl (PCRE)

  • การแสดงออกที่ตรงกับรูปแบบข้อความที่เรียกว่า "regexes" ที่มีลักษณะใด ๆ ข้างต้นและทำสิ่งที่คล้ายกัน

ไม่มีความหมายเหล่านี้เข้ากันได้

ในขณะที่ regexes Perl มีความหมาย superset ของการแสดงออกปกติอย่างเป็นทางการของพวกเขาอยู่ห่างไกลที่มีประโยชน์มากขึ้นในหลาย ๆ ด้าน แต่ยังมีความเสี่ยงมากขึ้นที่จะย้อนรอยทางพยาธิวิทยา

ในขณะที่ Perl Compatible Regular Expressions เข้ากันได้กับ Perl ในแง่ที่พวกเขาเดิมเหมือนกับ Perl regexes มาตรฐานในช่วงปลายปี 1990 และในแง่ที่ Perl รองรับเครื่องมือ regex ที่เสียบได้ซึ่งรวมถึงเครื่องยนต์ PCRE ไวยากรณ์ PCRE regex ไม่เหมือนกับมาตรฐาน Perl regex ใช้โดยค่าเริ่มต้นโดย Perl ในปี 2020

และในขณะที่นิพจน์การจับคู่รูปแบบข้อความที่เรียกว่า "regexes" โดยทั่วไปจะดูเหมือนกันและทำข้อความที่ตรงกันทั้งหมดมีหลายสิบอาจจะหลายร้อยของการเปลี่ยนแปลงในไวยากรณ์และแม้ในความหมายสำหรับไวยากรณ์เดียวกัน

การจับคู่รูปแบบข้อความ Raku โดยทั่วไปมักเรียกว่า "กฎ" หรือ "regexes" การใช้คำว่า "regexes" บ่งบอกถึงความจริงที่ว่าพวกเขาดูเหมือน regexes อื่น ๆ (แม้ว่าไวยากรณ์ได้รับการล้างขึ้น) คำว่า "กฎ" บ่งบอกถึงความจริงที่ว่าพวกเขาเป็นส่วนหนึ่งของชุดคุณลักษณะและเครื่องมือที่กว้างขึ้นซึ่งแยกเป็นส่วน ๆ (และอื่น ๆ )

แก้ไขด่วน

ด้วยแง่มุมพื้นฐานด้านบนของคำว่า "regexes" ทำให้ฉันสามารถหันไปใช้ลักษณะพื้นฐานของพฤติกรรม "regex" ของคุณ

หากเราสลับรูปแบบสามรูปแบบในไวยากรณ์ของคุณสำหรับผู้tokenประกาศเป็นผู้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คือregexbacktracks ในขณะที่ 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

ในระหว่างการประมวลผลของรูปแบบสุดท้าย (ซึ่งอาจเป็นและมักเรียกว่า "regex" แต่ผู้ประกาศที่แท้จริงคือtokenไม่ใช่regex) \Sจะกลืน the 'b'ดังเช่นที่เกิดขึ้นชั่วคราวระหว่างการประมวลผลของ regex ในบรรทัดก่อน แต่เนื่องจากรูปแบบถูกประกาศเป็น a token, เอ็นจินกฎ (หรือที่รู้จักในชื่อ "regex engine") จะไม่ย้อนกลับดังนั้นการแข่งขันโดยรวมจึงล้มเหลว

นั่นคือสิ่งที่เกิดขึ้นใน OP ของคุณ

การแก้ไขที่ถูกต้อง

ทางออกที่ดีโดยทั่วไปคือการหย่านมตัวเองจากการสมมติว่าพฤติกรรมย้อนรอยเพราะมันอาจจะช้าและแม้ช้าย่อยยับ (แยกไม่ออกจากที่แขวนโปรแกรม) เมื่อใช้ในการจับคู่กับสตริงสร้างมีเจตนาร้ายหรือหนึ่งที่มีการรวมกันที่โชคร้ายตั้งใจของตัวละคร

บางครั้งregexมีความเหมาะสม ตัวอย่างเช่นถ้าคุณกำลังเขียนเพียงครั้งเดียวและ regex ทำงานคุณก็ทำเสร็จแล้ว ไม่เป็นไร. นั่นเป็นส่วนหนึ่งของเหตุผลที่ว่า/ ... /ไวยากรณ์ใน Raku regexประกาศรูปแบบย้อนรอยเช่นเดียวกับ (จากนั้นอีกครั้งคุณสามารถเขียน/ :r ... /หากคุณต้องการเปิดratcheting - "ratchet" หมายถึงตรงกันข้ามกับ "backtrack" ดังนั้น:rเปลี่ยน regex เป็นtokensemantics)

การย้อนรอยเป็นครั้งคราวยังคงมีบทบาทในบริบทการแยกวิเคราะห์ ตัวอย่างเช่นในขณะที่ไวยากรณ์สำหรับ raku โดยทั่วไปจะหลบเลี่ยงการย้อนรอยและแทนที่จะมีหลายร้อยrules และtokens แต่ก็ยังคงมี 3 regexs


ฉันตอบกลับมาแล้ว @ user0721090601 ++ เพราะมีประโยชน์ นอกจากนี้ยังกล่าวถึงสิ่งต่าง ๆ ที่ดูเหมือนว่าฉันจะปิดโดยทันทีในรหัสของคุณและที่สำคัญคือติดกับtokens มันอาจเป็นคำตอบที่คุณต้องการซึ่งจะเจ๋ง

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.