解析 Wikipedia XML 转储






4.94/5 (11投票s)
本文介绍了一个维基百科页面从 XML 摘要中提取的解析器。以提取传记数据和类别及其父类别为例。
引言
维基百科是数据挖掘的绝佳对象,许多研究都专注于从中检索感兴趣信息的各种技术。对于在线提取,Rion Williams 在他的项目中利用了 Petr Onderka 设计的一个强大库。维基百科本身提供了GUI 工具等。在线方法的优势在于它始终返回最新信息。虽然维基百科变化迅速,但关于特定主题的信息在很长一段时间内基本保持不变。因此,摘要的新鲜度并不那么重要。同时,在线方法在返回结果的大小和编程查询的兴趣方面存在明显限制。
对于离线研究,维基百科会定期更新每种语言的摘要文件。它们的结构可能略有不同。WikiExtractor.py 是一个用于获取意大利语页面纯文本的 Python 脚本。还有许多其他解析器。然而,没有一个项目能满足所有需求。
在本文中,我们
- 介绍了一个非常简单的解析器,用于解析“pages-articles” XML 摘要文件,能够提取页面的正文、所属类别的页面标题以及类别名称及其父类别。正文未清理标记,特别是,因为它可能包含重要信息。
- 计算每个维基百科文章页面被其他页面引用的次数。这些数字可用于一种“引文索引”,衡量页面的重要性。
- 生成一份传记数据列表,包含一百多万有已知出生和/或死亡年份的人物,并保留他们所属的类别。其中约一半有这两个日期,因此可以计算年龄。这样的列表可用于社会学或历史研究,类似于我们之前使用较少数据进行的研究。
解析器生成的输出足以进行简单分类。如果一个类别列在页面的父类别中,则该页面匹配该类别。然而,感兴趣的类别通常不在直接父类别中提及。最深的子类别(至少应该)被列出。要揭示类别之间多层级的父子关系,需要构建整个类别层级图。我们将在下一篇文章中展示这样的层级图以及相应的分类器。
背景
我们建议熟悉维基百科页面创建的过程、它们的结构以及它们的分类。请查看实际“pages-articles” XML 文件的内容。我们使用了enwiki-20160305-pages-articles-multistream.xml.bz2 文件(约 12.7G 的压缩文件,包含 52.5G 数据)。最新的摘要可以在通用的维基百科数据库下载地点找到。至少,请预览其中 0.05% 的随机选择,仅 24M,位于项目...\bin\Debug 目录下。此文件包含与巨大原始文件相同的标题部分。
从 C# 方面来说,最好具备使用 XmlReader
、Dictionary<>
、HashSet<>
类、一些 LINQ 以及正则表达式的经验。
主程序和测试运行
提供的 C# 主程序演示了该解析器的用法。
private static void Main(string[] args)
{
WikiXmlParser parser = new WikiXmlParser(@"enwiki-pages-articles-test.xml");
parser.ExtractPagesAndHierarchy(
"Pages&Parents.txt",
"Categories&Parents.txt",
"BiographicalPages.txt"
);
}
WikiXmlParser
对象的构造函数需要一个现有 XML 摘要的路径。在上面的代码片段中,使用了位于...\bin\Debug 目录下的相对较小的示例 XML 文件。假设程序是从该文件夹启动的,它会在那里创建三个制表符分隔的文件:
-
"Pages&Parents.txt" 文件包含三列:页面标题、引用数,以及用“
|
”分隔的类别。示例文本行:Alchemy 433 Alchemy|Hermeticism|Esotericism|Alchemists|
-
"Categories&Parents.txt",两列:类别及其用“
|
”分隔的父类别。示例文本行:Computer science Applied sciences|Computing|
- "BiographicalPages.txt",六列:姓名(页面标题)、出生年份、死亡年份、年龄、引用数,以及用“
|
”分隔的类别。示例文本行:Aristotle -380 -320 60 3958 ...Academic philosophers|Empiricists|Meteorologists|Zoologists|...
现在请在 MS Excel、MS Access 或其他数据库中打开“BiographicalPages.txt”,并按引用(链接)数对表格进行数值排序。您会发现只有少数行具有非零的引用值。这是因为测试中只使用了实际 XML 摘要的一小部分。然而,即使在这种情况下,Michael Jackson 也获得了四次引用……请按出生、死亡和年龄年份列对表格进行数值排序。结果是合理的。
当输出文件的结构及其目的变得清晰时,就可以使用上面的链接下载完整存档,从其中提取 XML 文件,并在 WikiXMLParser
构造函数中指定 XML 的路径。处理维基百科文章 XML 摘要就只需要这些。
现在请分析完整的结果,并按上述方式对传记表格进行排序。您会看到大约 25 行,年龄为负数或不切实际的大。这些行的确切数量意义不大,因为您使用的摘要可能与上面提到的摘要略有不同。它们代表了维基百科中的实际错误。页面由人们编写,因此不太可能消除所有错误。然而,这种明显错误的比例小于 0.005%,证明了维基百科数据的极高质量。另一方面,发现此类错误的能力是我们解析器的额外好处。
页面类
Page
是一个类,它提供对维基百科页面属性的公共访问,例如:string Title
是当前页面的标题。它也是在线页面地址的后缀:https://en.wikipedia.org/wiki/[ Title ]。string Text
是转储的维基百科页面中<text>
标签的内容。int Namespace
是页面的“命名空间”。命名空间在 XML 的开头进行了简要解释。通用文章分配值 0,类别分配 14,文件分配 6,模板分配 10,草稿分配 118 等。
其他可能的属性,如“id
”、“revision
”、“timestamp
”、“contributor
”等,不在我们的兴趣范围内,但如果需要,可以以同样的方式提取。请查看 XML 摘要以了解元素的可能名称、格式和含义,或阅读相应的维基百科解释。
在用
private void GetReferences()
方法处理 Text 后,两个重要的集合将公开可用:
HashSet<string> Links
存储指向其他页面的链接标题。每个链接只计数一次。HashSet<string> Parents
包含页面所属类别的不同标题。
这些集合在第一次调用它们时,内部计算一次。
有一个 public
方法用于解析“Infobox
”
bool ParseInfobox(
out string title,
out List<KeyValuePair<string, string>> keysAndValues)
它在我们的测试程序中未执行,但可能对研究人员感兴趣。
让我们熟悉一下“infobox”。它不是页面必需的部分,但有时可能提供父类别中未包含的有价值信息。以下 XML 文件摘录是一个示例:
{{Infobox writer
| name = George MacDonald
| image = George MacDonald 1860s.jpg
| imagesize = 225px
| caption = George MacDonald in the 1860s
| pseudonym =
| birth_date = {{birth date|1824|12|10|df=y}}
| birth_place = [[Huntly, Scotland|Huntly]],
[[Aberdeenshire (traditional)|Aberdeenshire]], Scotland
| death_date = {{death date and age|1905|9|18|1824|12|10|df=y}}
| death_place = [[Ashtead]], Surrey, England,
[[United Kingdom of Great Britain and Ireland]]
| occupation = [[Minister (Christianity)|Minister]], Writer (poet, novelist)
| nationality = Scottish/British
| period = 19th century
| genre = Children's literature <!-- [[Fantasy literature|Fantasy]],
[[Christian apologetics]] -->
| subject =
| movement =
| signature =
| website =
| notableworks = ''[[Lilith (novel)|Lilith]]'', ''[[Phantastes]]'',
''[[David Elginbrod]]'', '...
| influences = [[Novalis]], [[Friedrich de la Motte Fouqué|Fouqué]],
[[Edmund Spenser|Spenser]],...
| influenced = [[C. S. Lewis]], [[J. R. R. Tolkien]],
[[G. K. Chesterton]], [[Mark Twain]],...
}}
实际上,infobox
是一个相当任意的键值对列表。ParseInfobox (...)
提取 infobox
的标题以及 List<KeyValuePair<string, string>
,其中包含所有键及其非空值。研究人员可以关注特定的键并解析它们的值。我们不提供“通用”解析器,因为格式太多。我们希望保持代码尽可能简单。幸运的是,许多 infobox
值根本不需要任何特殊解释。像 [[...|...|...]] 这样的构造以与正文相同的方式使用,并表示到维基百科页面的链接。第一个用“|
”分隔的字符串是引用的页面的标题,其他用于显示目的。解析日期需要更多工作,但这是直接的。
Page
类的对象在 WikiXMlParser
类读取 XML 摘要时遇到维基百科页面时构建。
WikiXmlParser 类的公共方法
构造函数
WikiXmlParser(string pathToXMLDumpFile)
只需创建一个私有的 System.Xml.XmlReader
对象来处理给定的 XML 文件。
Page GetNextPage()
函数通过页面逐页、仅向前读取 XML 文件来创建当前的 Page
对象,使用 <font face="Courier New">XmlReader</font>
。该方法应在循环中调用。
while ((page = GetNextPage()) != null)
{
// Do something using properties of page.
}
在 GetNextPage()
内部,我们读取与当前页面相关的 XML 子树,并通过其 Name
识别每个元素的 NodeType
。如果页面显示“重定向”(标题拼写和含义合理,但实际文章名称不同),则跳过它。名为“title
”、“ns
”、“text
”的元素分别提供 Page
对象的 Title
、Namespace
和 Text
属性。此方法会跳过不是文章或类别命名空间的页面。如果需要,包含其他命名空间非常简单。
public void ExtractPagesAndHierarchy(
string pathPagesAndParents,
string pathCategoriesAndParent)
它完成了我们测试项目的全部工作,生成了“主程序”部分提到的三个文件。
首先,它使用上面的 GetNextPage()
读取 XML 摘要,并创建两个临时文件 tempPagesAndParents
和 tempBiographicalPages
。临时文件与最终文件几乎相同,只是它们没有引用计数列。在此过程中,我们收集每个页面指向其他页面的所有链接。
在第二阶段,当所有链接都收集完毕后,我们在 Dictionary<string, int> linksCount
中计算每个页面的引用数。
收集整个维基百科每个页面的 linksCount
所需的内存可能会超过标准的 2Gb 堆栈大小限制。这就是为什么在项目生成选项中取消选中“偏好 32 位”复选框。
当然,内存使用可以通过使用索引而不是字符串来优化,特别是。但是,我们希望保持代码尽可能简单,并希望结果易于查看。此外,花费大量时间进行此类优化可能不太合理,因为解析大型摘要只需要大约一个小时(或更少),并且应该只执行一次或几次。
在计算完 linksCount
字典后,我们读取临时文件,添加缺失列的值,并写入最终输出。工作完成了。
历史
- 2016 年 4 月 27 日:初始版本