维基百科是世界上访问量最高的网站之一。这个庞大的在线百科全书,任何人都可以编辑,已经成为任何主题的通用信息的首选来源。然而,维基百科使用的“众包”模式也为歪曲事实和粉饰太平的编辑敞开了大门——这些编辑可能不那么符合事实。为了帮助记者、公民和行动主义者追踪这些编辑,TWG(The Working Group 工作组)与 Metro News 和 Center for Investigative Reporting(调查报道中心)合作构建了 WikiWash。
WikiWash 是一款开源工具,任何拥有网络浏览器的人都可以观察维基百科页面的历史记录,并实时查看编辑。记者们已经开始使用 WikiWash 来挖掘有争议的维基百科页面的历史。
构建 WikiWash 这个用户友好的应用程序,它建立在维基百科庞大的数据集之上,需要克服许多技术挑战。下面,我们详细介绍了我们针对其中一些挑战的解决方案,其他构建基于维基百科的软件的人可能会觉得这些方案很有帮助。
为了促进对该项目的开源贡献,WikiWash 使用 Javascript 构建,Javascript 是当今最易于访问和最灵活的流行编程语言之一。还使用了一些网络上最受欢迎的新技术,包括 Node.js、Express.js、Angular.js 和 Socket.IO。
访问维基百科的数据库
为了获取有关维基百科页面历史记录的数据,WikiWash 依赖于维基百科的 API。维基百科通过其 API 提供对其庞大数据库中几乎所有数据的访问权限,从而允许开发人员基于这些数据构建应用程序。不幸的是,并非所有这些数据都可以快速获得。我们从维基百科需要的大量数据,例如与每个修订版本关联的用户名和修改时间列表,几乎可以立即获得。但是,每个维基百科页面的每个修订版本的实际内容生成速度都非常慢。要了解为什么会这样,我们需要首先了解维基百科是如何运作的。
让我们看一下维基百科访问量最高的页面之一,现任美国总统 贝拉克·奥巴马 的页面。此页面的开头段落是一个人类可读的文本块、链接和符号
然而,维基百科以一种称为“wiki 标记”或“wikitext”的格式存储此页面(以及其数据库中的每个页面)。上面对应的介绍段落在 Wikitext 中看起来像这样
为了准备好每个页面供用户查看,维基百科必须首先将存储在其数据库中的 Wikitext 页面转换为 HTML,这是 Web 浏览器使用的语言。由于多种原因,此操作要求很高
- Wikitext 允许在任何页面中使用动态模板,从而允许在一个页面中包含其他小页面。例如,许多维基百科页面右侧出现的“信息框”就是这些模板之一。每个模板都必须单独获取(因为它也是一个可以修改的页面),并使用为要放置它的特定页面提供的信息进行自定义。(前面引用的贝拉克·奥巴马页面在撰写本文时包含 591 个模板。)
- 维基百科必须检查页面中的每个链接,以验证该链接是否已损坏,并获取链接页面的名称。(贝拉克·奥巴马页面包含 758 个指向其他页面的链接。)
- 参考文献和引文必须在页面底部计算、链接和正确格式化。对于许多页面来说,这不是问题,但是许多写得很好的页面都有成百上千个修订版本。(贝拉克·奥巴马的例子有 464 个引文。)
考虑到生成每个维基百科页面所需的工作量,维基百科最初是如何做到如此之快的?答案很简单:维基百科广泛使用缓存。每个页面在编辑后,都会生成并保存其最新修订版本。将来每次有人访问该页面时,维基百科都不再需要将该页面转换为 HTML,而是可以从缓存中提供最新版本。
另一方面,WikiWash 没有这种便利。每次有人使用 WikiWash 查看页面的历史记录时,维基百科都必须将页面的每个版本转换为 HTML,因为维基百科仅存储每个页面最新版本的缓存副本。不幸的是,这会使每个修订版本的加载时间增加几秒钟。
维基百科上随机抽样的修订版本的 WikiText 和 HTML 版本的加载时间。请注意,将页面渲染为 HTML 的操作比仅获取页面的 WikiText 版本的时间长 30 倍。
为了缓解 WikiWash 中的这个问题,我们实施了一种称为抢占式缓存的策略,以加快修订版本的加载速度。当用户在 WikiWash 中加载页面时,我们首先立即从维基百科请求页面的最新修订版本和次新修订版本。根据页面大小,初始加载可能需要三到三十秒的时间。
一旦读取页面的第一个修订版本,我们注意到大多数用户倾向于在几秒钟后单击列表中的下一个修订版本。为了使第二次点击体验更加流畅,WikiWash 抢先从维基百科加载列表中的每个修订版本。当用户单击修订版本时,WikiWash 可能已经缓存了他们单击的修订版本。(我们还注意了维基百科的 API 使用指南,以避免对其服务器造成不必要的负载,同时确保流畅的用户体验。)
这种缓存解决方案并非万无一失,因为用户仍然必须等待几秒钟才能加载第一个页面。但是,在一般情况下,此策略使 WikiWash 看起来响应速度更快。幸运的是,所有缓存的数据在很大程度上都是幂等的。除了包含的模板外,维基百科页面修订版本中的内容永远不会更改。这意味着我们可以无限期地缓存每个修订版本,以加快将来所有访问者加载特定修订版本的页面速度——事实上,WikiWash 正是这样做的。
显示页面历史记录中的更改
由于 WikiWash 本质上是维基百科的历史记录浏览器,其主要目的是显示页面修订版本之间的差异。为此,它使用一种称为 Myer's Diff 算法 的复杂算法来确定页面中添加或删除了哪些文本。WikiWash 没有从头开始实现该算法,而是使用了 Google 的 Neil Fraser 实现的 该算法的 Javascript 端口。
此算法非常适合显示多个纯文本文档之间的差异,但是当给定像我们从维基百科收到的 HTML 这样的复杂结构化内容时,其结果就毫无用处。WikiWash 插入 HTML 标签以显示内容差异——但是如果差异发生在现有的 HTML 标签内,我们简单的处理过程会将 HTML 标签插入到其他 HTML 标签内,从而导致 HTML 无效
当插入 HTML 标签以指示 HTML 中的差异时,生成的文档可能无效。这里,删除的文本以红色显示,并用 <del> 标签包围,而插入的文本以绿色显示,并用 <ins> 标签包围。
为了规避这个问题,WikiWash 在进行差异比较之前处理 HTML,然后处理差异比较的输出。在运行算法以确定差异之前,WikiWash 将所有 HTML 标签替换为单个 Unicode 字符——来自 Unicode 私用区 的字符,保证这些字符在维基百科文章中尚不存在。这之所以有效,是因为差异算法通过查看特定字符来查找差异。通过将每个标签替换为唯一字符,我们强制算法检测到的每个差异都为“全有或全无”——要么标签已完全更改,要么根本没有更改。
将 HTML 标签替换为唯一的 Unicode 字符(此处用“\xE000”之类的标签表示)可以防止差异算法更改标签内部,从而防止 WikiWash 生成无效的 HTML。
在对结果输出运行差异算法后,WikiWash 通过将每个 Unicode 字符替换为其对应的 HTML 标签来解码文本。结果是现在语法上有效的 HTML——也就是说,没有标签损坏。但是,仍然存在一个问题:生成的文档可能具有不正确的结构。要了解为什么会这样,让我们看一下上面翻译回 HTML 的示例
中间的 HTML 代码片段显示,带有 class "class1" 的开始 "div" 标签被删除,并且插入了带有 class "class2" 的开始 "div" 标签。但是,由于 HTML 是一种分层或基于树的语言,因此此代码片段无效。虽然开始 "div" 标签确实被替换了,但表示这一点的正确方法是将标签的开始和结束(从开始 "<div" 到结束 "</div>")都包含在差异中。这就是 WikiWash 所做的——当它检测到标签的开头已更改时,它会找到从标签的开头到结尾的整个标签,并用插入或删除标签将其包围,如下所示
此解决方案并非完美无缺——即,如果我们从维基百科收到的 HTML 格式错误,或者我们从差异算法收到的差异以意想不到的方式格式化,则此解决方案将失败。但是,此解决方案在我们在大多数尝试过的案例中都解决了问题。(细心的读者可能会建议使用 基于树的差异算法 来完全解决此问题。不幸的是,这种算法在实践中相当慢且难以实现。)
WikiWash 是一款复杂的工具,旨在解决一个非常简单的问题——查看维基百科(世界上最大和最受欢迎的网站之一)中的更改。这些只是我们在构建此工具时必须克服的一些技术挑战,我们邀请您 在 Github 上 Fork 该项目,以帮助我们改进它。
最初发布在 The Working Group 博客上。通过 Creative Commons 重新发布。
评论已关闭。