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

lit*_*tle 14 csv grammar raku

我有一些 TSV 数据

ID     Name    Email
   1   test    test@email.com
 321   stan    stan@nowhere.net
Run Code Online (Sandbox Code Playgroud)

我想将其解析为哈希列表

@entities[0]<Name> eq "test";
@entities[1]<Email> eq "stan@nowhere.net";
Run Code Online (Sandbox Code Playgroud)

我在使用换行元字符从值行分隔标题行时遇到问题。我的语法定义:

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);
Run Code Online (Sandbox Code Playgroud)

但这是回归Nil。我想我误解了 raku 中关于正则表达式的一些基本知识。

use*_*601 12

扔掉它的主要因素可能是\s与水平垂直空间相匹配。要仅匹配水平空间,请使用\h,而仅匹配垂直空间,请使用\v

我提出的一个小建议是避免在令牌中包含换行符。您可能还想使用交替运算符%or %%,因为它们专为处理此类工作而设计:

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* }
} 
Run Code Online (Sandbox Code Playgroud)

这样做的结果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 => ??
Run Code Online (Sandbox Code Playgroud)

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

class ParserActions {
  method headerRow ($/) { ... }
  method valueRow  ($/) { ... }
  method TOP       ($/) { ... }
}
Run Code Online (Sandbox Code Playgroud)

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

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

上有一个量词任何令牌将被视为一个Positional,所以我们也可以访问每一个人头匹配$<header>[0]$<header>[1]等等。但是这些都是比赛的对象,所以我们只是快速字符串化他们。该make命令允许其他令牌访问我们创建的这些特殊数据。

我们的价值行看起来相同,因为$<value>令牌是我们关心的。

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

当我们使用最后一个方法时,我们将要创建带有哈希值的数组。

  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;
  }
Run Code Online (Sandbox Code Playgroud)

在这里您可以看到我们如何访问我们处理的内容headerRow()valueRow():您使用该.made方法。因为有多个valueRows,为了得到它们的每一个made值,我们需要做一个map(这种情况我倾向于把我的语法写成简单的<header><data>在语法中,并将数据定义为多行,但这是足够简单,还不错)。

现在我们有两个数组中的标题和行,只需将它们设为哈希数组即可,我们在for循环中执行此操作。The flat @x Z @yjust intercolates the elements, and the hash assignment does What We Mean,但还有其他方法可以让数组得到你想要的散列。

一旦你完成了,你就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} {}]
Run Code Online (Sandbox Code Playgroud)

将这些包装成一个方法是很常见的,比如

sub parse-tsv($tsv) {
  return Parser.parse($tsv, :actions(ParserActions)).made
}
Run Code Online (Sandbox Code Playgroud)

这样你就可以说

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


jjm*_*elo 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;
Run Code Online (Sandbox Code Playgroud)

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

最后一行将打印:

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

ID 字段将成为散列的键,整个事情将成为散列数组。

  • 因为实用所以点赞。不过,我不确定OP的目标是更多地学习语法(我的答案的方法)还是只需要解析(你的答案的方法)。无论哪种情况,他都应该可以走了:-) (2认同)
  • 出于同样的原因投票。:)我原以为OP可能旨在了解他们在正则表达式语义方面做错了什么(因此是我的答案),旨在学习如何正确做事(你的答案),或者只是需要解析(JJ的答案) )。团队合作。:) (2认同)

rai*_*iph 8

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

一个根本的误解?

我想我误解了 raku 中关于正则表达式的一些基本知识。

(如果您已经知道术语“正则表达式”是一个非常模糊的词,请考虑跳过本节。)

您可能会误解的一项基本内容是“正则表达式”一词的含义。以下是人们假设的一些流行含义:

  • 正式的正则表达式。

  • Perl 正则表达式。

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

  • 称为“正则表达式”的文本模式匹配表达式看起来像上述任何一种并且执行类似的操作。

这些含义中没有一个是相互兼容的。

虽然 Perl 正则表达式在语义上是正式正则表达式的超集,但它们在许多方面更有用,但也更容易受到病态回溯的影响

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

虽然称为“正则表达式”的文本模式匹配表达式通常看起来有些相似,并且都匹配文本,但在语法上存在数十种甚至数百种变化,甚至相同语法的语义也存在差异。

Raku 文本模式匹配表达式通常称为“规则”或“正则表达式”。术语“正则表达式”的使用传达了这样一个事实,即它们看起来有点像其他正则表达式(尽管语法已被清理)。术语“规则”传达了这样一个事实,即它们是更广泛的功能和工具的一部分,可扩展到解析(及更多)。

快速修复

有了上述“regex”这个词的基本方面,我现在可以转向“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+ }
}
Run Code Online (Sandbox Code Playgroud)

atoken和 a之间的唯一区别regex是 a 会regex回溯而 atoken不会。因此:

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
Run Code Online (Sandbox Code Playgroud)

在处理最后一个模式(这可能并且通常被称为“正则表达式”,但其实际声明符是token,而不是regex)期间, the\S将吞下'b',就像它在处理前一行中的正则表达式期间临时所做的那样。但是,由于模式被声明为 a token,规则引擎(又名“正则表达式引擎”)不会回溯,因此整体匹配失败。

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

正确的修复

一般来说,更好的解决方案是让自己不要假设回溯行为,因为当用于匹配恶意构造的字符串或具有意外不幸字符组合的字符串时,它可能会很慢甚至灾难性地慢(与程序挂起无法区分)。

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

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


我赞成@user0721090601++ 的答案,因为它很有用。它还解决了一些在我看来在您的代码中不合时宜的事情,并且重要的是,坚持使用tokens。这很可能是您喜欢的答案,这很酷。