硬核!手把手教你Python爬遍人类最强百科!

整编-lehui

来源- https://towardsdatascience.com/wikipedia-data-science-working-with-the-worlds-largest-encyclopedia-c08efbac5f5c(作者:William Koehrsen)

 

维基百科(Wikipedia)是现代人类最伟大的创造之一。谁能想到在短短数年之内,匿名无偿的撰稿者们就能创造出现存最伟大的在线知识库?维基百科不仅仅是你写大学论文时找资料的港湾,同时也是大量数据科学项目丰富的数据来源,包括自然语言处理(Natural Language Processing)和监督学习(supervised machine learning)。

 

维基百科庞大的数据量使之成为世界最大的百科全书,然而,也因此让我们有点害怕去运用它。然而,当我们使用了正确的工具,数据量的大小就不再是问题啦。在本文中,我们将梳理遍我们如何使用代码爬取下载并解析所有的维基百科(以英语为例)。

 

本文将涵盖几个数据科学中重要的内容:

1. 找到并用代码从网络中爬取数据

2. 使用Python的库解析网页数据(HTML,XML,MediaWiki)

3. 利用多线程和多进程并行加速爬虫

4. 解决数据科学问题的基准方案

 

我开始这段项目的初衷是为了收集维基百科上的每一本书,但是很快我发现个中涉及的方法有更广泛的应用。本文中涉及到的技巧和随文的Jupyter notebook可以让你有效利用维基百科上的任意文章,并且可以延伸到其他的网络数据上。

 

本文涉及到的Python代码,你可以我们公众号对话框,回复关键词“wiki”获取。

 

利用代码爬取下载数据

任何数据科学项目中的第一步是获得你的DATA!当我们从访问维基百科的网页并获得结果时,我们很快会被反爬虫手段进行任务限速。基于这个原因,我们转而从Wikimediadump of all of Wikipedia上获得权限。

 

Wiki的英语版本地址在这里dumps.wikimedia.org/enwiki。我们通过下面的代码查看可以使用的数据库版本。

 

硬核!手把手教你Python爬遍人类最强百科!

 

上面这段代码使用了BeautifulSoup这个HTML解析库。由于HTML是网页的标准标记语言,这个库用来对付网页数据是非常有帮助的。

 

对于本次项目,我们使用了2018年9月1日的版本(有些版本不完整,所以要确保选择你需要的数据)。为了找到数据库中所有的文件,我们使用下面的代码。

 

硬核!手把手教你Python爬遍人类最强百科!

我们再一次使用BeautifulSoup这个库来解析网页并且找到文件。当然我们可以访问https://dumps.wikimedia.org/enwiki/20180901/寻找文件再下载,但那就太低效了。知道如何解析网页并且在程序中与网页交互是非常有用的技巧,学习一些网页爬取的技巧,你就可以获得更多新的数据源。

 

决定下载的内容

 

上面的代码找到了数据集中的所有文件。这些文件包括了几种下载的选择:当前版本全部的文章,当前版本的文章和讨论,当前版本文章和所有过去版本的修改和讨论。如果我们选择最后一种方式下载,那就有好几TB的数据了!针对这个项目,我们只选择第一种方式下载。这个页面(https://en.wikipedia.org/wiki/Wikipedia:Database_download#English-language_Wikipedia)将在你决定下载什么文件时提供帮助。

 

当前版本所有文章都是单独文件的形式。但是,如果我们一得到一个单独的文件,然后就解析,这种单个处理的方式非常低效。更好的方法是,下载分区的数据文件,每个分区文件都是这些文件的一个子集,然后通过并行的方式一次处理多个文件,这样可以极大的提升处理的速度。

 

这些分区文件是bz2-compressed XML格式的。每个分区文件大约有300-400MB的大小,压缩前的大小是15.4GB。我们不需要解压这些文件,因为如果你这么做了,所有文件的大小将有58GB!对于人类的总体知识来说,这个大小的数据倒是不算大。

 

硬核!手把手教你Python爬遍人类最强百科!

下载文件

 

要真正的下载这些文件,Keras有个非常好用的方法:get_file。这个方法可以从链接下载文件,然后存到磁盘上。

 

硬核!手把手教你Python爬遍人类最强百科!

 

这些文件保存在~/.keras/datasets/的目录下,这是Keras的默认保存路径。下载全部文件大约需要2小时多一些。(你也可以试着并行下载,但是当我试着一次发出多个请求的时候遇到了限速。)

 

解析数据

 

看上去我们要做的第一件事是解压文件!但是,需要获得文章中所有的数据其实并不需要解压。我们可以遍历文件,一次解压一份并逐行处理文件。当我们处理那些没有办法读进内存的大型数据集时,遍历文件是唯一的选择。

 

为了遍历所有的bz2压缩文件,我们可以使用bz2库。在测试过程中,我发现了一个更快的选择:利用subprocess中的bzcat方法。通常一个问题有多个方法可以解决,我们就可以通过比较这些方法来寻找最高效的方法。在Jupyter Notebook中使用%%time it就可以解决。

 

完整的细节可以参考notebook,但是标准的格式遍历解压一个文件如下:

 

硬核!手把手教你Python爬遍人类最强百科!

如果我们只是读取XML数据然后加到list里,就会得到下图样子的东西:

 

硬核!手把手教你Python爬遍人类最强百科!

 

上图是单个维基百科文章的XML格式。我们下载的文件中包含了百万多行这样的内容。如果我们想特意让这个任务变的复杂,我们可以使用正则表达式,字符串匹配等方法来找到每篇文章。由于这样的操作非常低效,我们使用为解析XML和维基百科样式的文章定制化的工具来解析。

 

解析方法

 

我们需要在两个层面上解析文本。

1. 从XML中提取文章的题目和内容

2. 从文章文本中提取相关的信息

 

幸运的是,这些操作在python中都有很好的方法来解决。

 

解析XML

 

为了解决第一个定位文章的问题,我们使用了SAX parser,SAX是The simple API for XML的缩写。BeautifulSoup也可以用来解析XML,但是这就需要将整个文件一次导入内存中,然后建立一个DOM。而另一方面,SAX一次只处理一行XML,这与我们的方法完美结合。

 

基本的想法是通过搜索XML然后提取特定tag之间的信息,例如,我们有下面的XML:

 

硬核!手把手教你Python爬遍人类最强百科!

 

上面的XML中是文章的题目和内容的文本。我们要选择<title>和</title>中间的内容。我们使用parser和ContentHandler来提取我们想要的内容。们将XML一行行传递给Parser,然后Content Handler让我们企图出相关的信息。

 

这里如果自己不动手试一试的话会有一些难以理解,但是基本的方法是Content Handler将寻找特定的起始标签,当它找到一个特定的标签,就将后续的字符放在缓存中,直到它遇到与起始标签对应的结束标签。结果是我们我们得到了一个字典,字典的键是标签,值是对应的内容。然后我们将这个字典传到另一个函数中解析出值。

 

SAX中需要我们实现的部分就是Content Handler了。下面是总体代码:

 

硬核!手把手教你Python爬遍人类最强百科!

 

以上的代码中,我们找的是title和text两个标签。每次Parser遇到其中之一的起始标签,他就会把结束标志前的内容传到缓冲区。直到这时,它会将缓冲中的内容存进字典——self._values。文章是有<page>标签分割的,所以当Content Handler遇到<page>标签后,会将self._values中的值蠢到文章的列表中去,self._pages。如果你感到有些疑惑,也许自己运行观察一下会有帮助。

 

我们使用下面的代码来查找文章。我们暂时只是将他们存到了handler._pages属性中,但是稍后我们会将文章传给另一个函数去解析。

 

硬核!手把手教你Python爬遍人类最强百科!

 

我们检查一下handler._pages,我们会发现一个列表,列表中的每个元素都是一个元组,分别是文章的题目和文章的内容文本。

 

现在,我们已经成功在XML中定位出了文章。离成功解析文件已经不远啦!接下来的步骤是将文章本身解析,并在其中找到我们需要的特定的页面和信息。这一次,我们也使用一个专门为这个任务诞生的工具。

 

解析维基百科文本

 

维基百科是在一个叫MediaWiki的软件上运行的。这就意味着每篇文章都遵循标准的格式,我们就可以利用代码获得他们。虽然文章的文本看上去就像一个字符串,由于格式的存在,文本有更多的信息。为了有效的获得这些信息,我们引入了强大的mwparserfromhell,一个专门用来解析MediaWiki内容的库。

 

如果我们直接将维基百科的文本转递给mwparserfromhell,我们会得到一个Wikicode对象,有很多方法可以解析其中的数据。例如,下面的代码创造了一个wikicode对象(有关KENZ FM)并提取了文章中的wikilinks()。这些链接是指向其他维基百科文章的链接。

 

硬核!手把手教你Python爬遍人类最强百科!

 

wikicode有很多可以使用的有用的方法,例如找到注释或者查找特定的关键词。如果你想要一个文本内容的干净版本,你可以调用:

 

硬核!手把手教你Python爬遍人类最强百科!

 

由于我的最终目标是找到所有关于书的文章,问题是有没有方法利用解析器将文本归类成特定的类别呢?幸运的是,我们可以使用MediaWiki模板来解决。

 

文章模板

模板指用来记录信息的标准格式。维基百科上有针对万事万物的模板,其中和我们的目的最相关的一种叫Infoboxes。这种模板记录了一篇文章的摘要信息。例如,战争与和平的Infobox如下:

 

硬核!手把手教你Python爬遍人类最强百科!

 

维基百科中每个类别的文章,例如电影,书,电台等等都有自己特殊的infobox。以书为例,infobox的模板名字就叫Inforbox book。可以说是非常贴心了。更贴心的是,wiki对象也有一个方法叫filter_templates() 让我们可以从文章中抽取特定的模板。因此,如果我们想要知道为什么这篇文章是关于一本书的,我们可以用book infobok来作为过滤器:

 

硬核!手把手教你Python爬遍人类最强百科!

 

那我们如何将mwparserfromhell和解析文章使用的SAX parser联合起来呢?我们可以修改Content Handler中的endElement方法,将包含文章题目和内容的值的字典传递到一个函数。这个函数可以搜索特定的模板。一旦函数找到了我们想要的文章,那就回传给handler。首先我们来看下更新后的endElement:

 

硬核!手把手教你Python爬遍人类最强百科!

 

现在,一旦解析器到了文章的末尾,我们将文章传到一个process_article的函数:

 

硬核!手把手教你Python爬遍人类最强百科!

 

虽然我的目的是为了书,但这个函数可以用来在维基百科上找任意种类的文章,返回的也都是关于这个类别的信息。

 

我们写一个新的Content Handler来测试这个函数:

 

硬核!手把手教你Python爬遍人类最强百科!

硬核!手把手教你Python爬遍人类最强百科!

 

我们来看看其中一本书的输出:

 

硬核!手把手教你Python爬遍人类最强百科!

硬核!手把手教你Python爬遍人类最强百科!

 

对于维基百科上的每一本书,我们都有来自infobox的信息字典,内部的wikilinks,外部的链接和最近一次更新的时间。(可以使用这些信息来建一个书籍的推荐系统)你可以通过更改process_article这个函数和WIkiXmlHandler类来找到你想要的信息和文章!

 

我们观察处理一个文件就花了1055秒,如果将这个数字乘55,要处理所有的文件需要15小时!当然,你可以连夜运行这个程序,但有更好的方法为什么要浪费时间呢?我们来看看本文最后一个技巧,多线程和多进程。

 

并行处理

 

一篇篇处理文章太慢了,我们想一次处理几篇文章(这就是为什么我们下载了分区文件的原因)。我们可以通过并行处理的技巧,也就是多进程和多线程。

 

多线程和多进程

 

多线程和多进程是同时在一台或多台计算机上运行多个任务的方法。磁盘上的文件都需要被相同的方式处理,然而这并没有将计算机的资源最大化的利用。如果使用多线程和多进程来同时处理文件,我们将显著提升运行速度。

 

总体而言,多线程对于有关输入输出任务表现更好,例如读入文章或发出请求;多进程在有关CPU的任务上表现更好。在处理文章的过程中,我也不确定哪个是最好的方法,所以我又一次比较了两个方法。

 

(测试多进程和多线程的代码在notebook的下方)当我进行测试的时候,我发现多进程要快10倍,这也表明了这应该是个受限于CPU的过程。

 

硬核!手把手教你Python爬遍人类最强百科!

进行了多次测试后,我发现最快处理文章的方法是使用16进程,每个进程都占用了我计算机的一个核心。这就意味着,我可以每次处理16篇文章而不是一篇。

 

设置并发执行的代码

 

为了将代码并行化,我们需要一个service和一系列tasks。service就是个函数,而tasks是可以循环执行的(比如列表),每个task都将被传到函数中。为了解析XML文件,每个task就是一个文件,而函数将去一个文件,然后找到所有的书然后将他们保存的磁盘上。这个函数的伪代码如下:

 

硬核!手把手教你Python爬遍人类最强百科!

 

运行这个函数返回的结果是书的列表。这些文件被保存为json。这些tasks就是所有的压缩的文件。

 

硬核!手把手教你Python爬遍人类最强百科!

 

对于每个文件,我们都会将它交给find_books这个函数去解析。

 

搜索所有的维基百科

 

下面是搜索维基百科上所有文章的最终代码:

 

硬核!手把手教你Python爬遍人类最强百科!

 

我们将每个task都映射到service,即用来找书的函数。在16个多进程的并发执行下,搜索全部的维基百科只需要3小时。最终每本书都单独以json的文件保存到磁盘上。

 

多线程读取和连接文件

 

实际应用中,我们用多线程这种并发执行的代码来读入单独的文件。multiprocessing.dummy库中提供了线程的模块。这次,service是read_data,而需要执行的task是将文件保存到磁盘上。

 

硬核!手把手教你Python爬遍人类最强百科!

 

多线程的代码也是一样的形式:将任务映射到一个可以循环执行的函数。一旦我们有了由列表组成的列表,我们将之转换成一个单独的列表:

 

硬核!手把手教你Python爬遍人类最强百科!

 

根据我们的统计,维基百科上有将近38000关于书的文章。最终的包含所有书的信息的json文件只有55MB。这就意味着我们搜索了近50GB的信息然后压缩成了55MB的信息。因为我们只找了关于书的信息,这也说的过去。

 

现在,我们就有了维基百科上每一本书的信息。你可以使用相同的代码去找任意类别的文章,或者修改函数来找不同的信息。用简单的Python我们就可以搜索到大量的信息。

 

结论

 

本文我们展示了如何下载并解析全部的英语维基百科。我们开发了一系列的方法来有效处理这大量的数据。

1. 使用代码找到并下载数据

2. 有效解析下载的数据

3. 并发运行代码,最大化利用我们的硬件

4. 设置测试,比较方法的有效性

 

在这个项目中用到的技巧不仅仅适用于维基百科,也可以广泛运用到不同的数据爬取任务上。希望大家在自己项目上运用这些方法,试着挖掘不同类别的文章。维基百科是人类创造的伟大的资源。现在我们利用代码获得并处理,知道了如何使用这个里程碑式的成就。我希望会在将来多做一些维基百科的数据科学项目。同时,这里展示的技巧也有更广泛的使用场景,所以多动手做做项目吧!