赞
踩
目标:处理批量zf网站 并自动抓取全站数据
解决方式:实现基于 RedisCrawlSpider 的通用分布式爬虫
技术选型思路:
抓取全站数据 ——> 选择scrapy框架的crawlspider爬虫;
提高抓取效率 ——> 选择基于redis的分布式,即RedisCrawlSpider;
自动处理每一个进来的网站 ——> 实现全网站通用爬虫。
本篇文章将介绍如何在CrawlSpider的基础上改造为RedisCrawlSpider分布式爬虫
以及给 RedisCrawlSpider改造为通用型爬虫的实现 提供一种思路。
网上相关的文章比较少,希望这篇文章可以给到大家一些帮助。
其实并不复杂,只需要在基于scrapy的CrawlSpider上,修改spider.py和settings.py这两个文件就可以了。
第一步,修改scrapy核心文件 spider.py;
# spider.py
# from scrapy.spiders import CrawlSpider
from scrapy_redis.spiders import RedisCrawlSpider
from scrapy.spiders import Rule
class SlaveSpider(RedisCrawlSpider):
name = 'slavespider'
allowed_domains = ['baidu.com']
# start_urls = ['https://www.baidu.com/']
redis_key = 'slavespider:start_urls'
# rules根据相关抓取网站来定
rules = (Rule(LinkExtractor(allow=r'.*baidu.com.*'),callback='parse_item',follow=False))
第二步,修改settings配置;
# settings.py
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
redis_host = 'localhost'
redis_port = 6379
redis_pwd = 'xxx'
redis_db = 0
REDIS_URL = 'redis://:{}@{}:{}/{}'.format(redis_pwd, redis_host, redis_port, redis_db)
ITEM_PIPELINES = {
'scrapy_redis.pipelines.RedisPipeline': 400,
}
我们可以新建一个 db.py 文件,代码如下:
# db.py
import redis
server = redis.StrictRedis(host='localhost', password='xxx', port=6379, db=0,
decode_responses=True)
server.lpush('slavespider:start_urls', 'https://www.baidu.com/')
run 运行该文件,将需要 start_urls 推进 redis 中的任务队列里,爬虫即可启动。
CrawlSpider有一个缺点
crawlspider没有提供start_requests入口,在redis取到任务后发出请求前,我们无法加入其它操作,最重要的是无法进行meta参数的构造与传递。
这个缺点是可以补足的,我们可以通过修改源码来让请求携带 meta,并让其能在请求中传递,但主要有以下两个难点:
① meta 如何从起始的请求
传递到 rules提取的新请求
depth = 1 --> depth = 2
② meta 如何从 rules提取的请求
传递到 下一次rules提取的请求
depth = 2 --> depth = n
解决方法是:
next_request
方法,使请求携带meta;_requests_to_follow
方法,使meta能够在起始请求的节点和其他从节点中进行传递。下面为相关源码展示,改写部分已用 [*] 注释标注:
next_request
方法# spider.py
def next_requests(self):
use_set = self.settings.getbool('REDIS_START_URLS_AS_SET', False)
fetch_one = self.server.spop if use_set else self.server.lpop
found = 0
while found < self.redis_batch_size:
data = fetch_one(self.redis_key)
if not data:
break
"""[*] 组装出需要传递的meta"""
meta = self.loads_data(data)
"""[*] 构造请求并携带上meta"""
req = self.make_request_from_data(meta)
if req:
yield req
found += 1
def loads_data(self, data):
# 根据业务需求自行组装
return data
def make_request_from_data(self, meta):
url = meta.get('url')
# 根据业务需求自行处理逻辑
return scrapy.Request(url=url, meta=meta, dont_filter=True)
_requests_to_follow
方法# spider.py
def _requests_to_follow(self, response):
"""[*] 取出响应的meta"""
meta = response.meta
if not isinstance(response, HtmlResponse):
return
seen = set()
for rule_index, rule in enumerate(self._rules):
links = [lnk for lnk in rule.link_extractor.extract_links(response)
if lnk not in seen]
for link in rule.process_links(links):
seen.add(link)
request = self._build_request(rule_index, link)
"""[*] 将新request的 规则 和 链接文本 赋值出来, 避免被旧的覆盖"""
meta['rule'], meta['link_text'] = request.meta['rule'], request.meta['link_text']
"""[*] 将meta传递给 rules 新提取出来的链接请求"""
request.meta.update(meta)
yield rule.process_request(request, response)
一定要将新生成请求中的meta里面的rule取出保留,不然继承了response的meta后,会导致旧reponse的rule对应的callback错误覆盖新请求的callback。
以上两步实现了meta请求的传递
起始请求
——> rules提取的新请求
rules提取的请求
——> 下一次rules提取的请求
实现通用爬虫的功能,需要根据不同的网站任务定制使用不同的rules。
这边提供一个思路。
我们可以先加载一个默认的通用的 rules 配置,并将 rules 保存到redis里。当我们发现默认的 rules 没办法匹配某个网站的链接时,通过redis我们可以达到不终止程序运行的情况下修改、定制和更新 rules 。
但是问题来了,rules 一般在crawlspider初始化的时候就赋值了,而我们只能在scrapy流程初始化稍后一点的next_request
方法里从redis中取出rules,即使赋值为self.rules
,程序依旧读取不到rules,因为已经过了初始化rules的流程。
我们依然可以通过重写对应的源码来解决该问题,还是需要重写上面提到的 crawl.py 文件中的 _requests_to_follow
方法,通过源码实现的self._compile_rules()
方法来让 rules 进行更新并再次重置一下。
_requests_to_follow
方法,新增self._compile_rules()
# spider.py
def _requests_to_follow(self, response):
# 取出响应的meta
meta = response.meta
if not isinstance(response, HtmlResponse):
return
seen = set()
"""[*] 重置self.rules"""
self._compile_rules()
for rule_index, rule in enumerate(self._rules):
links = [lnk for lnk in rule.link_extractor.extract_links(response)
if lnk not in seen]
for link in rule.process_links(links):
seen.add(link)
request = self._build_request(rule_index, link)
# 将新的request的 规则 和 链接文本 赋值出来, 避免错误覆盖
meta['rule'], meta['link_text'] = request.meta['rule'], request.meta['link_text']
# 将meta传递给提取出来的链接
request.meta.update(meta)
yield rule.process_request(request, response)
由于我们配置了 redis 作为分布式统一的任务队列,除了获取了起始请求任务的 spider 有初始化和绑定了自己的 self.rules
外,
我们还得考虑其他从节点的spider,它们拿到一个request后,是直接进行请求然后去触发prase_item
解析函数的,它们没有经过初始化 rules ,这时爬虫会报错,因为self.rules
是未定义的。
解决方法:
我们在前面构造self.loads_data
方法时,可以将self.rules
传到 meta 里面,
然后通过重写 crawl.py 文件中的 _callback
方法,
在方法通过 response.meta 里拿出 rules 并再一次赋值和重置该 spider 的self.rules
。
# spider.py
def _callback(self, response):
"""[*] 从上一个响应的meta获取rules,赋值为self.rules"""
self.rules = response.meta['rules']
"""[*] 重置self.rules"""
self._compile_rules()
rule = self._rules[response.meta['rule']]
return self._parse_response(response, rule.callback, rule.cb_kwargs, rule.follow)
当实现完上文提到的步骤后,
我们就初步完成了 RedisCrawlSpider 通用分布式爬虫,
其他组件以及流程逻辑可以根据大家不同的业务需求进行实现。
本文旨在为有相似需求的同学提供一种RedisCrawlSpider通用分布式爬虫的实现思路,其他一些细节还需各位自行实现。
总结一下,文章主要分享了以下几个知识点:
实现一个 RedisCrawlSpider 分布式爬虫
1.改写 spider.py
2.改写 settings.py
3.启动 RedisCrawlSpider
将 RedisCrawlSpider 改为通用爬虫
1.解决CrawlSpider的meta传递问题 (修改源码)
2.根据不同网站定制不同的rules (修改源码)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。