TL;DR
我将从非常适合某些场景的简洁代码开始[1],如果有人熟悉 shell 和 Raku 基础知识并且在着急:
> raku -e 'for slurp() ~~ m:g / "@article\{" (<-[,]>+) \, \s+
"title=\{" (<-[}]>+) \} / -> $/ { put "$0: $1\n" }' < derm.bib
这会精确地产生您指定的输出:
garg2017patch: Patch testing in patients with suspected cosmetic dermatitis: A retrospective study
hauso2008neuroendocrine: Neuroendocrine tumor epidemiology
siperstein1997laparoscopic: Laparoscopic thermal ablation of hepatic neuroendocrine tumor metastases
相同的单个语句,但在一个脚本中
跳过 shell 转义并添加:
►use tio.run to run the code below
for slurp() # "slurp" (read all of) stdin and then
~~ m :global # match it "globally" (all matches) against
/ '@article{' (<-[,]>+) ',' \s+ # a "nextgen regex" that uses (`(...)`) to
'title={' (<-[}]>+) '}' / # capture the article id and title and then
-> $/ { put "$0: $1\n" } # for each article, print "article id: title".
如果上面的内容看起来仍然像纯粹的 gobbledygook,请不要担心。后面的部分解释了上述内容,同时还介绍了更通用、更简洁、更易读的代码。[2]
四个语句而不是一个
my \input = slurp;
my \pattern = rule { '@article{' ( <-[,]>+ ) ','
'title={' ( <-[}]>+ ) }
my \articles = input .match: pattern, :global;
for articles -> $/ { put "$0: $1\n" }
my 声明一个词法变量。 Raku 在变量名的开头支持sigils。但它也允许开发人员像我一样“削减它们”。
my \pattern ...
my \pattern = rule { '@article{' ( <-[,]>+ ) ','
'title={' ( <-[}]>+ ) }
我已将模式语法从原始单行中的 / ... / 切换为 rule { ... }。我这样做是为了:
-
分配模式而不是匹配
如果我写了my \pattern = / ... /,匹配将被立即调用以匹配当前主题,并且该匹配的结果分配给pattern .哎呀。
-
消除病理性回溯的风险
经典正则表达式风险pathological backtracking。如果您可以杀死一个已经疯狂的程序,那很好,但是单击链接以阅读它会变得多么糟糕! ? 我们不需要回溯来匹配.bib 格式。
-
告知模式是rule
如果您编写了大量模式匹配代码,您会经常想要使用rule { ... }。 rule 消除了刚刚描述的经典正则表达式问题(病态回溯)的任何风险,并具有另一个超能力。在首先介绍与那些超级大国对应的副词之后,我将在下面介绍这两个方面。
Raku 正则表达式/规则可以(经常)与"adverbs" 一起使用。这些是修改模式应用方式的便捷快捷方式。
我已经在此代码的早期版本中使用了副词。 “全局”副词(使用:global 或其简写别名:g 指定)指示匹配引擎使用所有的输入,生成一个列表匹配它包含的内容,而不是只返回第一个匹配项。
虽然副词有速记别名,但有些被重复使用,以至于将它们捆绑成不同的规则声明符会更整洁。这就是我使用rule 的原因。它捆绑了两个适合匹配许多数据格式的副词,例如.bib 文件:
Ratcheting (:r / :ratchet) 告诉编译器,当一个“原子”(规则中被视为一个单元的子模式)匹配时,没有回头路 在那。如果同一规则中模式中更远的原子失败,则整个规则立即失败。
这消除了前面讨论的“病态回溯”的任何风险。
显着空间处理 (:s / :sigspace) 告诉编译器,模式中后跟文字间距的原子表示a "token" boundary pattern, aka ws应该被附加到原子上。
因此,这个副词处理标记化。你有没有发现我从模式中删除了\s+ 与单线中的原始模式相比?那是因为:sigspace,rule 的使用暗示,会自动处理这个问题:
say 'x@y x @ y' ~~ m:g:s /x\@y/; # (「x@y」) <-- only one match
say 'x@y x @ y' ~~ m:g /x \@ y/; # (「x@y」) <-- only one match
say 'x@y x @ y' ~~ m:g:s /x \@ y/; # (「x@y」 「x @ y」) <-- two matches
您可能想知道为什么我恢复使用/ ... / 来展示这两个示例。结果表明,虽然您可以将rule { ... } 与.match 方法(在下一节中描述)一起使用,但您不能将rule 与m 一起使用.没问题;我只是使用:s 来获得所需的效果。 (我没有费心使用:r 进行棘轮,因为它对这种模式/输入没有影响。)
为了进一步深入探讨经典正则表达式(也可以写成regex { ... })和rule 规则之间的区别,让我提一下另一个主要选项:token。 token 声明器暗示 :ratchet 副词,但不是 :sigspace 之一。因此,它还消除了regex(或/ ... /)的病态回溯风险,但是,就像regex,与rule 不同,token 忽略了开发人员在写出规则时使用的空格模式。
my \articles = input .match: pattern, :global
此行使用单行解决方案中使用的m 例程的方法形式(.match)。
使用:global 时的匹配结果是Match objects 的列表,而不仅仅是一个。在这种情况下,我们将得到三个,对应于输入文件中的三篇文章。
for articles -> $/ { put "$0: $1\n" }
此for 语句将与示例文件中的三篇文章中的每篇文章对应的Match 对象依次绑定到代码块中的符号$/ ({ ... })。
根据Raku doc on $/,“$/ 是匹配变量,因此它通常包含Match 类型的对象。”。它还提供了一些其他的便利;我们利用与编号捕获相关的这些便利之一:
插曲
这是休息的好时机。也许只是一杯,或者改天再来这里。
此答案的最后一部分建立并彻底解释了基于语法的方法。阅读它可能会提供对上述解决方案的进一步了解,并将展示如何将 Raku 的解析扩展到更复杂的场景。
但首先...
一种“无聊”的实用方法
我想用 Raku 解析这个。有人可以帮忙吗?
与使用其他工具相比,Raku 可以让编写解析器变得不那么乏味。但不那么乏味仍然乏味。并且 Raku 解析目前很慢。
在大多数情况下,当您想要解析众所周知的格式和/或非常大的文件时,实际的答案是找到并使用现有的解析器。这可能意味着根本不使用 Raku,或者使用现有的 Raku 模块,或者在 Raku 中使用现有的非 Raku 解析器。
建议的起点是在modules.raku.org 或raku.land 上搜索文件格式。查找已针对给定文件格式专门为 Raku 打包的公共共享解析模块。然后做一些简单的测试,看看你是否有好的解决方案。
在撰写本文时,“围兜”没有匹配项。
即使您不了解 C,也几乎可以肯定已经有一个“围兜”解析 C 库可供您使用。这可能是最快的解决方案。在您自己的 Raku 代码中使用外部解析库通常非常容易,即使它是用另一种编程语言编写的。
使用 C 库是通过一个名为 NativeCall 的功能完成的。我刚刚链接的文档可能太多或太少,但请随时访问the freenode IRC channel #raku 并寻求帮助。 (或发布一个 SO 问题。)我们是友好的人。 :)
如果 C 库不适合特定用例,那么您可能仍然可以通过 their respective Inline::* language adapters 使用以其他语言(例如 Perl、Python、Ruby、Lua 等)编写的包。
步骤如下:
(至少理论上是这样的。同样,如果您需要帮助,请在 IRC 频道上弹出或发布 SO 问题。)
Perl 适配器是最成熟的,所以我将使用它作为示例。假设您使用Perl's Text::BibTex packages,现在希望将 Raku 与该软件包一起使用。首先,按照它的自述文件进行设置。然后,在 Raku 中,编写如下内容:
use Text::BibTeX::BibFormat:from<Perl5>;
...
@blocks = $entry.format;
这两行的解释:
Raku 语法/解析器
好的。足够“无聊”的实用建议。现在让我们尝试为您的问题中的示例创建一个基于语法的 Raku 解析器来获得一些乐趣。
►use glot.io to run the code below
unit grammar bib;
rule TOP { <article>* }
rule article { '@article{' $<id>=<-[,]>+ ','
<kv-pairs>
'}'
}
rule kv-pairs { <kv-pair>* % ',' }
rule kv-pair { $<key>=\w* '={' ~ '}' $<value>=<-[}]>* }
有了这个语法,我们现在可以写如下内容:
die "Use CommaIDE?" unless bib .parsefile: 'derm.bib';
for $<article> -> $/ { put "$<id>: $<kv-pairs><kv-pair>[0]<value>\n" }
生成与之前的解决方案完全相同的输出。
当匹配或解析失败时,默认情况下 Raku 只返回 Nil,这是相当简洁的反馈。
有几个很好的 debugging options 可以用来判断正则表达式或语法发生了什么,但最好的选择到目前为止是使用 CommaIDE 的 Grammar-Live-View。
如果您尚未安装和使用 Comma,那么您错过了使用 Raku 的最佳部分之一。 Comma 免费版(“社区版”)内置的功能包括出色的语法开发/跟踪/调试工具。
“围兜”语法解释
unit grammar bib;
unit 声明符用于源文件的开头,告诉 Raku 文件的其余部分声明了一个特定类型的命名代码包。
grammar 关键字指定语法。语法就像一个类,但包含命名的“规则”——不仅仅是命名的方法,还命名为regexs、tokens 和rules。语法还从基本语法继承了一堆通用规则。
rule TOP {
除非您另外指定,否则在语法上调用的解析方法(.parse 和 .parsefile)首先调用名为 TOP 的语法规则(用 rule、token、regex 声明,或method 声明符)。
作为一个经验法则,如果您不知道是否应该使用rule、regex、token 或method 进行一些解析,请使用@987654463 @。 (与regex 模式不同,tokens 不会冒病态回溯的风险。)
但我使用了rule。与token 模式一样,rules 也避免了病理性回溯风险。但是,另外rules 以自然的方式将模式中的一些空白解释为重要的。 (有关详细信息,请参阅this SO answer。)
这通常适用于解析树的顶部。 (标记和偶尔的正则表达式通常适用于叶子。)
rule TOP { <article>* }
规则末尾的空格(* 和模式结束 } 之间)表示语法将匹配输入末尾的任意数量的空格。
<article> 调用此语法中的另一个命名规则。
因为看起来每个 bib 文件应该允许任意数量的文章,所以我在 <article>* 的末尾添加了一个 * (zero or more quantifier)。
rule article { '@article{' $<id>=<-[,]>+ ','
<kv-pairs>
'}'
}
如果您将这篇文章模式与我为早期基于 Raku 规则的解决方案编写的模式进行比较,您会看到各种变化:
| Rule in original one-liner |
Rule in this grammar |
| Kept pattern as simple as possible. |
Introduced <kv-pairs> and closing }
|
| No attempt to echo layout of your input. |
Visually echoes your input. |
<[...]> 是字符类的 Raku 语法,类似于传统正则表达式语法中的[...]。它更强大,但现在你只需要知道<-[,]> 中的- 表示否定,即与ye olde 正则表达式的[^,] 语法中的^ 相同。所以<-[,]>+ 尝试匹配一个或多个字符,但都不是,。
$<id>=<-[,]>+ 告诉 Raku 尝试匹配 = 右侧的量化“原子”(即 <-[,]>+ 位)并将结果存储在当前匹配对象内的键 <id> 中。后者将挂在解析树的一个分支上;我们稍后会到达确切的位置。
rule kv-pairs { <kv-pair>* % ',' }
此模式说明了几个方便的 Raku 正则表达式功能之一。它声明您要匹配零个或多个kv-pairs 以逗号分隔。
(更详细地说,% regex infix operator 要求其左侧的量化原子的匹配被其右侧的原子分开。)
rule kv-pair { $<key>=\w* '={' ~ '}' $<value>=<-[}]>* }
这里的新位是'={' ~ '}'。这是另一个方便的正则表达式功能。 regex Tilde operator 解析一个分隔结构(在这种情况下,一个带有 ={ 开启器和 } 关闭器)与分隔符之间的位匹配关闭器右侧的量化正则表达式原子。这带来了一些好处,但主要的好处是错误消息可以更清晰。
我可以在单行中的/.../ 正则表达式中使用~ 方法,反之亦然。但我希望这个语法解决方案能够继续向说明“更好的实践”习语发展。
构造/解构解析树
for $<article> { put "$<id>}: $<kv-pairs><kv-pair>[0]<value>\n" }`
$<article>、$<id> 等是指存储在“分析树”中某处的命名匹配对象。但他们是如何到达那里的? “那里”到底在哪里?
回到语法顶部:
rule TOP {
如果.parse 成功,则返回单个'TOP' 级别 匹配对象。 (解析完成后,变量$/ 也绑定到该顶部匹配对象。)在解析过程中,将通过将其他匹配对象挂在该顶部匹配对象上形成一棵树,然后其他匹配对象挂起,依此类推.
将匹配对象添加到解析树是通过将单个生成的匹配对象或它们的列表添加到“父”的Positional(编号)或Associative(命名)捕获来完成的匹配对象。这个过程在下面解释。
rule TOP { <article>* }
<article> 调用名为 article 的规则的匹配项。调用 <article> 规则有两个效果:
-
Raku 尝试匹配规则。
-
如果匹配,Raku 通过生成相应的匹配对象并将其添加到父匹配对象的键 <article> 下的解析树来捕获该匹配。 (在这种情况下,父对象是顶部匹配对象。)
如果成功匹配的模式被指定为<article>,而不是<article>*,那么只会尝试一个匹配,并且只有一个值,一个匹配对象,将在密钥 <article> 下生成并添加。
但模式是<article>*,而不仅仅是<article>。因此,Raku 尝试尽可能多地匹配article 规则。如果它完全匹配,则将一个或多个匹配对象的列表存储为<article> 键的值。 (更详细的解释请参见my answer to "How do I access the captures within a match?"。)
$<article> 是 $/<article> 的缩写。它指的是存储在当前匹配对象的<article> 键下的值(存储在$/ 中)。在这种情况下,该值是与输入中的 3 篇文章相对应的 3 个匹配对象的列表。
rule article { '@article{' $<id>=<-[,]>+ ','
正如顶部匹配对象有几个匹配对象挂在其上(存储在顶部匹配对象的 <article> 键下的文章匹配的三个捕获),这三个文章匹配对象中的每一个也都有自己的自己的“孩子”匹配悬挂在他们身上的对象。
要了解它是如何工作的,让我们只考虑三个文章匹配对象中的第一个,它对应于以“@article{garg2017patch,...”开头的文本。 article 规则与本文匹配。在进行匹配时,$<id>=<-[,]>+ 部分告诉 Raku 将与文章的 id 部分(“garg2017patch”)对应的匹配对象存储在该文章匹配对象的 <id> 键下。
希望这已经足够了(很可能太多了!),我终于可以详尽地(详尽地?)解释最后一行代码,这又是:
for $<article> -> $/ { put "$<id>: $<kv-pairs><kv-pair>[0]<value>\n" }`
在for这一层,变量$/指的是刚刚完成的解析生成的解析树的顶端。因此$<article> 是$/<article> 的简写,指的是三个文章匹配对象的列表。
然后for 遍历该列表,将$/ 在-> $/ { ... } 块的词法范围内依次绑定到这3 个文章匹配对象中的每一个。
$<id> 位是$/<id> 的简写,在块内指的是$/ 已绑定到的文章匹配对象中的<id> 键。换句话说,$<id>insideblock 等同于$<article><id>outsideblock。
$<kv-pairs><kv-pair>[0]<value> 遵循相同的方案,但在所有关键(命名/关联)子级中具有更多级别和位置子级([0])。
(请注意,article 模式不需要包含 $<kv-pairs>=<kv-pairs>,因为 Raku 只是假定 <foo> 形式的模式应该将其结果存储在键 <foo> 下。如果您希望禁用即,编写一个非字母字符作为第一个符号的模式。例如,如果您想获得与<foo> 完全相同的匹配效果,请使用<.foo>,但只是不将匹配的输入存储在解析树中。)
呼!
当自动生成的解析树不是你想要的时候
好像以上还不够,我还要提一件事。
解析树强烈地反映了语法规则的树结构,从顶部规则到叶规则相互调用。但最终的结构有时并不方便。
通常人们仍然想要一棵树,但是更简单的一棵,或者可能是一些非树的数据结构。
当自动结果不合适时,从解析中准确生成所需内容的主要机制是make。 (这可以在规则内的代码块中使用,也可以分解为与语法分离的Action classes。)
反过来,make 的主要用例是生成 a sparse tree 的挂在解析树上的节点,例如 AST。
脚注
[1] Basic Raku 适用于exploratory programming、spikes、one-offs、PoCs 和其他强调快速生成的场景可以在以后根据需要重构的工作代码。
[2] Raku 的正则表达式/规则可扩展到任意解析,如本答案后半部分所述。这与过去几代的正则表达式不同。[3]
[3] 也就是说,ZA̡͊͠͝LGΌ ISͮ̂҉̯͈͕̹̘̱ TO͇̹̺ͅƝ̴ȳ̳ TH̘Ë͖́̉ ͠P̯͍̭O̚N̐Y̡ 仍然是一本很棒且相关的读物。不是因为 Raku 规定 不能 解析 (X)HTML。原则上他们可以。但是对于像正确处理完全任意的 XHTML 这样具有纪念意义的任务,我强烈建议您使用专门为此目的编写的 existing 解析器。这通常适用于现有格式;最好不要重新发明轮子。但是 Raku 规则的好消息是,如果您需要编写一个完整的解析器,而不仅仅是一堆正则表达式,那么您可以这样做,而且不需要发疯!