分布式爬虫的演习。
分布式爬虫问题其实也就是多台机器多个 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里面做文章就行了。进一步描述如下:
master 产生 starts_urls,url 会被封装成 request 放到 redis 中的 spider:requests,总的 scheduler 会从这里分配 request,当这里的 request 分配完后,会继续分配 start_urls 里的 url。
slave 从 master 的 redis 中取出待抓取的 request,下载完网页之后就把网页的内容发送回 master 的 redis,key 是 spider:items。scrapy 可以通过 settings 来让 spider 爬取结束之后不自动关闭,而是不断的去询问队列里有没有新的 url,如果有新的 url,那么继续获取 url 并进行爬取,所以这一过程将不断循环。
master 里的 reids 还有一个 key 是 “spider:dupefilter” 用来存储抓取过的 url 的 fingerprint(使用哈希函数将url运算后的结果),防止重复抓取,只要 redis 不清空,就可以进行断点续爬。
对于已有的 scrapy 程序,对其扩展成分布式程序还是比较容易的。总的来说就是以下几步:
- 找一台高性能服务器,用于 redis 队列的维护以及数据的存储。
- 扩展 scrapy 程序,让其通过服务器的 redis 来获取 start_urls,并改写 pipeline 里数据存储部分,把存储地址改为服务器地址。
- 在服务器上写一些生成url的脚本,并定期执行。
关于 scheduler 到底是怎么进行调度的,需要看源码进行分析。
源码分析
可能上面的描述还是不够清楚,干脆看一下源码吧,scrapy-redis 主要要一下几个文件。
零件分析
connection.py
根据 settings 里的配置实例化 redis 连接,被 dupefilter 和 scheduler 调用。dupefilter.py
对 request 进行去重,使用了 redis 的 set。queue.py
三种 queue, SpiderQueue(FIFO), SpiderPriorityQueue,以及 SpiderStack(LIFI)。默认使用的是第二种。pipelines.py
分布式处理,将 item 存储在 redis 中。scheduler.py
取代 scrapy 自带的 scheduler,实现分布式调度,数据结构来自 queue。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
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分析