原文:The Tale of Creating a Distributed Web Crawler
作者:Benoit Bernard
译者:roy

编者按:作者通过创建和扩展自己的分布式爬虫,介绍了一系列工具和架构, 包括分布式体系结构、扩展、爬虫礼仪、安全、调试工具、Python 中的多任务处理等。以下为译文:

大概600万条记录,每个记录有15个左右的字段。

这是我的数据分析项目要处理的数据集,但它的记录有一个很大的问题:许多字段缺失,很多字段要么格式不一致或者过时了。换句话说,我的数据集非常脏。

但对于我这个业余数据科学家来说还是有点希望的-至少对于缺失和过时的字段来说。大多数记录包含至少一个到外部网站的超链接,在那里我可能找到我需要的信息。因此,这看起来像一个完美的网络爬虫的用例。

在这篇文章中,你将了解我是如何构建和扩展分布式网络爬虫的,特别是我如何处理随之而来的技术挑战。

初始需求

创建网络爬虫的想法令人兴奋。因为,你知道,爬虫很酷,对吧?

但我很快意识到,我的要求比我想象的要复杂得多:

  1. 给定指定 URL,爬虫程序需要自动发现特定记录中缺失字段的值。因此,如果一个网页不包含我正在寻找的信息,爬虫程序需要跟踪出站链接,直到找到该信息。
  2. 它需要是某种爬虫和抓取的混合功能,因为它必须同时跟踪出站链接并从网页中提取特定信息。
  3. 整个程序需要分布式处理,因为有可能有数亿个URL需要访问。
  4. 抓取的数据需要存储在某处,很可能是在数据库中。
  5. 爬虫程序需要7*24小时不间断工作,所以不能在我的笔记本电脑上运行它。
  6. 我不希望在云服务上花费太多 1
  7. 需要用Python编码,这是我选择的语言。

好吧,我曾经在以前的工作中写过很多爬虫,但从没有这么大的规模。所以对我来说这是个全新的领域。

初始设计

我最开始的设计是这样的:

主要组件包括:

  1. 一个爬虫调度器,负责把URL分派给 m 个爬虫主控制器,并从它们收集结果(字段)。
  2. m个爬虫主控制器,负责管理 n 个子进程。这些子过程执行实际的爬取操作。为方便起见,我把他们称为爬虫。
  3. 一个数据库服务器,负责存储初始URL和提取的字段。

这样我最终会有
m*n个爬虫,从而将负载分布在许多节点上。例如,4个主控制器,每个包含8个子进程的话,就相当于32个爬虫。

另外,所有进程间通信都将使用队列。 所以在理论上,它将很容易扩展。 我可以添加更多的主控制器,爬网率 - 一个性能指标- 会相应增加。

初始实现

现在我有一个看起来不错的设计,我需要选择使用哪些技术。

但别误会我的意思:我的目标不是提出一个完美的技术栈。 相反,我主要把它看作是一个学习的机会,也是一个挑战 - 所以如果需要,我更愿意提出自制的解决方案。

1. 云托管

我可以选择AWS,但是我对DigitalOcean更熟悉,恰好它是更便宜的。 所以我用了几个5美元每月的虚拟机(很省钱啦)。

2. HTTP 库

requests库是Python里处理HTTP请求的不二选择。

3. ETL 管道

当然,我需要从每个访问过的网页中提取所有的超链接。但我也需要在一些页面抓取具体数据。

因此,我构建了自己的ETL管道,以便能够以我所需的数据格式提取数据并进行转换。

它可以通过配置文件进行定制,如下所示:

{"name": "gravatar","url_patterns": [{"type": "regex","pattern": "^https?:\\/\\/(?:(?:www|\\w{2})\\.)?gravatar\\.com\\/(?!avatar|support|site|connect)\\w+\\/?$"}],"url_parsers": [{"description": "URLs in the 'Find Me Online' section.","processors": [{"type": "xpath","parameters": {"expression": "//h3[contains(text(),'Find Me Online')]/following-sibling::ul[@class='list-details'][1]//a/@href"}}]},{"description": "URLs in the 'Websites' section.","processors": [{"type": "xpath","parameters": {"expression": "//ul[@class='list-sites']//a/@href"}}]}],"fields": [{"name": "name","processors": [{"type": "xpath","parameters": {"expression": "//div[@class='profile-description']/h2[@class='fn']/a/text()"}},{"type": "trim","parameters": {}}]},{"name": "location","processors": [{"type": "xpath","parameters": {"expression": "//div[@class='profile-description']/p[@class='location']/text()"}},{"type": "trim","parameters": {}}]}]
}

你在上面看到的是一个Gravatar 用户个人资料页面的映射。它告诉爬虫程序应该从这些页面中抓取什么数据以及如何抓取:

  1. url_patterns 定义了与当前页URL 进行试探性匹配的模式。如果有一个匹配,那么当前页面确实是Gravatar的用户配置文件。

  2. url_parsers 定义了能够在页面中抓取特定URL的解析器,比如那些指向用户的个人网站或社交媒体资料的URL。

  3. fields 字段定义了要从页面抓取的数据。在Gravatar的用户配置文件里,我想抓取用户的全名和位置信息。

url_parsersfields 都包含了一系列针对 web 页面 HTML 数据的处理器。它们执行转换(XPath,JSONPath,查找和替换,等等)以获取所需的确切数据,并转成我想要的格式。因此,数据在存储在其它地方之前被规范化,这是特别有用的,因为所有网站都是不同的,并且它们表示数据的方式各不相同。

手动创建所有这些映射花费了我很多时间,因为相关网站的列表非常长(数百个)。

4. 消息处理

最初,我想知道RabbitMQ是否适合。 但是我决定,我不想要单独的服务器来管理队列。 我想要的一切都要如闪电般快速而且要独立运行。

所以我用了ZeroMQ的push/pull队列,我把它们加到了queuelib的FifoDiskQueue上,以便将数据保存到磁盘,以防系统崩溃。 另外,使用push/pull队列可以确保使用轮转调度算法将URL分派给主控制器。

了解ZeroMQ如何工作和理解其几个极端案例花了我一段时间。 但是学习如何实现自己的消息传递真的很有趣,最终是值得的,尤其是性能方面。

5. 存储处理

一个好的关系数据库可以完成这项工作。 但是我需要存储类似对象的结果(字段),所以我选了MongoDB。

加分项:MongoDB相当容易使用和管理。

6. 日志记录和监控

我使用了 Python 的日志模块,加上一个 RotatingFileHandler,每个进程生成一个日志文件。这对于管理由每个主控制器管理的各个爬虫进程的日志文件特别有用。这也有助于调试。

为了监视各种节点,我没有使用任何花哨的工具或框架。我只是每隔几个小时使用 MongoChef连接到 MongoDB 服务器,按照我的计算, 检查已经处理好的记录的平均数。如果数字变小了,很可能意味着某件事情 (坏的) 正在发生,比如一个进程崩溃了或其他别的什么事情。

当然,你知道的-所有的血,汗水和眼泪都在这里。

7. 管理已经爬过的URLs

Web爬虫很可能会不止一次碰到同一个URL。但是你通常不想重新抓取它,因为网页可能没有改变。

为了避免这个问题,我在爬虫程序调度器上使用了一个本地SQLite数据库来存储每个已爬过的URL,以及与其抓取日期相对应的时间戳。因此,每当新的URL出现时,调度程序会在SQLite数据库中搜索该URL,以查看是否已经被爬过。如果没有,则执行爬取。否则,就忽略掉。

我选择SQLite是因为它的快速和易于使用。每个爬取URL附带的时间戳对调试和事件回溯都非常有用,万一有人对我的爬虫提出投诉的话。

8. URL过滤

我的目标不是抓取整个网络。相反,我想自动发现我感兴趣的网址,并过滤掉那些没用的网址。

利用前面介绍的ETL配置,我感兴趣的URL被列入白名单。为了过滤掉我不想要的网址,我使用Alexa的100万顶级网站列表中的前20K个网站。

这个概念很简单:任何出现在前20K的网站有很大的可能性是无用的,如youtube.com或amazon.com。然而,根据我自己的分析,那些20K以外的网站更有可能有与我的分析相关,比如个人网站和博客等。

9. 安全

我不希望任何人篡改我的 DigitalOcean 虚拟机,所以:

  1. 我关闭了每个虚拟机上使用 iptables的所有端口。我选择性地打开了我绝对需要的端口(80、443、22、27017等)。
  2. 我在 MongoDB 上启用了 SSL 身份验证,因此只有拥有适当证书的用户才能登录。
  3. 我在所有虚拟机上都使用了加密的磁盘。
  4. 我在每个虚拟机上都启用了fail2ban,以阻止多次失败的登录请求。
  5. 我在所有虚拟机上都配置了基于SSH密钥的身份验证。
  6. 我在 ZeroMQ 中启用了 SSL身份验证。

好吧,也许我对安全有点过分了:) 但我是故意的:这不仅是一个很好的学习机会,而且也是保护我数据的一种非常有效的方法。

10. 内存

一个每月5美元的DigitalOcean 虚拟机只有512MB的内存,所以它可做的相当有限。 经过多次测试运行,我确定我的所有节点都应该有1GB的内存。 所以我在每个虚拟机上创建了一个512MB的交换文件。

礼貌…是啥?

我对自己实现最初设计的工作速度感到惊讶。事情进展顺利,我的早期测试显示了我爬虫的令人印象深刻的性能数字(爬网率) 。所以我很兴奋,那是肯定的:)!

但后来,我看到Jim Mischel的一篇文章,完全改变了我的想法。事实是,我的爬虫根本不 “客气”。它不停地抓取网页,没有任何限制。当然,它抓取速度非常快,但由于同样的原因,网站管理员可能会封杀它。

那么,礼貌对网络爬虫意味着什么呢?

  1. 它必须通过适当的用户代理字符串标识自己。
  2. 它必须尊重 robots.txt 的规则。
  3. 它不能太快地向网站发送连续请求。

相当容易实现,对不对?

。我很快意识到,我爬虫的分布式特性使事情复杂了许多。

更新的要求

除了我已经实现的需求之外,我还需要:

  1. 创建一个页面描述我的爬虫在做什么。
  2. 在我的爬虫所做的每一个HTTP请求中传递User-Agent头,并包含一个指向我创建的说明页面的链接。
  3. 为每个域定期下载robots.txt,并根据以下条件检查是否允许抓取URL:
    1. 包含/排除规则。
    2. 抓取延迟指令。在不存在的情况下,对同一域的后续请求需要以保守的秒数(例如15秒)间隔开。这是为了确保爬虫不会在网站上造成额外的负载。

然而,第三点有些难度。实际上,分布式Web爬虫怎么能:

# 保持一个单一的,最新的robots.txt文件缓存,并与所有进程分享?
# 避免过于频繁地下载同一个域的robots.txt文件?
# 跟踪每个域上次爬网的时间,以尊重抓取延迟指令?

这意味着我的爬虫会有一些重大的变化。

更新的设计

这是我更新后的设计。

与以前设计的主要区别是:

  1. 将为每个域下载Robots.txt文件。
  2. Robots.txt 文件将被缓存在数据库中。每隔一小时左右,每个文件将根据需要单独失效 并根据域重新下载。 这样做是为了确保爬虫能够遵守robots.txt文件里的任何更改。
  3. 最后一个抓取日期也将被缓存到每个域的数据库中。这将用作参考,以遵守 robots.txt 中包含的抓取延迟指令。

此时,我担心这些变化会减慢我爬虫的速度。实际上几乎肯定会。但我没有选择,否则我的爬虫会使其它网站超负载。

更新后的实现

到目前为止,我所选择的一切都保持不变,除了几个关键的区别。

1. 处理 robots.txt

我选择了 reppy 库而不是 urllib 的 robotparser 是因为:

  1. 它支持抓取延迟(crawl-delay)指令。
  2. 它会自动处理已过期的robots.txt文件的下载。
  3. 它支持目录包含规则 (即允许指令),基于Google 自己的 robots.txt 的实现。 这些规则在网络上的robots.txt文件中很常见。

所以这是一个显而易见的选择。

2. 缓存 robots.txt 和上次爬网日期

我添加了第二个专门用于缓存内容的MongoDB服务器。在服务器上,我创建了两个不同的数据库,以避免任何可能的数据库级锁争用2:

# 数据库(1): 保存了每个域的上次爬网日期。
# 数据库(2): 保存了每个域的 robots.txt 文件副本。

此外,我不得不小小修改一下修改 reppy 库,使它缓存 robots.txt 文件在 MongoDB而不是在内存中。

处理 bug 和问题

在开发过程中,我花了大量的时间调试、分析和优化我的爬虫。 实际上比我预期的时间多了很多。

除了挂掉3,内存泄漏4,变慢5,崩溃6和各种其他错误,我遇到了一系列意想不到的问题。

1. 内存管理

内存不是无限的资源 - 特别是在每月5美元的 DigitalOcean 虚拟机上。

事实上,我不得不限制在内存中一次存放多少个Python对象。 例如,调度员非常快地将URL推送给主控制器,比后者爬取它们要快得多。 同时,主控制器通常有8个爬取进程可供使用,因此这些进程需要不断地提供新的URL来爬取。

因此,我设置了一个阈值,确定主控制器上可以在内存中一次处理多少个URL。 这使我能够在内存使用和性能之间取得平衡。

2. 瓶颈

我很快意识到,我不能让我的网络爬虫不受约束,否则它会抓取整个网络-这根本不是我的目标。

因此,我将爬取深度限制为 1,这意味着只会抓取指定网址及其直接的子网址。这样我的爬虫可以自动发现它要特别寻找的大部分网页。

3. 动态生成的内容

我发现很多网站都是用JavaScript动态生成的。这意味着当你使用爬虫下载任意网页时,你可能没有它的全部内容。也就是说,除非你能够解释和执行其脚本来生成页面的内容。要做到这一点,你需要一个JavaScript引擎。

现在有很多方法可以解决这个问题,但我还是选择了一个非常简单的解决方案。我指定了一些主控制器,让它们只抓取动态生成的网页。

在那些主控制器上:

  1. 我安装了谷歌浏览器和Chrome驱动程序。
  2. 我安装了Selenium的Python绑定。
  3. 我安装了xvfb来模拟监视器的存在,因为Chrome有一个GUI,而CentOS默认没有。

因此,我有几个节点能够抓取动态生成的网页。

4. 极端情况

我已经知道,构建一个常规爬虫意味着要处理各种奇怪的API极端案例。但是网络爬虫呢?

好吧,如果你把网络看成是一个API,它肯定是巨大的,疯狂的,非常不一致的:

  1. 页面并非都是以同样的方式构建的。
  2. 页面通常包含无效字符(即与页面编码不兼容)。
  3. 服务器经常返回各种HTTP错误(500,404,400等等),包括自定义的错误(999,有人能告诉我这是啥不?)。
  4. 服务器经常无法访问,导致超时。域名/网站可能不再存在,或者可能存在DNS问题,或者可能是负载过重,或者服务器可能配置不正确或者…你明白的:)
  5. 有些页面是巨大的,有几十兆字节或者更多7。这意味着,如果你一次下载完全,并将它们全部加载到内存中的话,你很可能会在某个时刻耗尽内存8
  6. 服务器有时返回不正确的HTML,或非HTML内容,如JSON、XML或其他内容。谁知道为什么?!
  7. 网页通常包含无效和不正确的URL。或你不想爬取的URL,比如像大的二进制文件(如PDF文件,视频,等等)。

以上只是网络爬虫需要处理的许多问题的一部分。

性能数据

使用网络爬虫,你通常会对爬取速度感兴趣,即每秒下载的网页数量。例如,每4个主控制器,每个使用8个子进程,我估计我的爬虫程序速率超过每秒40页。

但我更感兴趣的是,每小时我的原始数据集有多少记录得到正确的解析。因为,正如前面提到的,我爬虫的最初目的是通过抓取丢失的字段或刷新过时的字段来填充数据集中的空白。

因此,使用与上面相同的配置,每小时它能够解析大约2600条记录。当然,这是一个令人失望的数字,但仍然足够好了,因为大多数网页都是无用的,而且过滤掉了。

未来的改进

如果我不得不从头开始的话,有几件事情,我会采用不同的方式:

1. 消息传递

我可能会选择 RabbitMQ 或者 Redis, 而不是ZeroMQ, 主要是为了方便和易用性,即使他们比较慢。

2. 监控/日志

我可能会使用 New Relic 和 Loggly 工具来监控我虚拟机上的资源并集中处理所有节点生成的日志。

3. 设计

我可能会把处理 robots.txt 文件和上次爬取日期的缓存去中心话来提高总体爬取速度。这意味着,对于每个爬虫过程,将 MongoDB 服务器 #2 替换为在每个主控制器上的缓存。

下面是可能的体系结构:

总结:

  1. 在每个主控制器节点上,每个爬虫程序进程都将有自己的 robots.txt 文件 和上次爬取的日期缓存;这将替换集中式缓存 (MongoDB 服务器 #2)。
  2. 由于这个原因,调度员需要将每个 URL 发送到一个非常特定的主控制器节点。
  3. 当接收到一个新的要爬取的URL,一个主控制器节点需要发送到一个非常特定的爬虫。否则,不同主控制器下面的的多个爬虫进程可能同时抓取完全相同的网站。我的爬虫可能会被禁止,因为它没有遵循 robots.txt 的规则。

幸运的是,ZeroMQ 支持前缀匹配,因此我可以根据域名将 URL 路由到特定的主控制器节点。我已经写了一个主要基于 SQLite的持久化缓存。我肯定会重用它,以防止多个缓存占用太多的内存。

 最后的思考

在这篇文章中,我们已经看到了如何构建一个分布式 web 爬虫来填补脏数据集中的缺失数据。

起初,我并不期待这个项目变得如此庞大和复杂-大多数软件项目可能都这样。

但最终我确实得到了回报,因为我学到了大量的东西: 分布式体系结构、扩展、礼仪、安全、调试工具、Python 中的多任务处理、robots.txt文件 等等。

现在,有一个问题,我没有在我的文章里回答。哪一个数据集可以证明所有的工作都是正确的?这一切背后的原因是什么?

这是你在我以后的文章中会看到的!

后记: 请在下面的评论栏中留下你的问题和意见!

更新(2017/09/19): 这篇文章发表在Reddit。它也发表在Python Weekly,Pycoders Weekly 和Programming Digest。如果你有机会订阅他们,你不会失望的!谢谢大家的支持和反馈!

1: 我只花了35美元每月 (5美元/月/VM * 7 VMs = 35美元/月)。我曾想给文章取标题为“一个穷人关于创建一个分布式网络爬虫的的建议”。
2: 现在回想起来,有2个不同的MongoDB数据库可能是不必要的。这是因为在MongoDB 3 以上版本写锁是针对每个文件的,而不是针对每个数据库。这似乎与3之前版本相反,据MongoDB的文档和这个Stackoverflow答案。
3: 关于更多挂机的细节,请看这里和这里
4: 关于更多内存泄露的细节,请看这里和这里
5: 关于更多运行缓慢的细节,请看这里
6: 关于更多崩溃的细节,请看这里
7: 这就是你为什么要按块下载网页
8: 有些网页就是这样设计的。其他的输出一条错误信息或者看起来无限长的堆栈跟踪信息。无论哪种方式,它们都很大!

创建一个分布式网络爬虫的故事相关推荐

  1. python学习(三)scrapy爬虫框架(二)——创建一个scrapy爬虫

    在创建新的scrapy爬虫之前,我们需要先了解一下创建一个scrapy爬虫的基本步骤 第一步:确定要爬取的数据 以爬取豆瓣电影数据为例: 每部电影所要爬取的信息有: 片名:<头号玩家> 导 ...

  2. 安装scrapy模块,创建一个Scrapy爬虫项目,并运行

    创建一个Scrapy项目,首先需要所需要的模块 一丶安装scrapy框架所有模块 1.第一个安装的依赖库是lxml ,命令是: pip install lxml 2.第二个依赖库是pyOpenSSL, ...

  3. 在anaconda下创建我的第一个scrapy爬虫——爬取dmoz网站某一网址下的目录的链接名称以及链接地址...

    这里我用的python工具是anaconda. 1.首先创建一个scrapy工程: 打开anaconda promt命令行(注意这里不是使用cmd打开windows下的命令行),进入到需要创建工程的目 ...

  4. 设计 一个高性能爬虫系统

    资料来源 http://www.xuebuyuan.com/1296711.html 开源中国 http://my.oschina.net/eshijia/blog/136595 最近看了一篇来自纽约 ...

  5. 在Objective-C中创建一个抽象类

    我原来是一名Java程序员,现在使用Objective-C. 我想创建一个抽象类,但在Objective-C中似乎不可能. 这可能吗? 如果没有,我可以在Objective-C中获得一个抽象类的接近程 ...

  6. 使用Scrapy构建一个网络爬虫

    记得n年前项目需要一个灵活的爬虫工具,就组织了一个小团队用Java实现了一个爬虫框架,可以根据目标网站的结构.地址和需要的内容,做简单的配置开发,即可实现特定网站的爬虫功能.因为要考虑到各种特殊情形, ...

  7. 如何用python创建一个下载网站-用Python写一个简单的网页下载

    作为一个Python+爬虫的小白,今天要搞点爬虫的活儿,以前没做过,从零开始. win10的系统,python我的电脑上以前不记得什么时候安装过,安装的是3.6的版本,能用.代码工具,先使用VS co ...

  8. 使用HttpClient实现一个简单爬虫,抓取煎蛋妹子图

    第一篇文章,就从一个简单爬虫开始吧. 这只虫子的功能很简单,抓取到"煎蛋网xxoo"网页(http://jandan.net/ooxx/page-1537),解析出其中的妹子图,保 ...

  9. python爬虫:做一个界面爬虫小软件

    任务目标: 1.抓取不同类型的图片 2.编写一个GUI界面爬虫程序,打包成exe重新文件 3.遇到的难点 1.分析如何抓取不同类型的图片 首先打开网站,可以看到有如下6个类型的菜单 点击不同菜单,发现 ...

最新文章

  1. 2017 必备的八款最佳反勒索软件工具
  2. C语言代码规范(九)运算符优先级使用括号提高阅读性
  3. 计算机中的数(一):数在计算机中的表示
  4. 【转载】Eclipse vs IDEA快捷键对比大全(win系统)
  5. 苹果汽车自动驾驶又出新信号:正与多家激光雷达供应商谈判
  6. AngularJs编辑器
  7. Android开发入门教程--3.Activity入门指南
  8. matlab角度和弧度的互换_MATLAB弧度与角度转换 -
  9. RandomUtils 随机生成中文姓名,性别,Email,手机号,住址
  10. sin的傅里叶变换公式_正弦和余弦函数的傅里叶变换
  11. 三国志战略版鸿蒙梦魇,三国志战略版梦中弑臣厉害吗 梦中弑臣战法搭配
  12. 瞎扯:修仙文明VS科技文明发展潜力
  13. 2015年中央电大c语言,2015年电大 2015年电大小抄-中央电大2012春季c语言程序设计期末复习指导(本)资料.doc...
  14. 计算机操作员考试模拟在线考试,计算机操作员高级问答集考试卷模拟考_试题...
  15. 学习MySQL-第六章
  16. 墨竹:黄金如期狂泻还稳居此枢轴点?成败在此一举!
  17. 滴滴-----浅谈滴滴派单算法
  18. XenDesktop 5 .VS.View 4.5
  19. 从零开始tableau
  20. java爬虫 403_使用java爬虫获取网络资源403错误解决

热门文章

  1. 跨境电商的三个增长点:产品曝光 品类轮转 入自建站
  2. vmware“增强型虚拟键盘”键呈灰色不能修改状态
  3. 360极速浏览器取消默认迅雷下载的正确方法
  4. 程序员颈椎病康复秘籍,你值得拥有!
  5. 遥感原始图像计算机格式,卫星遥感影像数据是什么样格式的?
  6. php gd表情包,gd表情包
  7. 期货穿仓和爆仓有何区别?
  8. CMS 常见问题分析和解决方案
  9. python自动生成ppt_用Python自动化生成倒计时图片
  10. android编码技巧_我如何使用编码技巧使航空公司取代丢失的婴儿车