如何定义 Raku 语法来解析 TSV 文本?

How can I define a Raku grammar to parse TSV text?

我有一些 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";

我在使用换行元字符将 header 行与值行分隔开时遇到了问题。我的语法定义:

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。我想我误解了关于 raku 中正则表达式的一些基本知识。

可能导致它失败的主要原因是 \s 匹配水平 垂直 space。要仅匹配水平 space,请使用 \h,并仅匹配垂直 space、\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 => 「」

这表明语法已成功解析所有内容。但是,让我们关注问题的第二部分,即您希望它在变量中对您可用。为此,您需要提供一个动作 class,这对于这个项目来说非常简单。您只需创建一个 class ,其方法与您的语法方法相匹配(尽管非常简单的 value/header 除了字符串化之外不需要特殊处理,可以忽略)。还有一些 creative/compact 方法可以处理您的处理,但我将采用一种相当基本的方法来进行说明。这是我们的 class:

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

每个方法都有签名 ($/),这是正则表达式匹配变量。所以现在,让我们问一下我们想从每个令牌中获得什么信息。在 header 行中,我们希望每个 header 值都排成一行。所以:

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

任何带有量词的标记都将被视为 Positional,因此我们还可以访问与 $<header>[0]$<header>[1] 等匹配的每个 header .但是那些匹配objects,所以我们只是快速地将它们字符串化。 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>,并定义数据是多行,但这很简单,还不错。

现在我们在两个数组中有 header 和行,只需将它们变成散列数组即可,我们在 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

TL;DR regexs 回溯。 token没有。这就是你的模式不匹配的原因。这个答案的重点是解释这一点,以及如何简单地修复你的语法。但是,您可能应该重写它,或者使用现有的解析器,如果您只想解析 TSV 而不是了解 raku 正则表达式,那么 绝对 应该这样做。

根本性的误解?

I think I'm misunderstanding something fundamental about regexes in raku.

(如果您已经知道术语 "regexes" 是一个非常模糊的词,请考虑跳过此部分。)

您可能误解的一个基本问题是 "regexes" 这个词的含义。以下是人们假设的一些流行含义:

  • 正式正则表达式。

  • Perl 正则表达式。

  • Perl 兼容正则表达式 (PCRE)。

  • 名为 "regexes" 的文本模式匹配表达式,看起来像以上任何一个,并且做类似的事情。

None这些含义是相互兼容的

虽然 Perl 正则表达式在语义上是正式正则表达式的超集,但它们在许多方面更有用,但也更容易受到 pathological backtracking.

虽然 Perl 兼容正则表达式在某种意义上与 Perl 兼容,但它们 最初 与 1990 年代后期的标准 Perl 正则表达式相同,并且在某种意义上 Perl 支持可插入的正则表达式包括 PCRE 引擎在内的引擎,PCRE 正则表达式语法与 Perl 在 2020 年默认使用的标准 Perl 正则表达式不同。

虽然称为 "regexes" 的文本模式匹配表达式通常看起来有些相似,并且都匹配文本,但在语法甚至语义上有数十种甚至数百种变体语法。

Raku 文本模式匹配表达式通常称为 "rules" 或 "regexes"。术语 "regexes" 的使用传达了这样一个事实,即它们看起来有点像其他正则表达式(尽管语法已被清理)。术语 "rules" 传达了这样一个事实,即它们是扩展到解析(以及更远)的大量 broader set of features and tools 的一部分。

快速修复

上述 "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+ }
}

tokenregex 之间的唯一区别是 regex 回溯而 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 将吞下 'b',就像它在处理前一行中的正则表达式期间暂时所做的那样。但是,因为模式被声明为 token,规则引擎(又名 "regex engine") 不会回溯 ,所以整体匹配失败。

这就是您的 OP 中发生的事情。

正确的修复

总的来说,一个更好的解决方案是让自己摆脱 假设 回溯行为,因为当用于匹配恶意构造的字符串或具有意外不幸的字符组合的字符串。

有时 regex 是合适的。例如,如果您正在编写一次性的并且正则表达式完成了这项工作,那么您就完成了。没关系。这就是 raku 中的 / ... / 语法声明回溯模式的部分原因,就像 regex 一样。 (如果你想打开 ratcheting,你可以再次写 / :r ... / -- "ratchet" 与 "backtrack" 相反,所以 :r 将正则表达式切换为 token 语义。)

偶尔回溯在解析上下文中仍然发挥作用。例如,虽然 raku 的语法通常避免回溯,而是有数百个 ruletoken,但它仍然有 3 个 regex


我对@user0721090601++ 的回答投了赞成票,因为它很有用。它还解决了一些在我看来立即在您的代码中惯用的事情,并且重要的是,坚持 tokens。很可能是你喜欢的答案,这会很酷。

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;

这里的关键部分是将初始文件转换为一个或多个数组的数据处理(在 @data 中)。然而,它只是需要的,因为 csv 命令不能处理字符串;如果数据在文件中,你就可以开始了。

最后一行将打印:

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

ID 字段将成为散列的键,整个东西是一个散列数组。