爬虫总结(四)-- 分布式爬虫

分布式爬虫的演习。

分布式爬虫问题其实也就是多台机器多个 spider 对 多个 url 的同时处理问题,怎样 schedule 这些 url,怎样汇总 spider 抓取的数据。最简单粗暴的方法就是将 url 进行分片,交给不同机器,最后对不同机器抓取的数据进行汇总。然而这样每个 spider 只能对自己处理的 url 去重,没办法全局的去重,另外性能也很难控制,可能有某台机器很早就跑完了,而别的机器还要跑很久。另一种思路就是把 url 存在某个地方,共享给所有的机器,总的调度器来分配请求,判断 spider 有没有闲置,闲置了就继续给它任务,直到所有的 url 都爬完,这种方法解决了去重问题(下面会具体讲到),也能提高性能,scrapy-redis 就实现了这样一个完整框架,总的来说,这更适合广度优先的爬取。

Scrapyd

Scrapy 并没有提供内置的分布式抓取功能,不过有很多方法可以帮你实现。

如果你有很多个spider,最简单的方式就是启动多个 Scrapyd 实例,然后将spider分布到各个机器上面。

如果你想多个机器运行同一个spider,可以将url分片后交给每个机器上面的spider。比如你把URL分成3份

http://somedomain.com/urls-to-crawl/spider1/part1.list
http://somedomain.com/urls-to-crawl/spider1/part2.list
http://somedomain.com/urls-to-crawl/spider1/part3.list

然后运行3个 Scrapyd 实例,分别启动它们,并传递part参数

curl http://scrapy1.mycompany.com:6800/schedule.json -d project=myproject -d spider=spider1 -d part=1
curl http://scrapy2.mycompany.com:6800/schedule.json -d project=myproject -d spider=spider1 -d part=2
curl http://scrapy3.mycompany.com:6800/schedule.json -d project=myproject -d spider=spider1 

Crawlera

这个,花钱就可以轻易解决~ 直达

Scrapy-redis

Redis 是高性能的 key-value 数据库。我们知道 MongoDB 将数据保存在了硬盘里,而 Redis 的神奇之处在于它将数据保存在了内存中,因此带来了更高的性能。

分布式原理

scrapy-redis实现分布式,其实从原理上来说很简单,这里为描述方便,我们把自己的核心服务器称为 master,而把用于跑爬虫程序的机器称为 slave。

回顾 scrapy 框架,我们首先给定一些start_urls,spider 最先访问 start_urls 里面的 url,再根据我们的 parse 函数,对里面的元素、或者是其他的二级、三级页面进行抓取。而要实现分布式,只需要在这个starts_urls里面做文章就行了。进一步描述如下:

  1. master 产生 starts_urls,url 会被封装成 request 放到 redis 中的 spider:requests,总的 scheduler 会从这里分配 request,当这里的 request 分配完后,会继续分配 start_urls 里的 url。

  2. slave 从 master 的 redis 中取出待抓取的 request,下载完网页之后就把网页的内容发送回 master 的 redis,key 是 spider:items。scrapy 可以通过 settings 来让 spider 爬取结束之后不自动关闭,而是不断的去询问队列里有没有新的 url,如果有新的 url,那么继续获取 url 并进行爬取,所以这一过程将不断循环。

  3. master 里的 reids 还有一个 key 是 “spider:dupefilter” 用来存储抓取过的 url 的 fingerprint(使用哈希函数将url运算后的结果),防止重复抓取,只要 redis 不清空,就可以进行断点续爬。

对于已有的 scrapy 程序,对其扩展成分布式程序还是比较容易的。总的来说就是以下几步:

  1. 找一台高性能服务器,用于 redis 队列的维护以及数据的存储。
  2. 扩展 scrapy 程序,让其通过服务器的 redis 来获取 start_urls,并改写 pipeline 里数据存储部分,把存储地址改为服务器地址。
  3. 在服务器上写一些生成url的脚本,并定期执行。

关于 scheduler 到底是怎么进行调度的,需要看源码进行分析。

源码分析

可能上面的描述还是不够清楚,干脆看一下源码吧,scrapy-redis 主要要一下几个文件。

零件分析

  1. connection.py
    根据 settings 里的配置实例化 redis 连接,被 dupefilter 和 scheduler 调用。

  2. dupefilter.py
    对 request 进行去重,使用了 redis 的 set。

  3. queue.py
    三种 queue, SpiderQueue(FIFO), SpiderPriorityQueue,以及 SpiderStack(LIFI)。默认使用的是第二种。

  4. pipelines.py
    分布式处理,将 item 存储在 redis 中。

  5. scheduler.py
    取代 scrapy 自带的 scheduler,实现分布式调度,数据结构来自 queue。

  6. spider.py
    定义 RedisSpider.py, 继承了 RedisMixin 和 CrawlSpider。

由上可知,scrapy-redis 实现的 爬虫分布式item处理分布式 就是由模块 scheduler 和模块 pipelines 实现。上述其它模块作为为二者辅助的功能模块。

调度过程

初始化

spider 被初始化时,同时会初始化一个对应的 scheduler 对象,这个调度器对象通过读取 settings,配置好自己的调度容器 queue 和判重工具dupefilter。

判重 & 进入调度池

每当一个 spider 产出一个 request 的时候,scrapy 内核会把这个 request 递交给这个 spider 对应的 scheduler 对象进行调度,scheduler 对象通过访问 redis 对 request 进行判重,如果不重复就把他添加进 redis 中的调度池。

调度

当调度条件满足时,scheduler 对象就从 redis 的调度池中取出一个 request 发送给spider,让 spider 爬取,若爬取过程中返回更多的url,那么继续进行直至所有的 request 完成。在这个过程中通过 connect signals.spider_idle 信号对 crawler 状态的监视,scheduler 对象发现 这个 spider 爬取了所有暂时可用 url,对应的 redis 的调度池空了,于是触发信号 spider_idle,spider收到这个信号之后,直接连接 redis 读取 strart_url池,拿去新的一批 url,返回新的 make_requests_from_url(url) 给引擎,进而交给调度器调度。

熟悉了原理其实可以自己来写 scheduler,自己定义调度优先级和顺序,👇

Redis 配置

下载 Redis

wget http://download.redis.io/releases/redis-3.2.1.tar.gz

下载 scrapy-redis

pip install scrapy-redis

安装 Redis

make
make test

修改配置文件

安装完成后,redis 默认是不能被远程连接的,此时要修改配置文件 redis.conf,修改后,重启 redis 服务器

#bind 127.0.0.1
bind 0.0.0.0

任意目录下运行

sudo cp redis.conf /etc/

可能错误

如果因为 gcc 而不能 make

sudo apt-get build-dep gcc

如果遇到这个,

make[1]: Entering directory `/opt/redis-2.6.14/src'
CC adlist.o
In file included from adlist.c:34:
zmalloc.h:50:31: error: jemalloc/jemalloc.h: No such file or directory
zmalloc.h:55:2: error: #error "Newer version of jemalloc required"
make[1]: *** [adlist.o] Error 1
make[1]: Leaving directory `/opt/redis-2.6.14/src'
make: *** [all] Error 2

可以看这里

用这个命令

make MALLOC=libc

如果遇到这个

You need tcl 8.5 or newer in order to run the Redis test

安装 tcl

sudo apt-get install tcl

(redis 更多安装配置)[https://testerhome.com/topics/3887]

Redis 常用命令

运行 Redis

redis-server redis.conf

进入命令行模式

redis-cli

清空缓存

flushdb

查看所有 key

127.0.0.1:6379> keys *
1) "dmoz:items"
2) "dmoz:requests"
3) "dmoz:dupefilter"

查看 list (item)

127.0.0.1:6379> LRANGE dmoz:items 0 3
1) "{\"spider\": \"dmoz\", \"crawled\": \"2016-07-12 11:18:35\", \"link\": \"http://feeds.abcnews.com/abcnews/topstories\", \"name\": \"ABC News: Top Stories \", \"description\": \"Collection of news headlines.\"}"
2) "{\"spider\": \"dmoz\", \"crawled\": \"2016-07-12 11:18:35\", \"link\": \"http://abcnews.go.com/\", \"name\": \"ABCNews.com \", \"description\": \"Includes American and world news headlines, articles, chatrooms, message boards, news alerts, video and audio webcasts, shopping, and wireless news service. As well as ABC television show information and content.\"}"
3) "{\"spider\": \"dmoz\", \"crawled\": \"2016-07-12 11:18:35\", \"link\": \"http://www.alarabiya.net/\", \"name\": \"Al Arabiya News Channel \", \"description\": \"Arabic-language news network. Breaking news and features along with videos, photo galleries and In-Focus sections on major news topics.  (Arabic, English, Persian, Urdu)\"}"
4) "{\"spider\": \"dmoz\", \"crawled\": \"2016-07-12 11:18:35\", \"link\": \"http://www.aljazeera.com/\", \"name\": \"Aljazeera \", \"description\": \"English version of the Arabic-language news network. Breaking news and features plus background material including profiles and global reactions.\"}"

查看 set (dupefilter)

127.0.0.1:6379> SMEMBERS dmoz:dupefilter
   1) "28bf6cfa1409d6d2ad2852663a3751ae077a0b01"
   2) "6af16713d5d423a2e91c87085f277a810c690cfa"
   3) "c0ccfd767892b2bbb533a52c7cde55543aa4605b"
   4) "0ca88e614179c791f258d89a820449c91940c4d4"
   5) "546577e3457c55057c56985b71e6a142fe5a64e9"
   6) "d2af0f8cf72e394dc46a720ee620fd7cdb0b6ad6"
   7) "e0c1ab903b2a95f05bc8f5a5036b2f6f0b3fcbd0"
   8) "bf1290602aa0fd2deb7f8b582f855535ca151990"
   9) "c59f100b08e424352e6e368ff94d797c35fc5a4b"
  10) "5acf897c445b3dbba5b371f811b74e26c52cd5c6"
  

查看 sorted set (requests)

127.0.0.1:6379> ZRANGE dmoz:requests 0 3
1) "\x80\x02}q\x01(U\x04bodyq\x02U\x00U\t_encodingq\x03U\x05utf-8q\x04U\acookiesq\x05}q\x06U\x04metaq\a}q\b(U\x05depthq\tK\x01U\tlink_textq\nclxml.etree\n_ElementStringResult\nq\x0bU\tInvestingq\x0c\x85\x81q\r}q\x0e(U\a_parentq\x0fNU\x0cis_attributeq\x10\x89U\battrnameq\x11NU\ais_textq\x12\x89U\ais_tailq\x13\x89ubU\x04ruleq\x14K\x00uU\aheadersq\x15}q\x16U\aRefererq\x17]q\x18U\x14http://www.dmoz.org/q\x19asU\x03urlq\x1aX'\x00\x00\x00http://www.dmoz.org/Business/Investing/U\x0bdont_filterq\x1b\x89U\bpriorityq\x1cK\x00U\bcallbackq\x1dU\x14_response_downloadedq\x1eU\x06methodq\x1fU\x03GETq U\aerrbackq!Nu."
2) "\x80\x02}q\x01(U\x04bodyq\x02U\x00U\t_encodingq\x03U\x05utf-8q\x04U\acookiesq\x05}q\x06U\x04metaq\a}q\b(U\x05depthq\tK\x01U\tlink_textq\nclxml.etree\n_ElementStringResult\nq\x0bU\tLibrariesq\x0c\x85\x81q\r}q\x0e(U\a_parentq\x0fNU\x0cis_attributeq\x10\x89U\battrnameq\x11NU\ais_textq\x12\x89U\ais_tailq\x13\x89ubU\x04ruleq\x14K\x00uU\aheadersq\x15}q\x16U\aRefererq\x17]q\x18U\x14http://www.dmoz.org/q\x19asU\x03urlq\x1aX(\x00\x00\x00http://www.dmoz.org/Reference/Libraries/U\x0bdont_filterq\x1b\x89U\bpriorityq\x1cK\x00U\bcallbackq\x1dU\x14_response_downloadedq\x1eU\x06methodq\x1fU\x03GETq U\aerrbackq!Nu."
3) "\x80\x02}q\x01(U\x04bodyq\x02U\x00U\t_encodingq\x03U\x05utf-8q\x04U\acookiesq\x05}q\x06U\x04metaq\a}q\b(U\x05depthq\tK\x01U\tlink_textq\nclxml.etree\n_ElementStringResult\nq\x0bU\tTeen Lifeq\x0c\x85\x81q\r}q\x0e(U\a_parentq\x0fNU\x0cis_attributeq\x10\x89U\battrnameq\x11NU\ais_textq\x12\x89U\ais_tailq\x13\x89ubU\x04ruleq\x14K\x00uU\aheadersq\x15}q\x16U\aRefererq\x17]q\x18U\x14http://www.dmoz.org/q\x19asU\x03urlq\x1aX-\x00\x00\x00http://www.dmoz.org/Kids_and_Teens/Teen_Life/U\x0bdont_filterq\x1b\x89U\bpriorityq\x1cK\x00U\bcallbackq\x1dU\x14_response_downloadedq\x1eU\x06methodq\x1fU\x03GETq U\aerrbackq!Nu."
4) "\x80\x02}q\x01(U\x04bodyq\x02U\x00U\t_encodingq\x03U\x05utf-8q\x04U\acookiesq\x05}q\x06U\x04metaq\a}q\b(U\x05depthq\tK\x01U\tlink_textq\nclxml.etree\n_ElementStringResult\nq\x0bU\nBasketballq\x0c\x85\x81q\r}q\x0e(U\a_parentq\x0fNU\x0cis_attributeq\x10\x89U\battrnameq\x11NU\ais_textq\x12\x89U\ais_tailq\x13\x89ubU\x04ruleq\x14K\x00uU\aheadersq\x15}q\x16U\aRefererq\x17]q\x18U\x14http://www.dmoz.org/q\x19asU\x03urlq\x1aX&\x00\x00\x00http://www.dmoz.org/Sports/Basketball/U\x0bdont_filterq\x1b\x89U\bpriorityq\x1cK\x00U\bcallbackq\x1dU\x14_response_downloadedq\x1eU\x06methodq\x1fU\x03GETq U\aerrbackq!Nu."

查看 list (items) 长度

127.0.0.1:6379> LLEN Search:items
(integer) 376

查看 sorted set (requests) 长度

127.0.0.1:6379> ZCARD Search:requests
(integer) 1

查看 set (dupefilter) 长度

127.0.0.1:6379> SCARD Search:dupefilter
(integer) 1

Redis 教程

scrapy_redis 配置

从 github 上 下载 example,修改相应文件,items.py, settings.py, process_items.py 等。最重要的是改 settings.py

通用配置

SCHEDULER = "scrapy_redis.scheduler.Scheduler"
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
SCHEDULER_QUEUE_CLASS = "scrapy_redis.queue.SpiderPriorityQueue"
SCHEDULER_PERSIST = True

# ITEM_PIPELINES
ITEM_PIPELINES = {
    'scrapy_redis.pipelines.RedisPipeline': 400,
}

master 配置

settings.py 中添加

# redis
REDIS_HOST = '127.0.0.1'
REDIS_PORT = 6379

slave 配置

settings.py 中添加

# redis
REDIS_URL = 'redis://host_ip:6379'

spider 改写

导入模块

from scrapy_redis.spiders import RedisSpider

继承 RedisSpider,并从 Redis 读取 url

class Search(RedisCrawlSpider):
    name = "Search"
    redis_key = 'Search:start_urls'

运行爬虫

在 master 上启动 Redis

redis-server

启动 spider,任意顺序

scrapy crawl Search

可以看到 schedule 了多少 request

$ scrapy crawl Search
... [Search] DEBUG: Resuming crawl (8712 requests scheduled)

导出数据

写到数据库里很简单,在 process_items.py 里添加代码,指定数据库 ip,插入同一个数据库。这里我们的数据不用导出到 mongodb 等数据库,只用把它转化为文本文件即可。

redis-dump

安装 redis-dump

gem install redis-dump

导出

redis-dump -u 127.0.0.1:6379 > db.json

注意的是,它导出的是数据库里所有的 key-value,也就是说之后处理 items 的时候可能会有问题,item list 太大读取造成 memory error。

python 连接 redis

很简单,导入模块,连接数据库,其他基本按照 redis 命令来。

import redis
r = redis.Redis(host='106.75.136.128', port=6379)

for i in range(0, r.llen('Search:items'), 100):
    items = r.lrange('Search:items', start=0, end=100)

然后把文件写到文件里。

监控

分布式系统还有一个问题,怎么监控 slave,知道哪台机器坏了,可以写个 socket 向 master 报告,或者用 email 告警。

其他

每次执行重新爬取,应该将redis中存储的数据清空,否则影响爬取现象。

另外,request 和 url 是不同的,前者是由后者经由函数make_request_from_url实现,并且这个过程由spider完成。spider会返回(return、yield)request给scrapy引擎进而交割调度器。url也是在spider中定义或由spider获取的。

参考链接:
使用scrapy,redis,mongodb实现的一个分布式网络爬虫
scrapy-redis实现爬虫分布式爬取分析与实现
定向爬虫:Scrapy 与 Redis 入门
Scrapy笔记(7)- 内置服务
基于Redis的三种分布式爬虫策略
基于Python,scrapy,redis的分布式爬虫实现框架
scrapy-redis源码分析
Scrapy Redis源码 spider分析

徐阿衡 wechat
欢迎关注:徐阿衡的微信公众号
客官,打个赏呗~