如何定义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。我认为我误解了Raku中的正则表达式。


1
Nil。就反馈而言,这是相当荒谬的,对吧?要进行调试,请下载commaide(如果尚未安装),和/或请参阅如何改进语法中的错误报告?。您会发现Nil您的模式采用了回溯语义。看到我对此的回答。我建议您避免回溯。请参阅@ user0721090601的答案。有关纯粹的实用性和速度,请参阅JJ的答案。另外,“我想用Raku解析X。有人可以帮忙吗?”的介绍性一般性回答。
雷夫

使用语法:: Tracer; #为我工作
p6steve

Answers:


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       ($/) { ... }
}

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

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

上有一个量词任何令牌将被视为一个Positional,所以我们也可以访问每一个人头匹配$<header>[0]$<header>[1]等等。但是这些都是比赛的对象,所以我们只是快速字符串化他们。该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>在语法中,并将数据定义为多行,但这是很简单,还算不错)。

现在,我们将标头和行包含在两个数组中,只需要使它们成为哈希数组即可,我们在for循环中执行此操作。在flat @x Z @y刚刚intercolates的元素,哈希分配做什么?我们的意思,但也有其他的方式来得到你想要的哈希数组。

一旦完成,就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)
布拉德·吉尔伯特

@BradGilbert是的,我倾向于编写我的action类以避免实例化,但是如果实例化,我可能会这样做class Actions { has @!header; has %!entries … },只是让valueRow直接添加条目,以便最终得到just 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*,这将使valueRow的逻辑定义为<header> % <.ws>
user0721090601

@ user0721090601我不记得以前读过%/ %%称为“替代”操作。但这是正确的名称。(虽然使用它|||和表兄弟一直让我觉得不可思议。)。我以前从未想到过这种“向后”技术。但这是一个很好的习惯用法,用于编写与重复模式匹配且带有一些分隔符断言的正则表达式,不仅可以在模式匹配之间进行匹配,还可以在两端(使用%%)或在开头但不结束(使用%)(例如,替代末尾的逻辑,但不启动rule和的逻辑:s。真好 :)
雷夫

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;

这里的关键部分是数据处理,它将初始文件转换成一个或多个数组(在中@data)。但是只需要它,因为该csv命令不能处理字符串。如果数据在文件中,那很好。

最后一行将打印:

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

ID字段将成为哈希的键,整个对象将成为哈希数组。


2
由于实用性而引起争议。不过,我不确定OP是否打算更多地学习语法(我的答案的方法)还是仅需要解析(您的答案的方法)。无论哪种情况,他都应该走的很好:-)
user0721090601

2
出于同样的原因而投票。:)我以为OP可能旨在了解他们在正则表达式语义上做错了什么(因此回答我),旨在了解如何正确处理(您的回答),或者只是需要解析(JJ的回答) )。团队合作。:)
雷夫

7

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

一个基本的误解?

我认为我误解了Raku中的正则表达式。

(如果您已经知道“ regexes”一词是非常含糊的,请考虑跳过此部分。)

您可能会误解的一件事是“ regexs”一词的含义。这是人们普遍认为的一些普遍含义:

  • 正规正则表达式。

  • Perl正则表达式。

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

  • 文本模式匹配表达式,称为“ regexes”,看起来与上面的任何表达式相似,并且执行类似的操作。

这些含义均不兼容。

虽然Perl正则表达式在语义上是形式正则表达式的超集,但它们在许多方面都非常有用,但也更容易遭受病理性回溯

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

尽管文本模式匹配表达式通常称为“ regexes”,并且都与文本匹配,但语法甚至是同一语法的语义都有数十种甚至数百种变化。

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

快速修复

有了“ regexs”一词的上述基本方面,我现在可以谈谈“ 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 之间的唯一区别regexregex回溯,而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将吞下'b',就像在上一行中处理regex时所做的那样。但是,由于模式被声明为a token,所以规则引擎(也称为“正则表达式引擎”)不会回溯,因此整体匹配失败。

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

正确的解决方法

一般较好的解决方案是从断奶自己假设在匹配中使用针对恶意构造字符串或一个与字符的意外不幸组合时回溯的行为,因为它可能是缓慢的和甚至灾难性慢(不可区分从节目挂)。

有时regexs是合适的。例如,如果您要编写一次,而正则表达式可以完成任务,那么您就完成了。没关系。这就是/ ... /raku 中的语法声明回溯模式的一部分原因,就像regex。(然后,/ :r ... /如果要打开棘轮功能,您可以再次编写-“棘轮”的含义与“回溯”相反,因此:r将正则表达式切换为token语义。)

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


我赞成@ user0721090601 ++的答案,因为它很有用。它也解决了我看来似乎在您的代码中惯用的几件事,而且重要的是,坚持使用tokens。这很可能是您喜欢的答案,这很酷。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.