Làm cách nào để xác định ngữ pháp Raku để phân tích văn bản TSV?


13

Tôi có một số dữ liệu TSV

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

Tôi muốn phân tích điều này thành một danh sách băm

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

Tôi gặp rắc rối với việc sử dụng metacharacter dòng mới để phân định hàng tiêu đề khỏi các hàng giá trị. Định nghĩa ngữ pháp của tôi:

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

Nhưng điều này đang trở lại Nil. Tôi nghĩ rằng tôi đang hiểu nhầm một cái gì đó cơ bản về regexes trong raku.


1
Nil. Nó khá cằn cỗi theo như phản hồi, phải không? Để gỡ lỗi, hãy tải xuống dấu phẩy nếu bạn chưa có và / hoặc xem Làm thế nào để báo cáo lỗi trong ngữ pháp được cải thiện? . Bạn có Nilcuz mô hình của bạn giả định ngữ nghĩa quay lui. Xem câu trả lời của tôi về điều đó. Tôi khuyên bạn nên quay lại eschew. Xem câu trả lời của @ user0721090601 về điều đó. Để biết tính thực tế và tốc độ, hãy xem câu trả lời của JJ. Ngoài ra, câu trả lời chung giới thiệu cho "Tôi muốn phân tích X với Raku. Có ai có thể giúp đỡ không?" .
ngày

sử dụng ngữ pháp :: Tracer; #works cho tôi
p6steve

Câu trả lời:


12

Có lẽ điều chính khiến nó bị loại bỏ là \sphù hợp với không gian ngang dọc. Để phù hợp với không gian ngang, sử dụng\h và để khớp với không gian dọc , \v.

Một khuyến nghị nhỏ mà tôi đưa ra là tránh đưa các dòng mới vào mã thông báo. Bạn cũng có thể muốn sử dụng các toán tử thay thế %hoặc %%, vì chúng được thiết kế để xử lý loại công việc này:

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

Kết quả của Parser.parse($dat)việc này là như sau:

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

trong đó cho chúng ta thấy rằng ngữ pháp đã phân tích thành công mọi thứ. Tuy nhiên, hãy tập trung vào phần thứ hai của câu hỏi của bạn, rằng bạn muốn nó có sẵn trong một biến cho bạn. Để làm điều đó, bạn sẽ cần cung cấp một lớp hành động rất đơn giản cho dự án này. Bạn chỉ cần tạo một lớp có các phương thức khớp với các phương thức ngữ pháp của bạn (mặc dù các phương thức rất đơn giản, như value/ headerkhông yêu cầu xử lý đặc biệt bên cạnh chuỗi, có thể bị bỏ qua). Có một số cách sáng tạo / nhỏ gọn hơn để xử lý việc xử lý của bạn, nhưng tôi sẽ đi theo một cách tiếp cận khá thô sơ để minh họa. Đây là lớp học của chúng tôi:

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

Mỗi phương thức có chữ ký ($/)là biến khớp regex. Vì vậy, bây giờ, hãy hỏi những thông tin chúng tôi muốn từ mỗi mã thông báo. Trong hàng tiêu đề, chúng tôi muốn mỗi giá trị tiêu đề, trong một hàng. Vì thế:

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

Bất kỳ mã thông báo nào có bộ định lượng trên đó sẽ được coi là một Positional, vì vậy chúng tôi cũng có thể truy cập từng đối tượng tiêu đề riêng lẻ với $<header>[0],$<header>[1] , vv Tuy nhiên, đó là những đối tượng phù hợp, vì vậy chúng tôi chỉ nhanh chóng stringify họ. Các makelệnh cho phép thẻ khác để truy cập dữ liệu đặc biệt này mà chúng tôi đã tạo ra.

Hàng giá trị của chúng tôi sẽ trông giống hệt nhau, bởi vì các $<value>mã thông báo là thứ chúng tôi quan tâm.

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

Khi chúng ta đến phương thức cuối cùng, chúng ta sẽ muốn tạo mảng bằng băm.

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

Tại đây bạn có thể thấy cách chúng tôi truy cập vào nội dung chúng tôi đã xử lý headerRow()valueRow(): Bạn sử dụng .madephương pháp. Bởi vì có nhiều giá trị, để có được mỗi giá trị của chúngmade giá trị giá trị chúng, chúng ta cần tạo một bản đồ (đây là tình huống tôi có xu hướng viết ngữ pháp của mình để đơn giản <header><data>trong ngữ pháp và xác định dữ liệu là nhiều hàng, nhưng đây là đủ đơn giản, nó không quá tệ).

Bây giờ chúng ta có các tiêu đề và hàng trong hai mảng, đơn giản chỉ là biến chúng thành một mảng băm, mà chúng ta thực hiện trong forvòng lặp. Việc flat @x Z @ychỉ xen kẽ các phần tử và phép gán băm Does What We We mean, nhưng có nhiều cách khác để có được mảng trong hàm băm bạn muốn.

Khi bạn đã hoàn tất, bạn chỉ cần makenó, và sau đó nó sẽ có sẵn trong madephân tích cú pháp:

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

Nó khá phổ biến để bọc chúng thành một phương thức, như

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

Bằng cách đó bạn chỉ có thể nói

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

Tôi nghĩ rằng tôi sẽ viết các lớp hành động khác nhau. 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 }Bạn tất nhiên sẽ phải khởi tạo nó trước :actions(Actions.new).
Brad Gilbert

@BradGilbert vâng, tôi có xu hướng viết các lớp hành động của mình để tránh khởi tạo, nhưng nếu bắt đầu, tôi có thể làm class Actions { has @!header; has %!entries … }và chỉ cần có giá trịRow thêm các mục trực tiếp để cuối cùng bạn chỉ cần method TOP ($!) { make %!entries }. Nhưng đây là Raku sau tất cả và TIMTOWTDI :-)
user0721090601

Từ việc đọc thông tin này ( docs.raku.org/lingu/regexes#Modified_quantifier:_%,_%% ), tôi nghĩ rằng tôi hiểu <valueRow>+ %% \n(Chụp các hàng được phân cách bằng dòng mới), nhưng theo logic đó, <.ws>* %% <header>sẽ là "bắt tùy chọn khoảng trắng được phân định bởi khoảng trắng ". Tui bỏ lỡ điều gì vậy?
Christopher Bottoms

@ChristopherBottoms gần như. Việc <.ws>không chụp ( <ws>sẽ). OP lưu ý rằng định dạng TSV có thể bắt đầu bằng một khoảng trắng tùy chọn. Trong thực tế, điều này có lẽ sẽ còn được xác định tốt hơn với mã thông báo khoảng cách dòng được xác định là \h*\n\h*, điều này sẽ cho phép valueRow được xác định hợp lý hơn như<header> % <.ws>
user0721090601

@ user0721090601 Tôi không nhớ đã đọc %/ %%gọi là "thay thế" op trước đây. Nhưng đó là tên đúng. (Trong khi sử dụng nó cho |, ||và anh em họ luôn luôn đánh tôi là kỳ lạ.). Tôi chưa từng nghĩ đến kỹ thuật "ngược" này trước đây. Nhưng đó là một thành ngữ hay để viết các biểu thức khớp với một mẫu lặp lại với một số xác nhận dấu tách không chỉ giữa các khớp của mẫu mà còn cho phép nó ở cả hai đầu (sử dụng %%), hoặc ở đầu nhưng không kết thúc (sử dụng %), như một, er, thay thế ở cuối nhưng không bắt đầu logic của rule:s. Đẹp. :)
raiph

11

TL; DR: bạn không. Chỉ dùngText::CSV , có thể đối phó với mọi định dạng.

Tôi sẽ chỉ ra bao nhiêu tuổi Text::CSVcó thể sẽ hữu ích:

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;

Phần quan trọng ở đây là munging dữ liệu chuyển đổi tập tin ban đầu thành một mảng hoặc mảng (in @data). Tuy nhiên, nó chỉ cần thiết bởi vì csvlệnh không thể xử lý chuỗi; nếu dữ liệu nằm trong một tập tin, bạn nên đi.

Dòng cuối cùng sẽ in:

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

Trường ID sẽ trở thành chìa khóa cho hàm băm và toàn bộ một mảng băm.


2
Nâng cao vì tính thực tiễn. Tuy nhiên, tôi không chắc chắn nếu OP đang nhắm nhiều hơn để học ngữ pháp (cách tiếp cận câu trả lời của tôi) hoặc chỉ cần phân tích cú pháp (cách tiếp cận câu trả lời của bạn). Trong cả hai trường hợp, anh ấy nên đi :-)
user0721090601

2
Nâng cao vì lý do tương tự. :) Tôi đã nghĩ rằng OP có thể đang nhắm đến việc tìm hiểu những gì họ đã làm sai về ngữ nghĩa regex (do đó là câu trả lời của tôi), nhằm tìm hiểu làm thế nào để làm điều đó đúng (câu trả lời của bạn) hoặc chỉ cần phân tích (câu trả lời của JJ ). Làm việc theo nhóm. :)
ngày

7

Quay lại TL; DR regex s. tokens không. Đó là lý do tại sao mô hình của bạn không phù hợp. Câu trả lời này tập trung vào việc giải thích điều đó và cách sửa chữa ngữ pháp của bạn một cách tầm thường. Tuy nhiên, có lẽ bạn nên viết lại hoặc sử dụng trình phân tích cú pháp hiện có, đây là điều bạn chắc chắn nên làm nếu bạn chỉ muốn phân tích TSV thay vì tìm hiểu về các biểu thức raku.

Một sự hiểu lầm cơ bản?

Tôi nghĩ rằng tôi đang hiểu nhầm một cái gì đó cơ bản về regexes trong raku.

(Nếu bạn đã biết thuật ngữ "regexes" là một từ rất mơ hồ, hãy xem xét bỏ qua phần này.)

Một điều cơ bản mà bạn có thể hiểu nhầm là ý nghĩa của từ "regexes". Dưới đây là một số ý nghĩa phổ biến dân gian giả định:

  • Biểu thức chính quy thường xuyên.

  • Perl regexes.

  • Biểu thức chính quy tương thích Perl (PCRE).

  • Mẫu văn bản khớp với các biểu thức được gọi là "regexes" trông giống như bất kỳ ở trên và làm một cái gì đó tương tự.

Không có ý nghĩa nào trong số này là tương thích với nhau.

Mặc dù các biểu thức Perl về mặt ngữ nghĩa là một siêu biểu thức chính quy, nhưng chúng hữu ích hơn nhiều về nhiều mặt nhưng cũng dễ bị tổn thương hơn khi quay lại bệnh lý .

Mặc dù Biểu thức chính quy tương thích Perl tương thích với Perl theo nghĩa ban đầu giống với biểu thức Perl tiêu chuẩn vào cuối những năm 1990 và theo nghĩa là Perl hỗ trợ các công cụ regex có thể cắm được bao gồm cả công cụ PCRE, cú pháp biểu thức PCRE không giống với tiêu chuẩn Regl Perl được mặc định sử dụng bởi Perl vào năm 2020.

Và trong khi các biểu thức khớp mẫu văn bản gọi là "regexes" nhìn chung trông hơi giống nhau và thực hiện tất cả các văn bản khớp, có hàng chục, có lẽ hàng trăm biến thể trong cú pháp và thậm chí cả về ngữ nghĩa cho cùng một cú pháp.

Các biểu thức khớp mẫu văn bản Raku thường được gọi là "quy tắc" hoặc "biểu thức chính quy". Việc sử dụng thuật ngữ "regexes" truyền tải thực tế rằng chúng trông hơi giống các regex khác (mặc dù cú pháp đã được xóa sạch). Thuật ngữ "quy tắc" truyền tải thực tế rằng chúng là một phần của tập hợp các tính năng và công cụ rộng hơn nhiều để phân tích cú pháp (và hơn thế nữa).

Cách khắc phục nhanh

Với khía cạnh cơ bản ở trên của từ "regexes" ngoài lề, giờ đây tôi có thể chuyển sang khía cạnh cơ bản trong hành vi "regex" của bạn .

Nếu chúng tôi chuyển ba trong số các mẫu trong ngữ pháp của bạn cho người tokenkhai báo sang người regexkhai báo, ngữ pháp của bạn sẽ hoạt động như bạn dự định:

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

Sự khác biệt duy nhất giữa a tokenvà a regexregexquay lại trong khi tokenkhông. Như vậy:

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

Trong quá trình xử lý mẫu cuối cùng (có thể và thường được gọi là "regex", nhưng người khai báo thực tế là tokenkhông regex), \Ssẽ nuốt 'b', giống như nó đã tạm thời thực hiện trong quá trình xử lý biểu thức chính quy trong dòng trước. Nhưng, vì mẫu được khai báo là a token, công cụ quy tắc (còn gọi là "công cụ regex") không quay lại , do đó, kết quả khớp tổng thể thất bại.

Đó là những gì đang diễn ra trong OP của bạn.

Sửa chữa đúng

Một giải pháp tốt hơn nói chung là hãy từ bỏ giả định hành vi quay lui, bởi vì nó có thể chậm và thậm chí chậm một cách thảm khốc (không thể phân biệt được với chương trình treo) khi được sử dụng để khớp với chuỗi được xây dựng độc hại hoặc một chuỗi có sự kết hợp các ký tự không may.

Đôi khi regexs là thích hợp. Ví dụ: nếu bạn viết một lần và regex thực hiện công việc, thì bạn đã hoàn thành. Tốt rồi. Đó là một phần lý do / ... /cú pháp trong raku tuyên bố kiểu quay lui, giống như regex. (Sau đó, một lần nữa bạn có thể viết / :r ... /nếu bạn muốn bật ratcheting - "ratchet" có nghĩa ngược lại với "backtrack", vì vậy hãy :rchuyển regex sang tokenngữ nghĩa.)

Thỉnh thoảng quay lại vẫn có một vai trò trong bối cảnh phân tích cú pháp. Ví dụ, trong khi ngữ pháp cho raku thường tránh quay lại, và thay vào đó có hàng trăm rules và tokens, tuy nhiên nó vẫn có 3 regexs.


Tôi đã nâng cấp câu trả lời của @ user0721090601 ++ vì nó hữu ích. Nó cũng giải quyết một số điều mà ngay lập tức tôi dường như bị tắt trong mã của bạn, và quan trọng là, dính vào tokens. Nó cũng có thể là câu trả lời bạn thích, sẽ rất tuyệt.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.