咱们在前面已经实现了Scrapy微博爬虫,虽然爬虫是异步加多线程的,可是咱们只能在一台主机上运行,因此爬取效率仍是有限的,分布式爬虫则是将多台主机组合起来,共同完成一个爬取任务,这将大大提升爬取的效率。
html
在了解分布式爬虫架构以前,首先回顾一下Scrapy的架构,以下图所示。数据库
Scrapy单机爬虫中有一个本地爬取队列Queue,这个队列是利用deque模块实现的。若是新的Request生成就会放到队列里面,随后Request被Scheduler调度。以后,Request交给Downloader执行爬取,简单的调度架构以下图所示。bash
若是两个Scheduler同时从队列里面取Request,每一个Scheduler都有其对应的Downloader,那么在带宽足够、正常爬取且不考虑队列存取压力的状况下,爬取效率会有什么变化?没错,爬取效率会翻倍。微信
这样,Scheduler能够扩展多个,Downloader也能够扩展多个。而爬取队列Queue必须始终为一个,也就是所谓的共享爬取队列。这样才能保证Scheduer从队列里调度某个Request以后,其余Scheduler不会重复调度此Request,就能够作到多个Schduler同步爬取。这就是分布式爬虫的基本雏形,简单调度架构以下图所示。网络
咱们须要作的就是在多台主机上同时运行爬虫任务协同爬取,而协同爬取的前提就是共享爬取队列。这样各台主机就不须要各自维护爬取队列,而是从共享爬取队列存取Request。可是各台主机仍是有各自的Scheduler和Downloader,因此调度和下载功能分别完成。若是不考虑队列存取性能消耗,爬取效率仍是会成倍提升。数据结构
那么这个队列用什么来维护?首先须要考虑的就是性能问题。咱们天然想到的是基于内存存储的Redis,它支持多种数据结构,例如列表(List)、集合(Set)、有序集合(Sorted Set)等,存取的操做也很是简单。多线程
Redis支持的这几种数据结构存储各有优势。架构
列表有lpush()
、lpop()
、rpush()
、rpop()
方法,咱们能够用它来实现先进先出式爬取队列,也能够实现先进后出栈式爬取队列。并发
集合的元素是无序的且不重复的,这样咱们能够很是方便地实现随机排序且不重复的爬取队列。异步
有序集合带有分数表示,而Scrapy的Request也有优先级的控制,咱们能够用它来实现带优先级调度的队列。
咱们须要根据具体爬虫的需求来灵活选择不一样的队列。
Scrapy有自动去重,它的去重使用了Python中的集合。这个集合记录了Scrapy中每一个Request的指纹,这个指纹实际上就是Request的散列值。咱们能够看看Scrapy的源代码,以下所示:
import hashlib
def request_fingerprint(request, include_headers=None):
if include_headers:
include_headers = tuple(to_bytes(h.lower())
for h in sorted(include_headers))
cache = _fingerprint_cache.setdefault(request, {})
if include_headers not in cache:
fp = hashlib.sha1()
fp.update(to_bytes(request.method))
fp.update(to_bytes(canonicalize_url(request.url)))
fp.update(request.body or b'')
if include_headers:
for hdr in include_headers:
if hdr in request.headers:
fp.update(hdr)
for v in request.headers.getlist(hdr):
fp.update(v)
cache[include_headers] = fp.hexdigest()
return cache[include_headers]复制代码
request_fingerprint()
就是计算Request指纹的方法,其方法内部使用的是hashlib的sha1()
方法。计算的字段包括Request的Method、URL、Body、Headers这几部份内容,这里只要有一点不一样,那么计算的结果就不一样。计算获得的结果是加密后的字符串,也就是指纹。每一个Request都有独有的指纹,指纹就是一个字符串,断定字符串是否重复比断定Request对象是否重复容易得多,因此指纹能够做为断定Request是否重复的依据。
那么咱们如何断定重复呢?Scrapy是这样实现的,以下所示:
def __init__(self):
self.fingerprints = set()
def request_seen(self, request):
fp = self.request_fingerprint(request)
if fp in self.fingerprints:
return True
self.fingerprints.add(fp)复制代码
在去重的类RFPDupeFilter
中,有一个request_seen()
方法,这个方法有一个参数request
,它的做用就是检测该Request对象是否重复。这个方法调用request_fingerprint()
获取该Request的指纹,检测这个指纹是否存在于fingerprints
变量中,而fingerprints
是一个集合,集合的元素都是不重复的。若是指纹存在,那么就返回True
,说明该Request是重复的,不然这个指纹加入到集合中。若是下次还有相同的Request传递过来,指纹也是相同的,那么这时指纹就已经存在于集合中,Request对象就会直接断定为重复。这样去重的目的就实现了。
Scrapy的去重过程就是,利用集合元素的不重复特性来实现Request的去重。
对于分布式爬虫来讲,咱们确定不能再用每一个爬虫各自的集合来去重了。由于这样仍是每一个主机单独维护本身的集合,不能作到共享。多台主机若是生成了相同的Request,只能各自去重,各个主机之间就没法作到去重了。
那么要实现去重,这个指纹集合也须要是共享的,Redis正好有集合的存储数据结构,咱们能够利用Redis的集合做为指纹集合,那么这样去重集合也是利用Redis共享的。每台主机新生成Request以后,把该Request的指纹与集合比对,若是指纹已经存在,说明该Request是重复的,不然将Request的指纹加入到这个集合中便可。利用一样的原理不一样的存储结构咱们也实现了分布式Reqeust的去重。
在Scrapy中,爬虫运行时的Request队列放在内存中。爬虫运行中断后,这个队列的空间就被释放,此队列就被销毁了。因此一旦爬虫运行中断,爬虫再次运行就至关于全新的爬取过程。
要作到中断后继续爬取,咱们能够将队列中的Request保存起来,下次爬取直接读取保存数据便可获取上次爬取的队列。咱们在Scrapy中指定一个爬取队列的存储路径便可,这个路径使用JOB_DIR
变量来标识,咱们能够用以下命令来实现:
scrapy crawl spider -s JOB_DIR=crawls/spider复制代码
更加详细的使用方法能够参见官方文档,连接为:https://doc.scrapy.org/en/latest/topics/jobs.html。
在Scrapy中,咱们实际是把爬取队列保存到本地,第二次爬取直接读取并恢复队列便可。那么在分布式架构中咱们还用担忧这个问题吗?不须要。由于爬取队列自己就是用数据库保存的,若是爬虫中断了,数据库中的Request依然是存在的,下次启动就会接着上次中断的地方继续爬取。
因此,当Redis的队列为空时,爬虫会从新爬取;当Redis的队列不为空时,爬虫便会接着上次中断之处继续爬取。
咱们接下来就须要在程序中实现这个架构了。首先实现一个共享的爬取队列,还要实现去重的功能。另外,重写一个Scheduer的实现,使之能够从共享的爬取队列存取Request。
幸运的是,已经有人实现了这些逻辑和架构,并发布成叫Scrapy-Redis的Python包。接下来,咱们看看Scrapy-Redis的源码实现,以及它的详细工做原理。
本资源首发于崔庆才的我的博客静觅: Python3网络爬虫开发实战教程 | 静觅
如想了解更多爬虫资讯,请关注个人我的微信公众号:进击的Coder
weixin.qq.com/r/5zsjOyvEZ… (二维码自动识别)