#text-format #wiki #parse #mediawiki #tags #config-parser #async

async_parse_wiki_text

将 Mediawiki 中的 Wiki 文本解析成元素树

2 个版本

0.2.1 2023年6月30日
0.2.0 2023年6月30日

#13#wiki

自定义许可

225KB
6K SLoC

解析 Wiki 文本

将 Mediawiki 中的 Wiki 文本解析成元素树。

Parse Wiki Text


这实际上是 parse_wiki_text 的异步版本。

关于原始crate的异步版本的一些说明

这是为了在异步环境中使用而进行的重构。

创建此crate的原因

总的来说,crate parse_wiki_text 是可靠的,但在某些情况下并不如此。

在解析不同的 wiki-text 格式输入时,这个解析器会花费过多时间,更不用说它还会用大量数据填充内存。我假设这个解析器陷入了无限递归循环。

我无法彻底调查这个问题,因为,正如 Fredrik 下面解释的那样,wiki 格式是一团糟的规则,因此我选择了在执行时间过长时提前退出。这里应该提到的是 crate futures-time,它为异步代码提供了方便的解析函数。但这个 crate 也有一些缺点。一个长时间运行的线程不会超时,因为 futures-time crate 依赖于线程被 await 关键字中断,这意味着,旧的解析函数尽管是异步的并且设置了超时,但仍然花费了过多的时间(有时是无限长时间)来完成。

另一件事是,我不得不在输入数据周围引入一个 WikiText Wrapper 结构。原始代码使用单个线程方法,在内存中表示中使用单个 &str 以最优化工作内存的使用。我无法履行这一承诺,因此现在,异步解决方案需要更多的内存。

以下文本是 Fredrik 在原始说明书中全文复制的。


介绍

Wiki 文本是遵循 PHP 最大原则“尽可能让一切都变得不一致和混乱”的格式。有数亿个有趣的文档用这种格式编写,在主要使用 Mediawiki 软件并免费许可的网站上分发,主要是维基百科和维基词典。能够解析 Wiki 文本并处理这些文档将允许访问世界上很大一部分知识。

MediaWiki软件本身会将wiki文本文档转换成一种过时的HTML文档格式,以便在浏览器中显示给人类阅读者。它是通过一系列的字符串替换步骤来实现的,其中一些步骤取决于先前步骤的结果。这个步骤的主要文件有6200行代码,第二大的文件有2000行,还有一个只有1400行的文件,专门用于解析器的选项。解析步骤的详细信息可以在这里找到。

更有趣的是,将wiki文本文档解析成一种可以被计算机程序用来推理文档中的事实并以不同方式呈现的结构。这将使这些内容可用于各种应用。

有人尝试使用正则表达式来解析wiki文本。这在非平凡文本中是极其天真的,并且一旦文本变得复杂就会失败。正则表达式的功能远远无法满足正确解析wiki文本所需的复杂性。有一个项目勇敢地尝试使用解析器生成器来解析wiki文本。然而,wiki文本从未被设计为适用于正式解析器,因此解析器生成器在正确解析wiki文本方面也毫无帮助。

wiki文本有悠久的历史,其中包含了大量设计不良的添加项,随意堆积在一起。wiki文本的语法在每个wiki上因配置不同而不同。你甚至不知道起始标签是什么,直到看到相应的结束标签,除非解析从起始标签到结束标签之间的嵌套标签层次结构,否则你也不知道结束标签在哪里。简而言之:如果你认为自己理解了wiki文本,那么你并没有真正理解wiki文本。

Parse Wiki Text试图通过将其转换为另一种易于处理的形式,来消除解析wiki文本的所有不确定性。目标格式是Rust对象,可以使用迭代器和match表达式以 ergonomic 方式进行处理。

设计目标

正确性

Parse Wiki Text旨在解析wiki文本,与MediaWiki解析的完全一致。即使MediaWiki中显然存在bug,Parse Wiki Text也会复制这个bug。如果Parse Wiki Text没有与MediaWiki完全相同地解析某些内容,请将其报告为一个问题。

速度

Parse Wiki Text旨在以尽可能少的时间解析一个页面。它在每个处理器核心上每秒可以解析数十万个页面,并可以快速解析包含数百万页面的整个wiki。如果任何可以改变以使Parse Wiki Text更快的东西,请将其报告为一个问题。

安全性

Parse Wiki Text旨在与不受信任的输入一起工作。如果任何输入使用合理的资源无法安全解析,请将其报告为一个问题。没有使用任何不安全的代码。

平台支持

Parse Wiki Text旨在在各种环境中运行,例如

  • 运行机器码的服务器
  • 运行Web Assembly的浏览器
  • 嵌入到其他编程语言中

Parse Wiki Text可以在没有任何依赖的情况下部署到任何地方。

注意

Wiki文本是旧软件使用的旧格式。Parse Wiki Text旨在仅恢复为运行旧软件的Wiki编写的文本信息,复制在旧软件中发现的精确错误。请勿将Wiki文本作为新应用程序的格式。Wiki文本是一个糟糕的格式,具有惊人的不一致性、糟糕的设计选择和错误。对于新应用程序,请使用易于处理的格式,例如JSON,甚至更好的CBOR。参见Wikidata,了解一个使用JSON作为其格式并提供丰富界面以编辑数据而不是让人们编写代码的Wiki的示例。如果您需要将用Wiki文本编写的文本信息重新用于新应用程序,可以使用Parse Wiki Text将其转换为可以进一步处理为现代格式的中间格式。

站点配置

Wiki文本具有许多功能,其解析方式取决于Wiki的配置。这意味着在解析之前必须知道配置。

  • 只有当链接的URI方案在配置的有效协议列表中时,才会解析外部链接。当方案无效时,链接将被解析为纯文本。
  • 类别和图像表面上看起来与链接相同,但解析方式不同。这些只能通过了解Wiki配置中的命名空间别名来区分。
  • 与配置的魔法词集匹配的文本被解析为魔法词。
  • 扩展标签的语法与HTML标签相同,但解析方式不同。配置说明哪些标签名应被视为扩展标签。

可以通过向Wiki上的站点信息资源发出请求来查看配置。实用程序Fetch site configuration获取解析Wiki中页面所需的配置部分,并输出用于实例化具有该配置的解析器的Rust代码。Parse Wiki Text包含一个默认配置,可用于测试。

限制

Wiki文本从未设计成可以解析为结构化格式。它设计成可以多遍解析,每遍都依赖于上一遍的输出。最重要的是,模板在较早的遍中展开,格式化代码在较晚的遍中解析。这意味着您在原始文本中看到的格式化代码不一定与解析器在模板展开后看到的相同。幸运的是,这对人类编辑者和计算机来说都很糟糕,因此人们倾向于避免编写导致格式化代码以与阅读展开模板之前的原始Wiki文本不同的模板。Parse Wiki Text假定模板永远不会改变其周围格式化代码的含义。

沙盒

一个沙盒(Github在线尝试)可供使用,允许交互式输入Wiki文本并检查解析结果。

与Mediawiki解析器的比较

还有一个名为Mediawiki Parser的crate(crates.ioGithub),基本上做的是同一件事,将Wiki文本解析为一棵元素树。然而,该crate没有考虑到正确解析Wiki文本所需的惊人数量奇怪之处。该crate诚然仅解析Wiki文本的子集,目的是报告任何不适合该子集的文本的错误,这是一个良好的意图,但在检查它时,发现该子集很快就被发现太小,无法解析来自实际Wiki的页面,更糟糕的是,错误报告只是一个空洞的承诺,没有迹象表明文本被错误解析。

该存储库可能可以通过始终在文本不在支持子集内时报告错误来改进,但实际维基中的页面往往不符合可以无怪异现象解析的维基文本的小子集,因此这仍然没有用。将存储库改进为正确解析足够大的维基文本子集所需的努力与从头开始一样多,这就是为什么Parse Wiki Text没有从Mediawiki Parser中借用任何内容而创建的原因。Parse Wiki Text旨在正确解析所有维基文本,而不仅仅是子集,并在遇到应避免的怪异现象时报告警告。

示例

默认配置仅用于测试目的。解析真实维基需要特定于站点的配置。在解析多个页面以提高效率时,请重用相同的配置。

use parse_wiki_text::{Configuration, Node};
let wiki_text = concat!(
    "==Our values==\n",
    "*Correctness\n",
    "*Speed\n",
    "*Ergonomics"
);
let result = Configuration::default().parse(wiki_text);
assert!(result.warnings.is_empty());
for node in result.nodes {
    if let Node::UnorderedList { items, .. } = node {
        println!("Our values are:");
        for item in items {
            println!("- {}", item.nodes.iter().map(|node| match node {
                Node::Text { value, .. } => value,
                _ => ""
            }).collect::<String>());
        }
    }
}

依赖关系

~2.3–4MB
~65K SLoC