使用 Raku(以前称为 Perl 6)从 .bib 文件中提取

Extracting from .bib file with Raku (previously aka Perl 6)

我在用 LaTeX 写论文时有这个 .bib file 用于参考管理:

@article{garg2017patch,
  title={Patch testing in patients with suspected cosmetic dermatitis: A retrospective study},
  author={Garg, Taru and Agarwal, Soumya and Chander, Ram and Singh, Aashim and Yadav, Pravesh},
  journal={Journal of Cosmetic Dermatology},
  year={2017},
  publisher={Wiley Online Library}
}

@article{hauso2008neuroendocrine,
  title={Neuroendocrine tumor epidemiology},
  author={Hauso, Oyvind and Gustafsson, Bjorn I and Kidd, Mark and Waldum, Helge L and Drozdov, Ignat and Chan, Anthony KC and Modlin, Irvin M},
  journal={Cancer},
  volume={113},
  number={10},
  pages={2655--2664},
  year={2008},
  publisher={Wiley Online Library}
}

@article{siperstein1997laparoscopic,
  title={Laparoscopic thermal ablation of hepatic neuroendocrine tumor metastases},
  author={Siperstein, Allan E and Rogers, Stanley J and Hansen, Paul D and Gitomirsky, Alexis},
  journal={Surgery},
  volume={122},
  number={6},
  pages={1147--1155},
  year={1997},
  publisher={Elsevier}
}

如果谁想知道什么是bib文件,可以找详细的here

我想用 Perl 6 解析它以提取密钥和标题,如下所示:

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

你能帮我做这件事吗,可能有两种方式:

  1. 使用基本的 Perl 6
  2. 使用 Perl 6 语法

TL;DR

  • 一个完整而详细的答案,完全符合@Suman 的要求。

  • 对“我想解析 X。有人能帮忙吗?”的介绍性一般回答

一个one-liner在一个shell

我将从最适合某些场景的简洁代码开始[1],如果他们是熟悉 shell 和 Raku 基础知识并匆忙:

> raku -e 'for slurp() ~~ m:g / "@article\{" (<-[,]>+) \, \s+
"title=\{" (<-[}]>+) \} / -> $/ { put "[=10=]: \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 转义并添加:

  • 白色space.

  • 评论。

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 "[=12=]: \n" }           # for each article, print "article id: title".

如果上面的内容看起来仍然像官方文章,请不要担心。后面的部分解释了上述内容,同时还介绍了更通用、更简洁、更易读的代码。[2]

四个语句而不是一个

my \input = slurp;

my \pattern = rule { '@article{' ( <-[,]>+ ) ','
                       'title={' ( <-[}]>+ ) }

my \articles = input .match: pattern, :global;

for articles -> $/ { put "[=13=]: \n" }

my 声明了一个词法变量。 Raku 支持变量名开头的 sigils。但它也允许开发人员像我所做的那样“削减它们”。

my \pattern ...

my \pattern = rule { '@article{' ( <-[,]>+ ) ','
                       'title={' ( <-[}]>+ ) }

我已将模式语法从原始 one-liner 中的 / ... / 切换为 rule { ... }。我这样做是为了:

  • 消除病态回溯风险

    经典正则表达式风险 pathological backtracking。如果您可以杀死一个失控的程序,那很好,但是单击 link 了解它会变得多么糟糕!我们不需要回溯来匹配 .bib 格式。

  • 传达模式是rule

    如果您编写了大量模式匹配代码,您将经常需要使用 rule { ... }rule 消除了刚刚描述的 classic 正则表达式问题(病态回溯)的任何风险,并且具有另一个超能力。在先介绍与那些超级大国相对应的副词之后,我将在下面涵盖这两个方面。


Raku regexes/rules 可以(经常)与 "adverbs" 一起使用。这些是修改模式应用方式的便捷快捷方式。

我已经在这段代码的早期版本中使用了一个副词。 “全局”副词(使用 :global 或其 shorthand 别名 :g 指定)指示匹配引擎消耗 all 输入,生成 一个包含尽可能多匹配项的列表,而不是return只包含第一个匹配项。


虽然副词有 shorthand 个别名,但有些被反复使用,因此将它们捆绑到不同的规则声明符中会更整洁。这就是我使用 rule 的原因。它捆绑了两个适合匹配许多数据格式的副词,例如 .bib 文件:


Ratcheting (:r / :ratchet) 告诉编译器,当一个“原子”(规则中的 sub-pattern 被视为一个单元)匹配时,不能回头。如果同一规则中模式中更靠后的原子失败,则整个规则立即失败。

这消除了前面讨论的“病态回溯”的任何风险。


Significant space 处理 (:s / :sigspace) 告诉编译器一个原子后跟 文字间距 在模式中表示a "token" boundary pattern, aka ws应该附加到原子。

因此这个副词处理分词。您是否发现与 one-liner 中的原始模式相比,我从模式中删除了 \s+?这是因为 :sigspacerule 的使用暗示,会自动处理:

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 方法(在下一节中描述)一起使用时,您 不能 使用 rulem。没问题;我只是使用 :s 来获得预期的效果。 (我没有费心使用 :r 来进行棘轮调整,因为这对 pattern/input 没有影响。)


为了深入探讨 classic 正则表达式(也可以写成 regex { ... })和 rule 规则之间的区别,让我提一下另一个主要选项:tokentoken 声明符暗示 :ratchet 副词,而不是 :sigspace 副词。因此它也消除了 regex(或 / ... /)的病态回溯风险,但是,就像 regexrule 不同,token 会忽略whitespace 开发人员在编写规则模式时使用。

my \articles = input .match: pattern, :global

此行使用 one-liner 解决方案中使用的 m 例程的方法形式 (.match)。

使用:global匹配的结果是一个lisMatch objects 而不是一个。在这种情况下,我们将得到三个,对应于输入文件中的三篇文章。

for articles -> $/ { put "[=78=]: \n" }

for 语句将与示例文件中的三篇文章中的每一篇相对应的 Match object 依次绑定到代码块内的符号 $/ ({ ... }).

根据 Raku doc on $/,“$/ 是匹配变量,因此它通常包含 object 类型 Match”。它还提供了一些其他的便利;我们利用与编号捕获相关的这些便利之一:

  • 前面匹配的pattern包含两对parentheses;

  • 整体 Match object ($/) 通过 Positional subscripting (postfix []),因此在 for 的块中,$/[0]$/[1] 提供对每篇文章的两个位置捕获的访问;

  • 为了方便起见,Raku 将 [=93=] 别名为 $/[0](等等),因此大多数开发者都使用较短的语法。

插曲

现在是休息的好时机。也许只是一杯茶,或者 return 改天再来。

这个答案的最后一部分建立并彻底解释了 grammar-based 方法。阅读它可能会进一步深入了解上述解决方案,并将展示如何将 Raku 的解析扩展到更复杂的场景。

但首先...

一个“无聊”的实用方法

I want to parse this with Raku. Can anyone help?

与使用其他工具相比,Raku 可能会使编写解析器变得不那么乏味。但是不那么乏味仍然很乏味。并且 Raku 解析目前很慢。

在大多数情况下,当您想解析众所周知的格式 and/or 非常大的文件时,实际的答案是找到并使用现有的解析器。这可能意味着根本不使用 Raku,或者使用现有的 Raku 模块,或者在 Raku 中使用现有的 non-Raku 解析器。


建议的起点是在 modules.raku.org or raku.land 上搜索文件格式。寻找已针对给定文件格式专门为 Raku 打包的公开共享解析模块。然后做一些简单的测试,看看你有没有好的解决方案。

在撰写本文时,'bib' 没有匹配项。


即使您不了解 C,也几乎可以肯定有一个 'bib' 解析 C 库可供您使用。而且它可能是最快的解决方案。在您自己的 Raku 代码中使用外部库通常非常容易,即使它是用另一种编程语言编写的。

使用 C 库是通过一个名为 NativeCall. The doc I just linked may well be too much or too little, but please feel free to visit the freenode IRC channel #raku 的功能完成的,并寻求帮助。 (或者 post 一个 SO 问题。)我们是友好的人。 :)


如果 C 库不适合特定用例,那么您可能仍然可以使用用其他语言编写的包,例如 Perl、Python、Ruby、Lua, 等等 通过 their respective Inline::* language adapters.

步骤是:

  • 安装一个包(用 Perl,Python 或其他语言编写);

  • 确保它 运行 在您的系统上使用与其编写语言对应的编译器;

  • 安装适当的内联语言适配器,让 Raku 运行 以其他语言打包;

  • 包含导出的 Raku 函数、classes、objects、值等

(至少,理论上是这样。同样,如果您需要帮助,请打开 IRC 频道或 post 一个 SO 问题。)

Perl 适配器是最成熟的,所以我将以它为例。假设您使用 Perl's Text::BibTex packages 并且现在希望将 Raku 与该软件包一起使用。首先,根据其自述文件进行设置。然后,在 Raku 中,这样写:

use Text::BibTeX::BibFormat:from<Perl5>;
...
@blocks = $entry.format;

这两行的解释:

  • 第一行是告诉 Raku 您希望加载 Perl 模块的方式。

    (除非 Inline::Perl5 已经安装并正常工作,否则它不会工作。但如果你使用的是流行的 Raku 捆绑包,它应该是。如果没有,你至少应该有模块安装程序 zef 所以你可以 运行 zef install Inline::Perl5.)

  • 最后一行只是 the SYNOPSIS of the Perl package Text::BibTeX::BibFormat.

    @blocks = $entry->format; 行的机械 Raku 翻译

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 只是 returns Nil,这是相当简洁的反馈。

有几个不错debugging options to figure out what's going on with a regex or grammar, but the best option by far is to use CommaIDE's Grammar-Live-View.

如果您还没有安装和使用 Comma,那么您就错过了最好的 pa 之一使用 Raku 的技巧。免费版 Comma(“社区版”)内置的功能包括出色的语法开发/跟踪/调试工具。

'bib'语法的解释

unit grammar bib;

unit 声明符用于源文件的开头,告诉 Raku 文件的其余部分声明了特定类型的命名代码包。

grammar关键字指定了一个语法。语法类似于 class,但包含命名的“规则”——不仅是命名的方法,而且还命名为 regexs、tokens 和 rules。语法还从基本语法继承了一堆通用规则。


rule TOP {

除非您另外指定,否则在语法上调用的解析方法(.parse.parsefile)首先调用名为 TOP 的语法规则(用 [=36 声明) =]、tokenregexmethod 声明符)。

根据,呃,经验法则,如果您不知道是否应该使用 ruleregextokenmethod对于一些解析,使用 token。 (与 regex 模式不同,token 不会冒病态回溯的风险。)

但我用过rule。与 token 模式一样,rules 也避免了病理性回溯风险。但是,另外 rules 以自然的方式将模式中的一些白色 space 解释为重要的。 (有关详细信息,请参阅 。)

rules 通常适用于分析树的顶部。 (令牌通常适合叶子。)


rule TOP { <article>* }

规则末尾的space(在*和模式结束}之间)意味着语法将匹配任意数量的白色space输入结束。

<article> 调用此语法中的另一个命名规则。

因为看起来每个 bib 文件应该允许任意数量的文章,所以我在 <article>*.[=230= 的末尾添加了一个 * (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.

<[...]> 是字符 class 的 Raku 语法,类似于传统正则表达式语法中的 [...]。它更强大,但现在你只需要知道 <-[,]> 中的 - 表示否定,即与 ye olde [^,] 语法中的 ^ 相同正则表达式。因此 <-[,]>+ 尝试匹配一个或多个字符,none 其中 ,.

$<id>=<-[,]>+ 告诉 Raku 尝试匹配 = 右侧的量化“原子”(即 <-[,]>+ 位)并将结果存储在键 <id> 在当前匹配中 object。后者将挂在解析树的一个分支上;我们稍后会到达准确位置。


rule kv-pairs { <kv-pair>* % ',' }

此模式说明了几个方便的 Raku 正则表达式功能之一。它声明您要匹配零个或多个 kv-pairs 以逗号分隔 .

(更详细地说,% regex infix operator要求左边的量化原子的匹配被右边的原子隔开。)


rule kv-pair { $<key>=\w* '={' ~ '}' $<value>=<-[}]>* }

这里的新位是 '={' ~ '}'。这是另一个方便的正则表达式功能。 regex Tilde operator 解析定界结构(在本例中为 ={ opener 和 } closer),定界符之间的位与 closer 右侧的量化正则表达式原子匹配。这带来了几个好处,但主要的一个是错误消息可以更清晰。

我本可以在 one-liner 和 vice-versa 的 /.../ 正则表达式中使用 ~ 方法。但我希望这个语法解决方案能够继续朝着说明“更好的做法”习语的方向发展。

构造/解构解析树

for $<article> { put "$<id>: $<kv-pairs><kv-pair>[0]<value>\n" }`

$<article>$<id> 等指的是存储在“解析树”某处的命名匹配 object。但是他们是怎么到那里的呢? “那里”到底在哪里?


返回语法顶部:

rule TOP {

如果 .parse 成功,则 'TOP' 级别 匹配 object 被 return 编辑。 (在解析完成后,变量 $/ 也绑定到那个顶级匹配 object。)在解析期间,通过将其他匹配 object 挂在这个顶级匹配 object,然后其他人挂掉那些,依此类推。

将匹配项 object 添加到解析树是通过将单个生成的匹配项 object 或它们的列表添加到 Positional (numbered) or Associative (命名的)来完成的捕获“parent”匹配 object。这个过程解释如下。


rule TOP { <article>* }

<article> 调用名为 article 的规则的匹配项。调用规则 <article> 有两个效果:

  1. Raku 尝试匹配规则。

  2. 如果匹配,Raku 通过生成相应的匹配项 object 并将其添加到关键字 <article> 下的解析树中来捕获该匹配项parent 匹配 object。 (在这种情况下,parent 是最匹配的 object。)

如果成功匹配的模式被指定为<article>,而不是<article>*,那么只会尝试一个匹配,并且只会生成一个值,一个匹配 object,并添加到键 <article>.

但模式是 <article>*,而不仅仅是 <article>。因此 Raku 会尽可能多地尝试匹配 article 规则 。如果它完全匹配 ,则一个或多个匹配 object 的 list 被存储为 [=130] 的值=] 键。 (有关更详细的说明,请参阅 。)

$<article>$/<article> 的缩写。它指的是当前匹配object的<article>键下存储的值(存储在$/中)。在这种情况下,该值是与输入中的 3 篇文章相对应的 3 个匹配 object 的列表。


rule article { '@article{' $<id>=<-[,]>+ ','

正如顶级比赛 object 有几个比赛 object 挂在它上面(存储在顶级比赛 object 下的三个文章比赛的捕获 <article> 键),这三个文章匹配项 object 中的每一个也都有自己的“child”匹配项 objects。

要了解它是如何工作的,让我们只考虑匹配 object 的三篇文章中的 第一篇 ,对应于以“@article{ garg2017 补丁,...”。 article 规则匹配这篇文章。在进行匹配时,$<id>=<-[,]>+ 部分告诉 Raku 将与文章的 id 部分(“garg2017patch”)对应的匹配项 object 存储在该文章匹配项 object 的 <id>键。


希望这就足够了(很可能太多了!)我终于可以详尽地(详尽地?)解释最后一行代码,它再次是:

for $<article> -> $/ { put "$<id>: $<kv-pairs><kv-pair>[0]<value>\n" }`

for层,变量$/指的是刚刚完成的解析生成的解析树的顶端。因此$<article>,也就是shorthand for $/<article>,指的是三篇文章匹配的列表objects.

然后 for 遍历该列表,将 -> $/ { ... } 块的词法范围内的 $/ 依次绑定到这 3 个文章匹配项中的每一个 object .

$<id> 位是 shorthand 对于 $/<id>,它在块内指的是文章中的 <id> 键匹配 object 那个 $/已绑定。换句话说,$<id> inside block等同于$<article><id> outside block.

$<kv-pairs><kv-pair>[0]<value> 遵循相同的方案,尽管在所有键(命名/关联)中间有更多级别和位置 child([0])child仁.

(请注意,article 模式不需要包含 $<kv-pairs>=<kv-pairs>,因为 Raku 只是假定 <foo> 形式的模式应该将其结果存储在键 <foo>。如果你想禁用它,写一个以 non-alpha 字符作为第一个符号的模式。例如,如果你想与 [= 具有完全相同的匹配效果,请使用 <.foo> 194=] 但只是不将匹配的输入存储在解析树中。)

呸!

当自动生成的解析树不是您想要的时

好像上面这些还不够,我还要提一件事。

解析树强烈反映了从顶层规则到叶规则相互调用的语法规则的树结构。但最终的结构有时会带来不便。

通常人们仍然想要一棵树,但更简单的树,或者可能是一些 non-tree 数据结构。

当自动结果不合适时,从解析中准确生成所需内容的主要机制是 make. (This can be used in code blocks inside rules or factored out into Action classes,它与语法分开。)

反过来,make 的主要用例是生成 a sparse tree 节点挂在解析树上,例如 AST。

脚注

[1] Basic Raku 适用于 exploratory programming, spikes, one-offs, PoCs 和其他强调快速生成工作代码的场景如果需要,可以稍后重构。

[2] Raku 的 regexes/rules 扩展到任意解析,如本答案后半部分所述。这与过去几代的正则表达式形成鲜明对比。[3]

[3] 也就是说,ZA̡͊͠͝LGΌ ISͮ̂҉̯͈͕̹̘̱ TO͇̹̺ͅƝ̴ȳ̳ TH̘Ë͖́̉ ͠P̯͍̭O̚​N̐Y̡ 仍然是一篇很棒且相关的读物。不是因为 Raku 规则不能 解析 (X)HTML。原则上他们可以。但是对于像正确处理完整的任意 in-the-wild XHTML 这样艰巨的任务,我强烈建议您使用专门为此目的编写的 existing 解析器。这通常适用于现有格式;最好不要重新发明轮子。但是 Raku 规则的好消息是,如果你 需要 写一个完整的解析器,不只是一堆正则表达式,你可以这样做,而且不需要发疯!