如何把一个Scrapy项目改造成Scrapy-Redis增量式爬虫
前提: 安装Scrapy-Redis
- 1.原有的爬虫代码不用改动,启动方式和scrapy一样
- 2 在setting配置文件中添加如下配置
1. 增加了一个去重容器类的配置, 作用使用Redis的set集合来存储请求的指纹数据, 从而实现请求去重的持久化
DUPEFILTER_CLASS = “scrapy_redis.dupefilter.RFPDupeFilter”
2. 增加了调度的配置, 作用: 把请求对象存储到Redis数据, 从而实现请求的持久化.
SCHEDULER = “scrapy_redis.scheduler.Scheduler”
3. 配置调度器是否要持久化, 也就是当爬虫结束了, 要不要清空Redis中请求队列和去重指纹的set。如果是True, 就表示要持久化存储, 就不清空数据, 否则清空数据
SCHEDULER_PERSIST = True
4. redis_url配置
REDIS_URL = ‘reds://127.0.0.1:6379/2’
5. 如果需要把数据存储到Redis数据库中, 可以配置RedisPipeline
ITEM_PIPELINES = {
# 把爬虫爬取的数据存储到Redis数据库中
‘scrapy_redis.pipelines.RedisPipeline’: 400,
}
源码分析
# 调度配置,把请求对象存储到Redis数据, 从而实现请求的持久化
# TODO: add SCRAPY_JOB support.
class Scheduler(object):
def close(self, reason):
# 如果不持久化就调用flush
if not self.persist: # SCHEDULER_PERSIST = True
self.flush()
def flush(self):
# 清空指纹容器: 清空Redis中存储指纹的set集合
self.df.clear()
# 清空请求队列: 请求Redis中存储请求对象二进制数据的zset集合
self.queue.clear()
def enqueue_request(self, request):
"""把引擎交给调度器的请求, 存储到Redis数据库中"""
# 如果请求是过滤的 并且 该请求重复; 也就是在去重容器中已经有这个请求了
if not request.dont_filter and self.df.request_seen(request):
# 记录去重日志, 然后就结束了,return false表示没有入队
self.df.log(request, self.spider)
return False # return会终止return下边代码的执行
# 走到下一步的两个条件, 只要满足其中一个即可
# 1. 如果请求不过滤就直接进来了,入队
# 2. 如果请求需要去重, 但是它是一个全新请求
# 把请求对象入队
self.queue.push(request)
return True
def next_request(self):
"""redis的请求队列中取出一个请求对象: Request对象"""
# 默认是 0,默认取第一个
request = self.queue.pop(0)
return request
# 去重容器类配置,使用Redis的set集合来存储请求的指纹数据, 从而实现请求去重的持久化
# TODO: Rename class to RedisDupeFilter.
class RFPDupeFilter(BaseDupeFilter):
def request_seen(self, request):
# 根据Request对象, 生成一个请求的指纹字符串
fp = self.request_fingerprint(request)
# 向redis的用于存储请求指纹的set集合中添加该请求对应的指纹
# 如果添加成功了就返回一个大于0数, 添加失败了就返回0
added = self.server.sadd(self.key, fp)
# 如果added == 0就说明添加失败了, 也就是当set集合已经有这个数据了才会添加失败
# 也就是这个请求已经请求过了(即重复了)), 此时就返回True, 否则就返回False
return added == 0
def request_fingerprint(self, request):
return request_fingerprint(request)
## - 生成指纹的代码在scrapy.utils.request.py文件中
def request_fingerprint(request, include_headers=None):
"""
http://www.example.com/query?id=111&cat=222
http://www.example.com/query?cat=222&id=111
# 规范化处理: 起始对URL后的参数进行一个排序. 保证上面的URL是同一个.
"""
# 用于缓存请求指纹的
cache = {}
# 通过hashlib库获取一个sha1()算法的对象.
# sha1()是一个数字摘要的算法, 一般不同的数据通过sha1生成的数字摘要就不同的. 所以可以用来做去重.
# 好处: 节省存储空间.
fp = hashlib.sha1()
# 更加sha1算法中原始二进制数据
# 把请求的方法名添加sha1算法中
fp.update(to_bytes(request.method))
# 把请求的URL进行规范化处理后(就是对请求参数进行排序)添加到sha1算法中
fp.update(to_bytes(canonicalize_url(request.url)))
# 对POST请求的请求体数据,如果没有这个数据就是添加一个b'' 添加到sha1算法中
fp.update(request.body or b'')
# sha1算法会根据内部数据生成一个十六进制的指纹,赋值 cache[include_headers]
cache[include_headers] = fp.hexdigest()
# 返回了ha1算法会根据内部数据生成一个十六进制的指纹
return cache[include_headers]
# 把爬虫爬取的数据存储到Redis数据库中,以list形式
class RedisPipeline(object):
def process_item(self, item, spider):
# 把_process_item的任务分发到线程中完成.
return deferToThread(self._process_item, item, spider)
def _process_item(self, item, spider):
# 根据爬虫名称生成一个key,存储到Redis数据库中。
# 格式: 爬虫名称:items
key = self.item_key(item, spider)
# 把 item 序列化为 json格式的数据
data = self.serialize(item)
# 把数据存储到Redis队列中
self.server.rpush(key, data)
return item
def item_key(self, item, spider):
"""Returns redis key based on given spider.
"""
return self.key % {'spider': spider.name}