Scrapy-redis 源码分析 及 框架使用

 

From:https://blog.csdn.net/weixin_37947156/article/details/75044971
From:https://cuiqingcai.com/6058.html

Scrapy-redis github:https://github.com/rmax/scrapy-redis
scrapy-redis分布式爬虫框架详解:https://segmentfault.com/a/1190000014333162?utm_source=channel-hottest
集群版 Scrapy-Redis:https://github.com/thsheep/scrapy_redis_cluster
scrapy-redis 和 scrapy 有什么区别?:https://www.zhihu.com/question/32302268
scrapy-redis使用以及剖析:https://www.cnblogs.com/wangyongsong/p/7485852.html
scrapy-redis 解析:https://www.cnblogs.com/zy0517/articles/9109681.html
基于 Scrapy-redis 的分布式爬虫设计:https://www.jianshu.com/p/cd4054bbc757/
小白进阶之Scrapy第三篇(基于Scrapy-Redis的分布式以及cookies池):https://cuiqingcai.com/4048.html
Scrapy+redis实现分布式爬虫简易教程:https://www.jianshu.com/p/ed5afa658ccb?from=jiantop.com

 

scrapy 是 python 的一个非常好用的爬虫库,功能非常强大,如果是小站的话,我们使用 scrapy 本身就可以满足。但是当我们要爬取的页面非常多的时候,面对一些比较大型的站点的时候,单个 scrapy 就显得力不从心了。单个主机的处理能力就不能满足我们的需求了(无论是处理速度还是网络请求的并发数)。

这时候分布式爬虫的优势就显现出来,人多力量大。很遗憾 Scrapy 官方并不支持多个同时采集一个站点,虽然官方给出一个方法:**将一个站点的分割成几部分 交给不同的scrapy去采集**。似乎是个解决办法,但是很麻烦诶!毕竟分割很麻烦的哇

下面就该 Scrapy-Redis 登场了。scrapy-redis 就是结合了分布式数据库 redis,重写了 scrapy 一些比较关键的代码,将 scrapy 变成一个可以在多个主机上同时运行的分布式爬虫。 

scrapy-redis 是 github 上的一个开源项目,可以直接下载到他的源代码: https://github.com/rmax/scrapy-redis

scrapy-redis 的官方文档写的比较简洁,没有提及其运行原理,所以如果想全面的理解分布式爬虫的运行原理,还是得看 scrapy的源代码才行(还得先理解 scrapy 的运行原理,不然看 scrapy-redis 还是比较费劲)。

 

 

 

来看一看 Scrapy 的架构图

 

这张图大家相信大家都很熟悉了。重点看一下SCHEDULER

 

1. 先来看看官方对于SCHEDULER的定义:

 

**SCHEDULER接受来自Engine的Requests,并将它们放入队列(可以按顺序优先级),以便在之后将其提供给Engine**

官方文档:https://doc.scrapy.org/en/latest/topics/architecture.html#component-scheduler

 

2. 现在我们来看看SCHEDULER都提供了些什么功能:

 

根据官方文档说明 在我们没有没有指定 SCHEDULER 参数时,默认使用:'scrapy.core.scheduler.Scheduler' 作为SCHEDULER(调度器)

scrapy.core.scheduler.py:

class Scheduler(object):def __init__(self, dupefilter, jobdir=None, dqclass=None, mqclass=None,logunser=False, stats=None, pqclass=None):self.df = dupefilterself.dqdir = self._dqdir(jobdir)self.pqclass = pqclassself.dqclass = dqclassself.mqclass = mqclassself.logunser = logunserself.stats = stats@classmethoddef from_crawler(cls, crawler):'''注意在 scrapy 中优先注意这个方法,此方法是一个钩子 用于访问当前爬虫的配置'''settings = crawler.settings# 获取去重用的类 默认:scrapy.dupefilters.RFPDupeFilterdupefilter_cls = load_object(settings['DUPEFILTER_CLASS'])# 对去重类进行配置from_settings 在 scrapy.dupefilters.RFPDupeFilter 43行# 这种调用方式对于IDE跳转不是很好  所以需要自己去找# @classmethod# def from_settings(cls, settings):#     debug = settings.getbool('DUPEFILTER_DEBUG')#     return cls(job_dir(settings), debug)# 上面就是from_settings方法 其实就是设置工作目录 和是否开启debugdupefilter = dupefilter_cls.from_settings(settings)# 获取优先级队列 类对象 默认:queuelib.pqueue.PriorityQueuepqclass = load_object(settings['SCHEDULER_PRIORITY_QUEUE'])# 获取磁盘队列 类对象(SCHEDULER使用磁盘存储 重启不会丢失)dqclass = load_object(settings['SCHEDULER_DISK_QUEUE'])# 获取内存队列 类对象(SCHEDULER使用内存存储 重启会丢失)mqclass = load_object(settings['SCHEDULER_MEMORY_QUEUE'])# 是否开启debuglogunser = settings.getbool('LOG_UNSERIALIZABLE_REQUESTS', settings.getbool('SCHEDULER_DEBUG'))# 将这些参数传递给 __init__方法return cls(dupefilter, jobdir=job_dir(settings), logunser=logunser,stats=crawler.stats, pqclass=pqclass, dqclass=dqclass, mqclass=mqclass)def has_pending_requests(self):"""检查是否有没处理的请求"""return len(self) > 0def open(self, spider):"""Engine创建完毕之后会调用这个方法"""self.spider = spider# 创建一个有优先级的内存队列 实例化对象# self.pqclass 默认是:queuelib.pqueue.PriorityQueue# self._newmq 会返回一个内存队列的 实例化对象 在110  111 行self.mqs = self.pqclass(self._newmq)# 如果self.dqdir 有设置 就创建一个磁盘队列 否则self.dqs 为空self.dqs = self._dq() if self.dqdir else None# 获得一个去重实例对象 open 方法是从BaseDupeFilter继承的# 现在我们可以用self.df来去重啦return self.df.open()def close(self, reason):"""当然Engine关闭时"""# 如果有磁盘队列 则对其进行dump后保存到active.json文件中if self.dqs:prios = self.dqs.close()with open(join(self.dqdir, 'active.json'), 'w') as f:json.dump(prios, f)# 然后关闭去重return self.df.close(reason)def enqueue_request(self, request):"""添加一个Requests进调度队列"""# self.df.request_seen是检查这个Request是否已经请求过了 如果有会返回Trueif not request.dont_filter and self.df.request_seen(request):# 如果Request的dont_filter属性没有设置(默认为False)和 已经存在则去重# 不push进队列self.df.log(request, self.spider)return False# 先尝试将Request push进磁盘队列dqok = self._dqpush(request)if dqok:# 如果成功 则在记录一次状态self.stats.inc_value('scheduler/enqueued/disk', spider=self.spider)else:# 不能添加进磁盘队列则会添加进内存队列self._mqpush(request)self.stats.inc_value('scheduler/enqueued/memory', spider=self.spider)self.stats.inc_value('scheduler/enqueued', spider=self.spider)return Truedef next_request(self):"""从队列中获取一个Request"""# 优先从内存队列中获取request = self.mqs.pop()if request:self.stats.inc_value('scheduler/dequeued/memory', spider=self.spider)else:# 不能获取的时候从磁盘队列队里获取request = self._dqpop()if request:self.stats.inc_value('scheduler/dequeued/disk', spider=self.spider)if request:self.stats.inc_value('scheduler/dequeued', spider=self.spider)# 将获取的到Request返回给Enginereturn requestdef __len__(self):return len(self.dqs) + len(self.mqs) if self.dqs else len(self.mqs)def _dqpush(self, request):if self.dqs is None:returntry:reqd = request_to_dict(request, self.spider)self.dqs.push(reqd, -request.priority)except ValueError as e:  # non serializable requestif self.logunser:msg = ("Unable to serialize request: %(request)s - reason:"" %(reason)s - no more unserializable requests will be"" logged (stats being collected)")logger.warning(msg, {'request': request, 'reason': e},exc_info=True, extra={'spider': self.spider})self.logunser = Falseself.stats.inc_value('scheduler/unserializable',spider=self.spider)returnelse:return Truedef _mqpush(self, request):self.mqs.push(request, -request.priority)def _dqpop(self):if self.dqs:d = self.dqs.pop()if d:return request_from_dict(d, self.spider)def _newmq(self, priority):return self.mqclass()def _newdq(self, priority):return self.dqclass(join(self.dqdir, 'p%s' % priority))def _dq(self):activef = join(self.dqdir, 'active.json')if exists(activef):with open(activef) as f:prios = json.load(f)else:prios = ()q = self.pqclass(self._newdq, startprios=prios)if q:logger.info("Resuming crawl (%(queuesize)d requests scheduled)",{'queuesize': len(q)}, extra={'spider': self.spider})return qdef _dqdir(self, jobdir):if jobdir:dqdir = join(jobdir, 'requests.queue')if not exists(dqdir):os.makedirs(dqdir)return dqdir

从上面的代码可以很清楚的知道 SCHEDULER 主要是完成了 push Requestpop Request 去重 的操作。而且 queue 操作是在内存队列中完成的。大家看 queuelib.queue 就会发现是基于内存的(deque)。

那么去重呢?

class RFPDupeFilter(BaseDupeFilter):"""Request Fingerprint duplicates filter"""def __init__(self, path=None, debug=False):self.file = Noneself.fingerprints = set()self.logdupes = Trueself.debug = debugself.logger = logging.getLogger(__name__)if path:# 此处可以看到去重其实打开了一个名叫 requests.seen的文件# 如果是使用的磁盘的话self.file = open(os.path.join(path, 'requests.seen'), 'a+')self.file.seek(0)self.fingerprints.update(x.rstrip() for x in self.file)@classmethoddef from_settings(cls, settings):debug = settings.getbool('DUPEFILTER_DEBUG')return cls(job_dir(settings), debug)def request_seen(self, request):fp = self.request_fingerprint(request)if fp in self.fingerprints:# 判断我们的请求是否在这个在集合中return True# 没有在集合就添加进去self.fingerprints.add(fp)# 如果用的磁盘队列就写进去记录一下if self.file:self.file.write(fp + os.linesep)

按照正常流程就是大家都会进行重复的采集;我们都知道进程之间内存中的数据不可共享的,那么你在开启多个Scrapy的时候,它们相互之间并不知道对方采集了些什么那些没有没采集。那就大家伙儿自己玩自己的了。完全没没有效率的提升啊!

怎么解决呢?

这就是我们 Scrapy-Redis 解决的问题了,不能协作不就是因为 Request去重 这两个不能共享吗?

那我把这两个独立出来好了。

将 Scrapy 中的 SCHEDULER 组件独立放到大家都能访问的地方不就OK啦!加上 scrapy-redis 后流程图就应该变成这样了?

scrapy-redis 在 scrapy 的架构上增加了 redis,基于 redis 的特性拓展了如下四种组件:Scheduler,Duplication Filter,Item Pipeline,Base Spider

 

 

scrapy-redis 源码分析

 

scrapy-redis 的源代码很少,也比较好懂,很快就能看完。

下面开始 scrapy-redis 源码分析:

scrapy-redis 工程的主体还是 redis 和 scrapy 两个库,工程本身实现的东西不是很多,这个工程就像胶水一样,把这两个插件粘结了起来。下面我们来看看,scrapy-redis的每一个源代码文件都实现了什么功能,最后如何实现分布式的爬虫系统:

 

defaults.py

 

redis 的一些基础的默认的设置。其实就是一些默认配置:

import redis# For standalone use.
DUPEFILTER_KEY = 'dupefilter:%(timestamp)s'PIPELINE_KEY = '%(spider)s:items'REDIS_CLS = redis.StrictRedis
REDIS_ENCODING = 'utf-8'
# Sane connection defaults.
REDIS_PARAMS = {'socket_timeout': 30,'socket_connect_timeout': 30,'retry_on_timeout': True,'encoding': REDIS_ENCODING,
}SCHEDULER_QUEUE_KEY = '%(spider)s:requests'
SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.PriorityQueue'
SCHEDULER_DUPEFILTER_KEY = '%(spider)s:dupefilter'
SCHEDULER_DUPEFILTER_CLASS = 'scrapy_redis.dupefilter.RFPDupeFilter'START_URLS_KEY = '%(name)s:start_urls'
START_URLS_AS_SET = False

 

connect.py

 

connect 文件引入了redis 模块,这个是 redis-python库的接口,用于通过python访问redis数据库,可见,这个文件主要是实现连接redis数据库的功能(返回的是redis库的Redis对象或者StrictRedis对象,这俩都是可以直接用来进行数据操作的对象)。这些连接接口在其他文件中经常被用到。其中,我们可以看到,要想连接到redis数据库,和其他数据库差不多,需要一个ip地址、端口号、用户名密码(可选)和一个整形的数据库编号,同时我们还可以在scrapy工程的setting文件中配置套接字的超时时间、等待时间等。

其实这个模块的功能:

  • 1. 从 settings 里面获取 redis 的链接配置
  • 2. 获取 redis 的 链接 实例
import sixfrom scrapy.utils.misc import load_objectfrom . import defaults# Shortcut maps 'setting name' -> 'parmater name'.
SETTINGS_PARAMS_MAP = {'REDIS_URL': 'url','REDIS_HOST': 'host','REDIS_PORT': 'port','REDIS_ENCODING': 'encoding',
}def get_redis_from_settings(settings):"""Returns a redis client instance from given Scrapy settings object.This function uses ``get_client`` to instantiate the client and uses``defaults.REDIS_PARAMS`` global as defaults values for the parameters. Youcan override them using the ``REDIS_PARAMS`` setting.Parameters----------settings : SettingsA scrapy settings object. See the supported settings below.Returns-------serverRedis client instance.Other Parameters----------------REDIS_URL : str, optionalServer connection URL.REDIS_HOST : str, optionalServer host.REDIS_PORT : str, optionalServer port.REDIS_ENCODING : str, optionalData encoding.REDIS_PARAMS : dict, optionalAdditional client parameters."""params = defaults.REDIS_PARAMS.copy()params.update(settings.getdict('REDIS_PARAMS'))# XXX: Deprecate REDIS_* settings.for source, dest in SETTINGS_PARAMS_MAP.items():val = settings.get(source)if val:params[dest] = val# Allow ``redis_cls`` to be a path to a class.if isinstance(params.get('redis_cls'), six.string_types):params['redis_cls'] = load_object(params['redis_cls'])return get_redis(**params)# Backwards compatible alias.
from_settings = get_redis_from_settingsdef get_redis(**kwargs):"""Returns a redis client instance.Parameters----------redis_cls : class, optionalDefaults to ``redis.StrictRedis``.url : str, optionalIf given, ``redis_cls.from_url`` is used to instantiate the class.**kwargsExtra parameters to be passed to the ``redis_cls`` class.Returns-------serverRedis client instance."""redis_cls = kwargs.pop('redis_cls', defaults.REDIS_CLS)url = kwargs.pop('url', None)if url:return redis_cls.from_url(url, **kwargs)else:return redis_cls(**kwargs)

 

dupefilters.py

 

这个主要是用来去重的。RFPDupeFilter继承自 Scrapy 的BaseDupeFilter,实现了 request 去重功能,基于 Scrapy 的 request_fingerprint 生成指纹,并在 Redis 上存储。当收到新的 request,首先生成指纹判断是否存在于已爬取的指纹库内(Redis set),若存在则返回 False,不存在返回 True.总得来说是这样的,这个文件首先获取到redis的server,然后从scrapy的request中获取request的指纹,将这个指纹进行存到redis的去重库中。达到去重的目的。

这个文件看起来比较复杂,重写了scrapy本身已经实现的 request 判重功能。因为本身 scrapy 单机跑的话,只需要读取内存中的request 队列 或者 持久化的 request 队列(scrapy默认的持久化似乎是json格式的文件,不是数据库)就能判断这次要发出的request url是否已经请求过或者正在调度(本地读就行了)。而 分布式跑的话,就需要各个主机上的scheduler都连接同一个数据库的同一个 request池 来判断这次的请求是否是重复的了。 

在这个文件中,通过继承 BaseDupeFilter 重写他的方法,实现了基于redis的判重。根据源代码来看,scrapy-redis 使用了scrapy本身的一个 fingerprint 接口 request_fingerprint,这个接口很有趣,根据scrapy文档所说,他通过hash来判断两个url是否相同(相同的url会生成相同的hash结果),但是当两个url的地址相同,get型参数相同但是顺序不同时,也会生成相同的hash结果(这个真的比较神奇。。。)所以 scrapy-redis 依旧使用 url 的 fingerprint 来判断 request 请求是否已经出现过。这个类通过连接 redis,使用一个key来向redis的一个set中插入fingerprint(这个key对于同一种spider是相同的,redis 是一个key-value的数据库,如果key是相同的,访问到的值就是相同的,这里使用 spider名字+DupeFilter 的 key 就是为了在不同主机上的不同爬虫实例,只要属于同一种 spider,就会访问到同一个set,而这个 set 就是他们的url判重池 ),如果返回值为0,说明该set中该fingerprint 已经存在(因为集合是没有重复值的),则返回 False,如果返回值为 1,说明添加了一个fingerprint到set中,则说明这个 request 没有重复,于是返回True,还顺便把新fingerprint加入到数据库中了。 

DupeFilter 判重会在 scheduler 类中用到,每一个 request 在进入调度之前都要进行判重,如果重复就不需要参加调度,直接舍弃就好了,不然就是白白浪费资源。

import logging
import timefrom scrapy.dupefilters import BaseDupeFilter
from scrapy.utils.request import request_fingerprintfrom . import defaults
from .connection import get_redis_from_settingslogger = logging.getLogger(__name__)# TODO: Rename class to RedisDupeFilter.
class RFPDupeFilter(BaseDupeFilter):"""Redis-based request duplicates filter.This class can also be used with default Scrapy's scheduler."""logger = loggerdef __init__(self, server, key, debug=False):"""Initialize the duplicates filter.Parameters----------server : redis.StrictRedisThe redis server instance.key : strRedis key Where to store fingerprints.debug : bool, optionalWhether to log filtered requests."""self.server = serverself.key = keyself.debug = debugself.logdupes = True@classmethoddef from_settings(cls, settings):"""Returns an instance from given settings.This uses by default the key ``dupefilter:<timestamp>``. When using the``scrapy_redis.scheduler.Scheduler`` class, this method is not used asit needs to pass the spider name in the key.Parameters----------settings : scrapy.settings.SettingsReturns-------RFPDupeFilterA RFPDupeFilter instance."""server = get_redis_from_settings(settings)# XXX: This creates one-time key. needed to support to use this# class as standalone dupefilter with scrapy's default scheduler# if scrapy passes spider on open() method this wouldn't be needed# TODO: Use SCRAPY_JOB env as default and fallback to timestamp.key = defaults.DUPEFILTER_KEY % {'timestamp': int(time.time())}debug = settings.getbool('DUPEFILTER_DEBUG')return cls(server, key=key, debug=debug)@classmethoddef from_crawler(cls, crawler):"""Returns instance from crawler.Parameters----------crawler : scrapy.crawler.CrawlerReturns-------RFPDupeFilterInstance of RFPDupeFilter."""return cls.from_settings(crawler.settings)def request_seen(self, request):"""Returns True if request was already seen.Parameters----------request : scrapy.http.RequestReturns-------bool"""fp = self.request_fingerprint(request)# This returns the number of values added, zero if already exists.added = self.server.sadd(self.key, fp)return added == 0def request_fingerprint(self, request):"""Returns a fingerprint for a given request.Parameters----------request : scrapy.http.RequestReturns-------str"""return request_fingerprint(request)@classmethoddef from_spider(cls, spider):settings = spider.settingsserver = get_redis_from_settings(settings)dupefilter_key = settings.get("SCHEDULER_DUPEFILTER_KEY", defaults.SCHEDULER_DUPEFILTER_KEY)key = dupefilter_key % {'spider': spider.name}debug = settings.getbool('DUPEFILTER_DEBUG')return cls(server, key=key, debug=debug)def close(self, reason=''):"""Delete data on close. Called by Scrapy's scheduler.Parameters----------reason : str, optional"""self.clear()def clear(self):"""Clears fingerprints data."""self.server.delete(self.key)def log(self, request, spider):"""Logs given request.Parameters----------request : scrapy.http.Requestspider : scrapy.spiders.Spider"""if self.debug:msg = "Filtered duplicate request: %(request)s"self.logger.debug(msg, {'request': request}, extra={'spider': spider})elif self.logdupes:msg = ("Filtered duplicate request %(request)s"" - no more duplicates will be shown"" (see DUPEFILTER_DEBUG to show all duplicates)")self.logger.debug(msg, {'request': request}, extra={'spider': spider})self.logdupes = False

Scrapy 用集合实现这个 request 去重功能,Scrapy 中把已经发送的 request 指纹 放入到一个集合中,把下一个request 的指纹拿到集合中比对,如果该指纹存在于集合中,说明这个 request 发送过了,如果没有则继续操作。

核心的判重功能:

在 scrapy-redis 中去重是由 Duplication Filter 组件来实现的,它通过 redis 的 set 不重复的特性,巧妙的实现了 DuplicationFilter去重。scrapy-redis 调度器从引擎接受request,将 request 的指纹存入 redis 的 set 检查是否重复,并将不重复的 request push写入 redis 的 request queue。

引擎请求 request (Spider发出的)时,调度器从 redis 的request queue 队列里根据优先级 pop 出⼀个request 返回给引擎,引擎将此request发给spider处理。

 

picklecompat.py

 

这里实现了 loads 和 dumps 两个函数,其实就是实现了一个 serializer,因为 redis 数据库不能存储复杂对象(value部分只能是字符串,字符串列表,字符串集合和hash,key部分只能是字符串)所以我们存啥都要先串行化成文本才行。这里使用的就是python 的 pickle 模块,一个兼容 py2 和 py3 的串行化工具。这个 serializer 主要用于一会的 scheduler 存 reuqest 对象,至于为什么不实用 json 格式,我也不是很懂,item pipeline 的串行化默认用的就是 json。

"""A pickle wrapper module with protocol=-1 by default."""try:import cPickle as pickle  # PY2
except ImportError:import pickledef loads(s):return pickle.loads(s)def dumps(obj):return pickle.dumps(obj, protocol=-1)

 

pipeline.py

 

这是是用来实现分布式处理的作用。它将 Item 存储在 redis 中以实现分布式处理。由于在这里需要读取配置,所以就用到了from_crawler() 函数。pipeline 文件 实现了一个 item pipieline类,和 scrapy 的 item pipeline 是同一个对象,通过从 settings 中拿到我们配置的REDIS_ITEMS_KEY 作为 key,把 item 串行化之后存入 redis 数据库对应的 value 中(这个value可以看出出是个list,我们的每个item是这个list中的一个结点),这个pipeline把提取出的item存起来,主要是为了方便我们延后处理数据。

from scrapy.utils.misc import load_object
from scrapy.utils.serialize import ScrapyJSONEncoder
from twisted.internet.threads import deferToThreadfrom . import connection, defaultsdefault_serialize = ScrapyJSONEncoder().encodeclass RedisPipeline(object):"""Pushes serialized item into a redis list/queueSettings--------REDIS_ITEMS_KEY : strRedis key where to store items.REDIS_ITEMS_SERIALIZER : strObject path to serializer function."""def __init__(self, server,key=defaults.PIPELINE_KEY,serialize_func=default_serialize):"""Initialize pipeline.Parameters----------server : StrictRedisRedis client instance.key : strRedis key where to store items.serialize_func : callableItems serializer function."""self.server = serverself.key = keyself.serialize = serialize_func@classmethoddef from_settings(cls, settings):params = {'server': connection.from_settings(settings),}if settings.get('REDIS_ITEMS_KEY'):params['key'] = settings['REDIS_ITEMS_KEY']if settings.get('REDIS_ITEMS_SERIALIZER'):params['serialize_func'] = load_object(settings['REDIS_ITEMS_SERIALIZER'])return cls(**params)@classmethoddef from_crawler(cls, crawler):return cls.from_settings(crawler.settings)def process_item(self, item, spider):return deferToThread(self._process_item, item, spider)def _process_item(self, item, spider):key = self.item_key(item, spider)data = self.serialize(item)self.server.rpush(key, data)return itemdef item_key(self, item, spider):"""Returns redis key based on given spider.Override this function to use a different key depending on the itemand/or spider."""return self.key % {'spider': spider.name}

 

queue.py

 

该文件实现了几个容器类,可以看这些容器和redis交互频繁,同时使用了我们上边 picklecompat 中定义的 serializer。这个文件实现的几个容器大体相同,只不过一个是队列,一个是栈,一个是优先级队列,这三个容器到时候会被scheduler对象实例化,来实现 request的调度。比如:我们使用 SpiderQueue 作为调度队列的类型,到时候 request 的调度方法就是先进先出,而实用SpiderStack 就是先进后出了。 

我们可以仔细看看 SpiderQueue 的实现,他的 push 函数就和其他容器的一样,只不过 push进去的 request请求先被scrapy的接口 request_to_dict 变成了一个dict对象(因为request对象实在是比较复杂,有方法有属性不好串行化),之后使用picklecompat中的serializer串行化为字符串,然后使用一个特定的 key 存入redis中(该key在同一种spider中是相同的)。而调用pop时,其实就是从redis用那个特定的key去读其值(一个list),从list中读取最早进去的那个,于是就先进先出了。 

这些容器类都会作为 scheduler 调度 request 的容器,scheduler 在每个主机上都会实例化一个,并且和 spider一一对应,所以分布式运行时会有一个 spider 的多个实例和一个 scheduler 的多个实例存在于不同的主机上,但是,因为 scheduler 都是用相同的容器,而这些容器都连接同一个 redis 服务器,又都使用 spider 名加 queue 来作为 key 读写数据,所以不同主机上的不同爬虫实例公用一个 request 调度池,实现了分布式爬虫之间的统一调度。

from scrapy.utils.reqser import request_to_dict, request_from_dictfrom . import picklecompatclass Base(object):"""Per-spider base queue class"""def __init__(self, server, spider, key, serializer=None):"""Initialize per-spider redis queue.Parameters----------server : StrictRedisRedis client instance.spider : SpiderScrapy spider instance.key: strRedis key where to put and get messages.serializer : objectSerializer object with ``loads`` and ``dumps`` methods."""if serializer is None:# Backward compatibility.# TODO: deprecate pickle.serializer = picklecompatif not hasattr(serializer, 'loads'):raise TypeError("serializer does not implement 'loads' function: %r"% serializer)if not hasattr(serializer, 'dumps'):raise TypeError("serializer '%s' does not implement 'dumps' function: %r"% serializer)self.server = serverself.spider = spiderself.key = key % {'spider': spider.name}self.serializer = serializerdef _encode_request(self, request):"""Encode a request object"""obj = request_to_dict(request, self.spider)return self.serializer.dumps(obj)def _decode_request(self, encoded_request):"""Decode an request previously encoded"""obj = self.serializer.loads(encoded_request)return request_from_dict(obj, self.spider)def __len__(self):"""Return the length of the queue"""raise NotImplementedErrordef push(self, request):"""Push a request"""raise NotImplementedErrordef pop(self, timeout=0):"""Pop a request"""raise NotImplementedErrordef clear(self):"""Clear queue/stack"""self.server.delete(self.key)class FifoQueue(Base):"""Per-spider FIFO queue"""def __len__(self):"""Return the length of the queue"""return self.server.llen(self.key)def push(self, request):"""Push a request"""self.server.lpush(self.key, self._encode_request(request))def pop(self, timeout=0):"""Pop a request"""if timeout > 0:data = self.server.brpop(self.key, timeout)if isinstance(data, tuple):data = data[1]else:data = self.server.rpop(self.key)if data:return self._decode_request(data)class PriorityQueue(Base):"""Per-spider priority queue abstraction using redis' sorted set"""def __len__(self):"""Return the length of the queue"""return self.server.zcard(self.key)def push(self, request):"""Push a request"""data = self._encode_request(request)score = -request.priority# We don't use zadd method as the order of arguments change depending on# whether the class is Redis or StrictRedis, and the option of using# kwargs only accepts strings, not bytes.self.server.execute_command('ZADD', self.key, score, data)def pop(self, timeout=0):"""Pop a requesttimeout not support in this queue class"""# use atomic range/remove using multi/execpipe = self.server.pipeline()pipe.multi()pipe.zrange(self.key, 0, 0).zremrangebyrank(self.key, 0, 0)results, count = pipe.execute()if results:return self._decode_request(results[0])class LifoQueue(Base):"""Per-spider LIFO queue."""def __len__(self):"""Return the length of the stack"""return self.server.llen(self.key)def push(self, request):"""Push a request"""self.server.lpush(self.key, self._encode_request(request))def pop(self, timeout=0):"""Pop a request"""if timeout > 0:data = self.server.blpop(self.key, timeout)if isinstance(data, tuple):data = data[1]else:data = self.server.lpop(self.key)if data:return self._decode_request(data)# TODO: Deprecate the use of these names.
SpiderQueue = FifoQueue
SpiderStack = LifoQueue
SpiderPriorityQueue = PriorityQueue

可以看出,是以 base 为基类,然后被三个队列的类继承。然后进行了pop和push的操作。

  • FifoQueue: 继承 Base,重写了 push 和 pop 方法实现 先进先出 队列。
  • PriorityQueue: 继承 Base,重写了 push 和 pop 方法实现 优先级 队列。
  • LifoQueue: 继承 Base,重写了 push 和 pop 方法实现 后进先出 队列。

 

scheduler.py

 

        scrapy 改造了 python 本来的 collection.deque(双向队列)形成了自己的 Scrapy queue,但是 Scrapy 多个 spider 不能共享待爬取队列 Scrapy queue,即 Scrapy 本身不支持爬虫分布式。

        scrapy-redis 的解决是把这个 Scrapy queue 换成 redis 数据库(也是指 redis 队列),从同一个 redis-server 存放要爬取的request,便能让多个 spider 去同一个数据库里读取。

        Scrapy 中跟 “待爬队列” 直接相关的就是调度器 Scheduler,它负责对新的 request 进行入列操作(加入Scrapy queue),取出下一个要爬取的 request(从Scrapy queue中取出)等操作。它把待爬队列按照优先级建立了一个字典结构,然后根据 request 中 的优先级,来决定该入哪个队列,出列时则按优先级较小的优先出列。为了管理这个比较高级的队列字典,Scheduler 需要提供一系列的方法。但是原来的 Scheduler 已经无法使用,所以使用 Scrapy-redis 的 scheduler 组件。

        scheduler.py 这个文件重写了 scheduler 类用来代替 scrapy.core.scheduler 的原有调度器。其实对原有调度器的逻辑没有很大的改变,主要是使用了redis 作为数据存储的媒介,以达到各个爬虫之间的统一调度。 scheduler 负责调度各个 spider 的 request 请求,scheduler 初始化时,通过 settings 文件读取 queue 和 dupefilters 的类型(一般就用上边默认的),配置 queue 和 dupefilters 使用的 key(一般就是spider name加上queue或者dupefilters,这样对于同一种spider 的不同实例,就会使用相同的数据块了)。每当一个 request 要被调度时,enqueue_request 被调用,scheduler 使用dupefilters 来判断这个url是否重复,如果不重复,就添加到 queue 的容器中(先进先出,先进后出和优先级都可以,可以在settings中配置)。当调度完成时,next_request 被调用,scheduler 就通过 queue 容器的接口,取出一个 request,把他发送给相应的 spider,让spider 进行爬取工作。

import importlib
import sixfrom scrapy.utils.misc import load_objectfrom . import connection, defaults# TODO: add SCRAPY_JOB support.
class Scheduler(object):"""Redis-based schedulerSettings--------SCHEDULER_PERSIST : bool (default: False)Whether to persist or clear redis queue.SCHEDULER_FLUSH_ON_START : bool (default: False)Whether to flush redis queue on start.SCHEDULER_IDLE_BEFORE_CLOSE : int (default: 0)How many seconds to wait before closing if no message is received.SCHEDULER_QUEUE_KEY : strScheduler redis key.SCHEDULER_QUEUE_CLASS : strScheduler queue class.SCHEDULER_DUPEFILTER_KEY : strScheduler dupefilter redis key.SCHEDULER_DUPEFILTER_CLASS : strScheduler dupefilter class.SCHEDULER_SERIALIZER : strScheduler serializer."""def __init__(self, server,persist=False,flush_on_start=False,queue_key=defaults.SCHEDULER_QUEUE_KEY,queue_cls=defaults.SCHEDULER_QUEUE_CLASS,dupefilter_key=defaults.SCHEDULER_DUPEFILTER_KEY,dupefilter_cls=defaults.SCHEDULER_DUPEFILTER_CLASS,idle_before_close=0,serializer=None):"""Initialize scheduler.Parameters----------server : RedisThe redis server instance.persist : boolWhether to flush requests when closing. Default is False.flush_on_start : boolWhether to flush requests on start. Default is False.queue_key : strRequests queue key.queue_cls : strImportable path to the queue class.dupefilter_key : strDuplicates filter key.dupefilter_cls : strImportable path to the dupefilter class.idle_before_close : intTimeout before giving up."""if idle_before_close < 0:raise TypeError("idle_before_close cannot be negative")self.server = serverself.persist = persistself.flush_on_start = flush_on_startself.queue_key = queue_keyself.queue_cls = queue_clsself.dupefilter_cls = dupefilter_clsself.dupefilter_key = dupefilter_keyself.idle_before_close = idle_before_closeself.serializer = serializerself.stats = Nonedef __len__(self):return len(self.queue)@classmethoddef from_settings(cls, settings):kwargs = {'persist': settings.getbool('SCHEDULER_PERSIST'),'flush_on_start': settings.getbool('SCHEDULER_FLUSH_ON_START'),'idle_before_close': settings.getint('SCHEDULER_IDLE_BEFORE_CLOSE'),}# If these values are missing, it means we want to use the defaults.optional = {# TODO: Use custom prefixes for this settings to note that are# specific to scrapy-redis.'queue_key': 'SCHEDULER_QUEUE_KEY','queue_cls': 'SCHEDULER_QUEUE_CLASS','dupefilter_key': 'SCHEDULER_DUPEFILTER_KEY',# We use the default setting name to keep compatibility.'dupefilter_cls': 'DUPEFILTER_CLASS','serializer': 'SCHEDULER_SERIALIZER',}for name, setting_name in optional.items():val = settings.get(setting_name)if val:kwargs[name] = val# Support serializer as a path to a module.if isinstance(kwargs.get('serializer'), six.string_types):kwargs['serializer'] = importlib.import_module(kwargs['serializer'])server = connection.from_settings(settings)# Ensure the connection is working.server.ping()return cls(server=server, **kwargs)@classmethoddef from_crawler(cls, crawler):instance = cls.from_settings(crawler.settings)# FIXME: for now, stats are only supported from this constructorinstance.stats = crawler.statsreturn instancedef open(self, spider):self.spider = spidertry:self.queue = load_object(self.queue_cls)(server=self.server,spider=spider,key=self.queue_key % {'spider': spider.name},serializer=self.serializer,)except TypeError as e:raise ValueError("Failed to instantiate queue class '%s': %s",self.queue_cls, e)self.df = load_object(self.dupefilter_cls).from_spider(spider)if self.flush_on_start:self.flush()# notice if there are requests already in the queue to resume the crawlif len(self.queue):spider.log("Resuming crawl (%d requests scheduled)" % len(self.queue))def close(self, reason):if not self.persist:self.flush()def flush(self):self.df.clear()self.queue.clear()def enqueue_request(self, request):if not request.dont_filter and self.df.request_seen(request):self.df.log(request, self.spider)return Falseif self.stats:self.stats.inc_value('scheduler/enqueued/redis', spider=self.spider)self.queue.push(request)return Truedef next_request(self):block_pop_timeout = self.idle_before_closerequest = self.queue.pop(block_pop_timeout)if request and self.stats:self.stats.inc_value('scheduler/dequeued/redis', spider=self.spider)return requestdef has_pending_requests(self):return len(self) > 0

这个文件下只有一个类 Scheduler,一如既往的通过类方法来实例化来实现 外部可以直接通过调用两个方法然后从爬虫中获取settings 和 crawler,但是有两个比较特殊的函数是 def open(self, spider) 和 def next_request(self):

  1. open(): 调度器启动时的自动的操作,这里主要实例化了任务队列 queue 和过滤器 dupefilter。
  2. next_request(): 从任务队列取出 request。

 

spider.py

 

如果在 settings.py 里面:REDIS_START_URLS_AS_SET = False 的话,就是列表的形式,存入就是 lpush 或者是 rpush 等操作,如果是 REDIS_START_URLS_AS_SET = True 的话,那么存入就是集合的形式,sadd 等操作。

REDIS_START_URLS_AS_SET = False  # 默认是 False ,即 默认 从列表的格式取数据出来。

如果不设置 REDIS_START_URLS_KEY,则默认 REDIS_START_URLS_KEY = '%(name)s:start_urls' ,这个是存入 redis 里面的 key,可以根据这来取 value,例如:start_urls:baidu

redis 写入 URL ( 即 添加任务 ):

添加 列表 形式 的 任务:

import  redisconn = redis.Redis(host='127.0.0.1',port=6379)
conn.lpush('start_urls:baidu','http://www.baidu.com')

添加 集合 形式 的 任务:

import redisconn = redis.Redis(host='127.0.0.1', port=6379)
conn.sadd('start_urls:baidu', 'http://www.baidu.com')  # 按照这个格式来存数据的
print(conn.smembers('start_urls:baidu'))

 

使用 scrapy-redis 提供的方法添加任务 示例:

首先需要在 setting.py 里面配置:

# REDIS_URL = 'redis://用户名:密码@主机IP:端口'
REDIS_URL = 'redis://127.0.0.1:6379'
REDIS_PARAMS = dict(db=15)

添加任务:

import redis
from scrapy.utils.project import get_project_settings
from scrapy_redis.connection import get_redis_from_settings# 方法 1
# 如果是在 scrapy-redis 工程里面可以使用这个方法
server_1 = get_redis_from_settings(get_project_settings())
server_1.sadd('start_urls:server_1', 'http://www.baidu.com')# 方法 2
server_2 = redis.Redis(host='127.0.0.1', port=6379, db=15)
server_2.sadd('start_urls:server_2', 'http://www.baidu.com')# 方法 3
redis_url = 'redis://root:xxxx@47.110.xx.xx:6379'# 加上 decode_responses=True,写入的键值对中的value为str类型,不加这个参数写入的则为字节类型。
r = redis.Redis.from_url(redis_url, decode_responses=True) 
r.lpush('test_key', 'wwww')

 

spider 的改动也不是很大,主要是通过 connect 接口,给 spider 绑定了 spider_idle 信号,spider 初始化时,通过 setup_redis 函数初始化好 redis 的连接,之后通过 next_requests 函数从 redis 中取出 strat url,使用的 key 是 settings 中REDIS_START_URLS_AS_SET 定义的(注意了这里的初始化 url 池 和 我们上边的 queue 的 url池 不是一个东西,queue的池是用于调度的,初始化 url池 是存放入口 url 的,他们都存在 redis 中,但是使用不同的 key 来区分,就当成是不同的表吧),spider 使用少量的 start url,可以发展出很多新的 url,这些 url 会进入 scheduler 进行判重和调度。直到 spider 跑到调度池内没有 url 的时候,会触发 spider_idle 信号,从而触发 spider 的 next_requests 函数,再次从 redis 的 start url 池中读取一些url。

分析:在这个 spider 中通过 connect signals.spider_idle 信号实现对 crawler 状态的监视。当 idle 时,返回新的make_requests_from_url(url) 给引擎,进而交给调度器调度。

from scrapy import signals
from scrapy.exceptions import DontCloseSpider
from scrapy.spiders import Spider, CrawlSpiderfrom . import connection, defaults
from .utils import bytes_to_strclass RedisMixin(object):"""Mixin class to implement reading urls from a redis queue."""redis_key = Noneredis_batch_size = Noneredis_encoding = None# Redis client placeholder.server = Nonedef start_requests(self):"""Returns a batch of start requests from redis."""return self.next_requests()def setup_redis(self, crawler=None):"""Setup redis connection and idle signal.This should be called after the spider has set its crawler object."""if self.server is not None:returnif crawler is None:# We allow optional crawler argument to keep backwards# compatibility.# XXX: Raise a deprecation warning.crawler = getattr(self, 'crawler', None)if crawler is None:raise ValueError("crawler is required")settings = crawler.settingsif self.redis_key is None:self.redis_key = settings.get('REDIS_START_URLS_KEY', defaults.START_URLS_KEY,)self.redis_key = self.redis_key % {'name': self.name}if not self.redis_key.strip():raise ValueError("redis_key must not be empty")if self.redis_batch_size is None:# TODO: Deprecate this setting (REDIS_START_URLS_BATCH_SIZE).self.redis_batch_size = settings.getint('REDIS_START_URLS_BATCH_SIZE',settings.getint('CONCURRENT_REQUESTS'),)try:self.redis_batch_size = int(self.redis_batch_size)except (TypeError, ValueError):raise ValueError("redis_batch_size must be an integer")if self.redis_encoding is None:self.redis_encoding = settings.get('REDIS_ENCODING', defaults.REDIS_ENCODING)self.logger.info("Reading start URLs from redis key '%(redis_key)s' ""(batch size: %(redis_batch_size)s, encoding: %(redis_encoding)s",self.__dict__)self.server = connection.from_settings(crawler.settings)# The idle signal is called when the spider has no requests left,# that's when we will schedule new requests from redis queuecrawler.signals.connect(self.spider_idle, signal=signals.spider_idle)def next_requests(self):"""Returns a request to be scheduled or none."""use_set = self.settings.getbool('REDIS_START_URLS_AS_SET', defaults.START_URLS_AS_SET)fetch_one = self.server.spop if use_set else self.server.lpop# XXX: Do we need to use a timeout here?found = 0# TODO: Use redis pipeline execution.while found < self.redis_batch_size:data = fetch_one(self.redis_key)if not data:# Queue empty.breakreq = self.make_request_from_data(data)if req:yield reqfound += 1else:self.logger.debug("Request not made from data: %r", data)if found:self.logger.debug("Read %s requests from '%s'", found, self.redis_key)def make_request_from_data(self, data):"""Returns a Request instance from data coming from Redis.By default, ``data`` is an encoded URL. You can override this method toprovide your own message decoding.Parameters----------data : bytesMessage from redis."""url = bytes_to_str(data, self.redis_encoding)return self.make_requests_from_url(url)def schedule_next_requests(self):"""Schedules a request if available"""# TODO: While there is capacity, schedule a batch of redis requests.for req in self.next_requests():self.crawler.engine.crawl(req, spider=self)def spider_idle(self):"""Schedules a request if available, otherwise waits."""# XXX: Handle a sentinel to close the spider.self.schedule_next_requests()raise DontCloseSpiderclass RedisSpider(RedisMixin, Spider):"""Spider that reads urls from redis queue when idle.Attributes----------redis_key : str (default: REDIS_START_URLS_KEY)Redis key where to fetch start URLs from..redis_batch_size : int (default: CONCURRENT_REQUESTS)Number of messages to fetch from redis on each attempt.redis_encoding : str (default: REDIS_ENCODING)Encoding to use when decoding messages from redis queue.Settings--------REDIS_START_URLS_KEY : str (default: "<spider.name>:start_urls")Default Redis key where to fetch start URLs from..REDIS_START_URLS_BATCH_SIZE : int (deprecated by CONCURRENT_REQUESTS)Default number of messages to fetch from redis on each attempt.REDIS_START_URLS_AS_SET : bool (default: False)Use SET operations to retrieve messages from the redis queue. If False,the messages are retrieve using the LPOP command.REDIS_ENCODING : str (default: "utf-8")Default encoding to use when decoding messages from redis queue."""@classmethoddef from_crawler(self, crawler, *args, **kwargs):obj = super(RedisSpider, self).from_crawler(crawler, *args, **kwargs)obj.setup_redis(crawler)return objclass RedisCrawlSpider(RedisMixin, CrawlSpider):"""Spider that reads urls from redis queue when idle.Attributes----------redis_key : str (default: REDIS_START_URLS_KEY)Redis key where to fetch start URLs from..redis_batch_size : int (default: CONCURRENT_REQUESTS)Number of messages to fetch from redis on each attempt.redis_encoding : str (default: REDIS_ENCODING)Encoding to use when decoding messages from redis queue.Settings--------REDIS_START_URLS_KEY : str (default: "<spider.name>:start_urls")Default Redis key where to fetch start URLs from..REDIS_START_URLS_BATCH_SIZE : int (deprecated by CONCURRENT_REQUESTS)Default number of messages to fetch from redis on each attempt.REDIS_START_URLS_AS_SET : bool (default: True)Use SET operations to retrieve messages from the redis queue.REDIS_ENCODING : str (default: "utf-8")Default encoding to use when decoding messages from redis queue."""@classmethoddef from_crawler(self, crawler, *args, **kwargs):obj = super(RedisCrawlSpider, self).from_crawler(crawler, *args, **kwargs)obj.setup_redis(crawler)return obj

这个 spider 文件有三个类,RedisMixin 是一个基类,剩余两个是多继承。

RedisMixin 类

  • 1 setup_redis 主要是获取 redis 的 server 和获取爬虫的 idle signal,当爬虫没有请求时,调用空闲信号,那时我们将从 redis队列安排新请求。
  • 2 spider_idle 主要是 idle 信号处理,这里调用 schedule_next_requests 完成从 Redis 调度
  • 3 make_request_from_data 主要是从 redis 队列获取ur
  • 4 next_requests 这个主要是 redis 获取 url,我从这里看到可以设置 url 队列在 redis 的存储的数据格式函数schedule_next_requests从next_requests 获取 url 包装为 HttpRequest

RedisSpider(RedisMixin, Spiser) 类多继承,用 RedisMixin 调度功能覆盖 Spider 原生

RedisCrawlSpider(RedisMixin, CrawlSpiser) 类多继承,用 RedisMixin 调度功能覆盖 CrawlSpider 原生

 

当编写分布式爬虫时,不在使用 scrapy 原有的 Spider 类,重写的 RedisSpider 继承了 Spider 和 RedisMixin 这两个类,RedisMixin 是用来从 redis 读取 url 的类。

当我们生成一个 Spider 继承 RedisSpider 时,调用 setup_redis 函数,这个函数会去连接 redis 数据库,然后会设置 signals (信号):

  • 一个是当 spider 空闲时候的 signal,会调用 spider_idle 函数,这个函数调用 schedule_next_request 函数,保证 spider 是一直活着的状态,并且抛出 DontCloseSpider 异常。
  • 一个是当抓到一个 item 时的 signal,会调用 item_scraped 函数,这个函数会调用 schedule_next_request 函数,获取下一个 request。

 

scrapy-redis 的 总体思路:

 

  1. scrapy-redis 通过重写 scheduler 和 spider 类,实现了调度、spider 启动 和 redis 的交互。
  2. 实现新的 dupefilter 和 queue 类,达到了 判重 和 调度容器 和 redis 的交互,因为每个主机上的爬虫进程都访问同一个 redis 数据库,所以 调度 和 判重 都统一进行统一管理,达到了分布式爬虫的目的。 
  3. 当 spider 被初始化时,同时会初始化一个对应的 scheduler 对象,这个调度器对象通过读取 settings,配置好自己的调度容器 queue 和 判重工具 dupefilter。
  4. 每当一个 spider 产出一个 request 的时候,scrapy 内核会把这个 reuqest 递交给这个 spider 对应的 scheduler 对象 进行调度,scheduler 对象 通过访问 redis 对 request 进行判重,如果不重复就把他添加进 redis 中的调度池。当调度条件满足时,scheduler 对象 就从 redis 的调度池中取出一个 request 发送给 spider,让他爬取。
  5. 当 spider 爬取的所有暂时可用 url 之后,scheduler 发现这个 spider 对应的 redis 的调度池空了,于是触发信号 spider_idle,spider 收到这个信号之后,直接连接 redis 读取 strart url 池,拿去新的一批 url 入口,然后再次重复上边的工作。

 

 

集成 bloomfilter 到 scrapy-redis 中

 

传送门:bloomfilter算法详解及实例

算法实现:bloomfilter_imooc

 

dupefilter.py:

import logging
import timefrom scrapy.dupefilters import BaseDupeFilter
from scrapy.utils.request import request_fingerprintfrom . import defaults
from .connection import get_redis_from_settingslogger = logging.getLogger(__name__)# TODO: Rename class to RedisDupeFilter.
class RFPDupeFilter(BaseDupeFilter):"""Redis-based request duplicates filter.This class can also be used with default Scrapy's scheduler."""logger = loggerdef __init__(self, server, key, debug=False):"""Initialize the duplicates filter.Parameters----------server : redis.StrictRedisThe redis server instance.key : strRedis key Where to store fingerprints.debug : bool, optionalWhether to log filtered requests."""self.server = serverself.key = keyself.debug = debugself.logdupes = True@classmethoddef from_settings(cls, settings):"""Returns an instance from given settings.This uses by default the key ``dupefilter:<timestamp>``. When using the``scrapy_redis.scheduler.Scheduler`` class, this method is not used asit needs to pass the spider name in the key.Parameters----------settings : scrapy.settings.SettingsReturns-------RFPDupeFilterA RFPDupeFilter instance."""server = get_redis_from_settings(settings)# XXX: This creates one-time key. needed to support to use this# class as standalone dupefilter with scrapy's default scheduler# if scrapy passes spider on open() method this wouldn't be needed# TODO: Use SCRAPY_JOB env as default and fallback to timestamp.key = defaults.DUPEFILTER_KEY % {'timestamp': int(time.time())}debug = settings.getbool('DUPEFILTER_DEBUG')return cls(server, key=key, debug=debug)@classmethoddef from_crawler(cls, crawler):"""Returns instance from crawler.Parameters----------crawler : scrapy.crawler.CrawlerReturns-------RFPDupeFilterInstance of RFPDupeFilter."""return cls.from_settings(crawler.settings)def request_seen(self, request):"""Returns True if request was already seen.Parameters----------request : scrapy.http.RequestReturns-------bool"""fp = self.request_fingerprint(request)# This returns the number of values added, zero if already exists.added = self.server.sadd(self.key, fp)return added == 0def request_fingerprint(self, request):"""Returns a fingerprint for a given request.Parameters----------request : scrapy.http.RequestReturns-------str"""return request_fingerprint(request)@classmethoddef from_spider(cls, spider):settings = spider.settingsserver = get_redis_from_settings(settings)dupefilter_key = settings.get("SCHEDULER_DUPEFILTER_KEY", defaults.SCHEDULER_DUPEFILTER_KEY)key = dupefilter_key % {'spider': spider.name}debug = settings.getbool('DUPEFILTER_DEBUG')return cls(server, key=key, debug=debug)def close(self, reason=''):"""Delete data on close. Called by Scrapy's scheduler.Parameters----------reason : str, optional"""self.clear()def clear(self):"""Clears fingerprints data."""self.server.delete(self.key)def log(self, request, spider):"""Logs given request.Parameters----------request : scrapy.http.Requestspider : scrapy.spiders.Spider"""if self.debug:msg = "Filtered duplicate request: %(request)s"self.logger.debug(msg, {'request': request}, extra={'spider': spider})elif self.logdupes:msg = ("Filtered duplicate request %(request)s"" - no more duplicates will be shown"" (see DUPEFILTER_DEBUG to show all duplicates)")self.logger.debug(msg, {'request': request}, extra={'spider': spider})self.logdupes = False

 

 

Scrapy-Redis 分布式爬虫框架使用实例

 

1. 创建项目

scrapy startproject example

example/
├── scrapy.cfg
└── example
    ├── __init__.py
    ├── items.py
    ├── middlewares.py
    ├── pipelines.py
    ├── settings.py
    └── spiders
        ├── __init__.py
        └── my_crawlspider.py/my_redisspider.py/my_rediscrawlspider.py  # 3中类型的Scrap-Redis爬虫

 

2. 明确目标

vim items.py :定义要爬取的字段

import scrapyclass ExampleItem(scrapy.Item):name = scrapy.Field()description = scrapy.Field()link = scrapy.Field()crawled = scrapy.Field()spider = scrapy.Field()url = scrapy.Field()

也可以不写 item ,直接返回一个 Python 类型 的 dict 对象,因为 item 本身就是一个 dict 类型的对象。

 

3. 编写自定义 pipeline

vim pipelines.py

from datetime import datetime
class ExampPipeline(object):def process_item(self,item,spider):item['crawled'] = datetime.utcnow()  # 调用datetime.utcnow()方法获取爬虫执行时的UTC时间# 调用spider.name属性获取当前爬虫名(因为可能同时有多个爬虫在爬取,这样可以看到谁爬了哪些网页)item['spider'] = spider.name          return item

 

4. 注册自定义 pipeline 及 Scrapy-Redis 分布式爬虫相关设置

vim settings.py

#-----------Scrapy-Redis分布式爬虫相关设置如下-------------
# 使用Scrapy-Redis的去重组件,不再使用scrapy的去重组件
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"   
# 使用Scrapy-Redis的调度器,不再使用scrapy的调度器
SCHEDULER = "scrapy_redis.scheduler.Scheduler"               
# 使用Scrapy-Redis的从请求集合中取出请求的方式,三种方式择其一即可:
SCHEDULER_CLASS = "scrapy_redis.queue.SpiderPriorityQueue"   
# 分别按(1)请求的优先级/(2)队列FIFO/(3)栈FILO 取出请求
#SCHEDULER_CLASS = "scrapy_redis.queue.SpiderQueue"          
#SCHEDULER_CLASS = "scrapy_redis.queue.SpiderStack"
SCHEDULER_PERSIST = True         # 允许暂停,redis请求记录不会丢失(重启爬虫不会重头爬取已爬过的页面)REDIS_HOST = "200.200.200.200"   # 这两项是Redis连接设置,如果注释或不写会默认将数据存放到本机的Redis中
REDIS_PORT = 6379                # 注意:master端的Redis需要允许远程连接--配置中注释掉bind 127.0.0.1#----------注册RedisPipeline/自定义pipeline------------------
# # 注意:自定义pipeline的优先级需高于Redispipeline,因为RedisPipeline不会返回item,
# 所以如果RedisPipeline优先级高于自定义pipeline,那么自定义pipeline无法获取到item
ITEM_PIPELINES = {"example.pipelines.ExampPipeline":300,        # 自定义pipeline视情况选择性注册(可选)"scrapy_redis.pipelines.RedisPipeline":400    # 将RedisPipeline注册到pipeline组件中(这样才能将数据存入Redis)
}   

 

5. 编写爬虫

( 三种 Scrapy-Redis 爬虫:CrawlSpider / RedisSpider / RedisCrawlSpider )

scrapy.Spider 和scrapy.CrawlSpider 区别:http://scrapy-chs.readthedocs.io/zh_CN/1.0/topics/spiders.html

Spider 是最简单的 spider。每个其他的 spider 必须继承自该类(包括 Scrapy 自带的其他 spider 以及您自己编写的 spider )。 Spider 并没有提供什么特殊的功能。 其仅仅提供了 start_requests() 的默认实现,读取并请求 spider 属性中的 start_urls,并根据返回的结果 (resulting responses) 调用 spider 的 parse 方法。

CrawlSpider 是爬取一般网站常用的 spider。CrawlSpider 定义了一些规则(rule)来提供跟进 link 的方便的机制 也许该 spider并不是完全适合您的特定网站或项目,但其对很多情况都使用。 因此您可以以其为起点,根据需求修改部分方法。当然您也可以实现自己的spider。

 

类型一:(基于 scrapy 的非分布式爬虫)

继承 CrawlSpider类 的 Scrapy 爬虫

导入 CrawlSpider 类:from scrapy.spiders import CrawlSpider

(1) 生成爬虫

        scrapy genspider -t crawl my_crawlspider "dmoz.org"

(2) 设置爬虫

        vim my_crawlspider.py

# -*- coding: utf-8 -*-from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Ruleclass TestSpider(CrawlSpider):"""TestSpider 继承 CrawlSpider 爬虫类,也可以继承 scrapy.spider.Spider 类。但是如果继承 CrawlSpider,则可以定义一些规则(rule)来提供跟进 link 的方便的机制"""name = "test_spider"allowed_domains = ["dmoz.org"]start_urls = ["http://www.dmoz.org/"]links = LinkExtractor(restrict_css=('.top-cat', '.sub-cat', '.cat-item'))rules = [Rule(links, callback='parse_directory', follow=True),]def __init__(self):super(TestSpider, self).__init__()self.temp = Nonedef parse_directory(self, response):self.temp = Nonefor div in response.css('.title-and-desc'):data = {'name': div.css('.site-title::text').extract_first(),'description': div.css('.site-descr::text').extract_first(),                   'link': div.css('.a::attr(href)').extract(),}yield dataif __name__ == '__main__':from scrapy import cmdlinecmdline.execute('scrapy crawl test_spider'.split())pass

(3) 执行爬虫方法--- scrapy crawl my_crawlspider ( 与正常 scrapy 一样,无需 redis_key,比较鸡肋并不是真正的多机器爬虫)

 

示例:爬取 豆瓣电影

# -*- coding: utf-8 -*-from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractorclass DoubanSpider(CrawlSpider):name = 'douban'allowed_domains = ['movie.douban.com']start_urls = ['https://movie.douban.com/top250']custom_settings = {'DEFAULT_REQUEST_HEADERS': {'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,''*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',"Accept-Encoding": "gzip, deflate","Accept-Language": "zh-CN,zh;q=0.9","Connection": "keep-alive","Host": "movie.douban.com","Upgrade-Insecure-Requests": "1","User-Agent": 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'' (KHTML, like Gecko) Chrome/81.0.4044.113 Safari/537.36',},'CONCURRENT_REQUESTS': 10,'DOWNLOAD_DELAY': 0.01,'CONCURRENT_REQUESTS_PER_IP': 0,'CONCURRENT_REQUESTS_PER_DOMAIN': 10000,'FEED_EXPORT_ENCODING': 'utf-8'}# 定义一个爬取规则,从start_urls中的网页提取LinkExtractor上规定的所有链接,callback为对这些链接如何处理rules = (Rule(LinkExtractor(allow=(r'https://movie.douban.com/subject/\d+',)),callback='parse_item'),)def parse_item(self, response):print(self.name)data = dict()data['title'] = ''.join(response.xpath('//span[@property="v:itemreviewed"]/text()').extract())print(data)# yield dataif __name__ == '__main__':from scrapy import cmdlinecmdline.execute('scrapy crawl douban'.split())pass

 

 

类型二:( 基于 Scrapy-Redis 的 分布式爬虫 )

基于 RedisSpider类 的 Scrapy-Redis 分布式爬虫

(1) 生成爬虫--- scrapy genspider my_redisspider "dmoz.org"

(2) 设置爬虫--- vim my_redisspider.py    

# -*- coding: utf-8 -*-# 变化 1: 从 scrapy_redis.spiders 中引入 RedisSpider
from scrapy_redis.spiders import RedisSpiderclass TestSpider(RedisSpider):  # 变化 2 : 爬虫类所继承的父类变为 RedisSpider类name = 'test_spider'# 变化 3 : 多了一个 redis_key,爬虫从这个 redis_key 里面取任务redis_key = "start_urls:test_spider"def __init__(self):super(TestSpider, self).__init__()def parse(self, response):# 直接将 name/url 存入 Redis数据库temp = {'name': response.css('.site-title::text').extract_first(),'url': response.url,}return tempif __name__ == '__main__':from scrapy import cmdlinecmdline.execute('scrapy crawl test_spider'.split())pass

(3) 执行爬虫方法

  • <1> 将项目代码复制到多台 slave 上 ( 可更改爬虫名 ) 并启动爬虫 --- scrapy runspider my_redisspider.py
  • <2> 往 redis 里面添加任务 --- lpush start_urls:test_spider http://www.dmoz.org/

 

类型三:( Scrapy-Redis 的 RedisCrawlSpider 分布式爬虫 )

基于 RedisCrawlSpider 类的 Scrapy-Redis 分布式爬虫

(1) 生成爬虫 --- scrapy genspider -t crawl my_rediscrawlspider "dmoz.org"
(2) 设置爬虫 --- vim my_rediscrawlspider.py   

# -*- coding: utf-8 -*-from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule# 变化1: 从 scrapy_redis.spiders 中引入 RedisCrawlSpider
from scrapy_redis.spiders import RedisCrawlSpider# 变化2: 爬虫类所继承的父类变为 RedisCrawlSpider类
class TestSpider(RedisCrawlSpider):name = 'test_spider'# 变化3: 多了一个 redis_key,爬虫从这个 redis_key 里面取任务redis_key = 'start_urls:test_spider'rules = [Rule(LinkExtractor(), callback='parse_page', follow=True), ]def __init__(self):super(TestSpider, self).__init__()self.temp = Nonedef parse_page(self, response):self.temp = Nonedata = {'name': response.css('.site-title::text').extract_first(),'url': response.url,  # 直接将name/url存入Redis数据库}return dataif __name__ == '__main__':from scrapy import cmdlinecmdline.execute('scrapy crawl test_spider'.split())pass

(3) 执行爬虫方法 

  • <1> 将项目代码复制到多台 slave上(可更改爬虫名)并启动爬虫 --- scrapy runspider my_rediscrawlspider.py
  • <2> 往 redis 里面添加任务 --- lpush start_urls:test_spider http://www.dmoz.org/

 

小结:                             

  • 如果只想使用 Redis 的去重和保存功能 ,使 用类型一
  • 如果写分布式,根据情况选择 类型二/类型三
  • 如果写聚焦爬虫(全站式爬虫),选择 类型三

 

 

2.Scrapy-Rdis-project: youyuan  

 

 (Scrapy-Redis 分布式爬虫框架进阶1----有缘网:非分布式基于CrawlSpider的scrapy项目  )

1.创建项目

        scrapy startproject youyuan

youyuan/
├── scrapy.cfg
└── youyuan
    ├── __init__.py
    ├── items.py
    ├── middlewares.py
    ├── pipelines.py
    ├── settings.py
    └── spiders
        ├── __init__.py

        └── yy.py

生成的项目目录截图

2.明确目标

        vim items.py

improt scrapy                             class YouyuanItem(scrapy.Item):username = scrapy.Field()       # 用户名age = scrapy.Field()            # 年龄header_url = scrapy.Field()     # 头像地址image_url = scrapy.Field()      # 相册个图片地址content = scrapy.Field()        # 内心独白place_from = scrapy.Field()     # 籍贯education = scrapy.Field()      # 教育hobby = scrapy.Field()          # 爱好source_url = scrapy.Field()     # 个人主页source = scrapy.Field()         # 数据来源网站

3.制作爬虫  

(1)生成爬虫--- scrapy genspider -t crawl yy "youyuan.com"
(2)设置爬虫--- vim yy.py

import scrapy
from scrapy.linkextractor import LinkExtractor
from scrapy.Spiders import CrawlSpider,Rule
from youyuan.items import YouyuanItem
import reclass YySpider(CrawlSpider):name = "yy"allowed_domains = ['youyuan.com']start_urls = ["http://www.youyuan.com/find/beijing/mm18-25/advance-0-0-0-0-0-0-0/p1/"]# 获取每个显示页的连接page_links = LinkExtractor(allow=(r'youyuan.com/find/beijing/mm18-25/advance-0-0-0-0-0-0-0/p\d+/')) # 获取个人主页person_links = LinkExtractor(allow =(r'youyuan.com/\d+-profile/'))rules = ( Rule(page_links),        # 没有callback/没有follow,默认follow=True继续跟进Rule(person_links,callback='parse_item'),   # 有callback/没有follow,默认follow=False不继续跟进)def parse_item(self,response):item = YouyuanItem()item['username'] = self.get_username()item['age'] = self.get_age()item['header_url'] = self.get_header_url()item['image_url'] = self.get_image_url()item['content'] = self.get_content()item['place_from'] = self.get_place_from()item['education'] = self.get_education()item['hobby'] = self.get_hobby()item['source_url'] = self.get_source_url()item['source'] = self.get_source()#-----所谓不再使用土枪土炮,看起来高大上的方法----------def get_username(self.response):username = response.xpath('//dl[@class="person_cen"]//div[@class="main"]/strong/text()').extract()if len(username):username = username[0]else:username = "Null"return username.strip()def get_age(self,response):age = response.xpath('//dl/[@class="person_cen"]//dd/p/text()').extract()if len(age):age = re.find_all(u"\d+岁",age[0])[0]else:age = "Null"return age.strip()def get_header_url(self,response):header_url = response.xpath('//dl[@class="person_cen"]//dt/img/@src').extract()if len (header_url):header_url = header_url[0]else:header_url = "Null"return header_url.strip()def get_image_url(self,response):image_url = response.xpath('//div[@class="ph_show"]/ul/li/a/img/@src').extract()if len(image_url):image_url = "|".join(image_url)  # 这样生成这种形式:xxxx|xxxx|xxxx|else:image_url = "Null"return image_urldef get_content(self,response):content = response.xpath('//div[@class="pre_data"]/ul/li[2]//ol[1]/span/text()').extract()if len(content):content = content[0]else:content = "Null"return content.strip()def get_place_from(self,response):place_from = response.xpath('//div[@class="pre_data"]/ul/li[2]//ol[2]/li[1]/span/text()').extract()if len(place_from):place_from = place_from[0]else:place_from = "Null"return place_from.strip()def get_education(self,response):education = response.xpath('//div[@class="pre_data"]/ul/li[3]//ol[2]//li[2]/span/text()').extract()if len(education):education = education[0]else:education = "Null"return education.strip()def get_hobby(self,response):hobby = response.xpath('//dl[@class="person_cen"]//ol/li/text()').extract()if len(hobby):hobby = ",".join(hobby).replace(" ","")else:hobby = "Null"return hobby.strip()

4.编写 item pipeline

        vim pipelines.py

import json                             class YouyuanJsonPipeline(obeject):def __init__(self):self.f = open("youyuan.json","w")def process_item(self,item,spider):text = json.dumps(dict(item),ensure_ascii=False) + ",\n"self.f.write(item)def close_spider(self,spider):self.f.close()

5.启动上述 pipeline---  vim settings.py

    vim settings.py
        ITEM_PIPELINES = {"youyuan.pipelines.YouyuanJsonPipeline":300}
                             
6.执行爬虫--- scrapy crawl yy

 

 

 

3.Scrapy-Redis-project: youyuan  

 

(Scrapy-Redis分布式爬虫框架进阶2----有缘网:非分布式基于CrawlSpider的scrapy项目的数据存入本机Redis  )

说明:仅仅实在上述scrapy项目的基础上进行settings.py文件的点滴修改,增加一个RedisPipeline而已 
<----并不属于Scrapy-Redis分布式

youyaun/
├── scrapy.cfg
└── youyaun
    ├── __init__.py
    ├── items.py
    ├── middlewares.py
    ├── pipelines.py
    ├── settings.py     <----仅对settings.py文件做部分添加修改
    └── spiders
        ├── __init__.py
        └── yy.py

5.settings.py添加部分信息--- 启用Scrapy-Redis的去重组件/调度器/取出请求方式,以及注册RedisPipeline组件(让数据存入Redis)

DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"  # 使用Scrapy-Redis的去重组件,不再使用scrapy的去重组件
SCHEDULER = "scrapy_redis.scheduler.Scheduler"              # 使用Scrapy-Redis的调度器,不再使用scrapy的调度器
# 使用Scrapy-Redis的从请求集合中取出请求的方式,三种方式择其一即可:
SCHEDULER_CLASS = "scrapy_redis.queue.SpiderPriorityQueue"  
# 分别按(1)请求的优先级/(2)队列FIFO/(3)栈FILO 取出请求
#SCHEDULER_CLASS = "scrapy_redis.queue.SpiderQueue"         
#SCHEDULER_CLASS = "scrapy_redis.queue.SpiderStack"
# 允许暂停,redis请求记录不会丢失(重启爬虫不会重头爬取已爬过的页面)
SCHEDULER_PERSIST = True                                    # 这两项是Redis连接设置,注释或不写默认将数据存放到本机的Redis中
#REDIS_HOST = "200.200.200.200"                             #REDIS_PORT = 6379              #----------注册RedisPipeline/自定义pipeline------------------
#注意:自定义pipeline的优先级需高于Redispipeline,因为RedisPipeline不会返回item,
#所以如果RedisPipeline优先级高于自定义pipeline,那么自定义pipeline无法获取到item
ITEM_PIPELINES = {"youyuan.pipelines.YouyuanJsonPipeline":300,        # 自定义pipeline视情况选择性注册(可选)# 将RedisPipeline注册到pipeline组件中(这样才能将数据存入Redis)"scrapy_redis.pipelines.RedisPipeline":400          }    

6.执行爬虫--- scrapy crawl yy

# 注意: 在原始scrapy项目的基础上,在settings.py文件中添加上述几行设置,就可以将scrapy爬取的数据存放到本机redis中
# 注意: 上述要想成功保存到本机Redis,有两个前提:本机必须(pip install scrapy-redis) 和本机redis必须启动(redis-server /etc/redis.conf)

 

 

4.Scrapy-Rdis-project: youyuan  

(Scrapy-Redis分布式爬虫框架进阶3----有缘网:非分布式基于CrawlSpider的scrapy项目 ----> 改写为:RedisCrawlSpider类的Scrapy-Redis分布式爬虫项目 )

说明:仅仅实在原始scrapy项目的基础上对settings.py/yy.py文件进行的点滴修改即可
    youyaun/
    ├── scrapy.cfg
    └── youyaun
        ├── __init__.py
        ├── items.py
        ├── middlewares.py
        ├── pipelines.py
        ├── settings.py     <----对settings.py文件做部分添加修改
                                            (使用Scrapy-Redis的去重组件/调度器/取出请求策略
          /允许暂停/指明远程Redis主机,并注册RedisPipeline组件)
        └── spiders
            ├── __init__.py
            └── yy.py       <----对爬虫文件进行部分修改(引入RedisCrawlSpider爬虫类/去掉allowed_domain/去掉start_urls/增加redis_key/改写init方法动态获取限制域)

1.修改设置文件--- vim settings.py   

DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"  # 使用Scrapy-Redis的去重组件,不再使用scrapy的去重组件
SCHEDULER = "scrapy_redis.scheduler.Scheduler"              # 使用Scrapy-Redis的调度器,不再使用scrapy的调度器
# 使用Scrapy-Redis的从请求集合中取出请求的方式,三种方式择其一即可:
SCHEDULER_CLASS = "scrapy_redis.queue.SpiderPriorityQueue"  #SCHEDULER_CLASS = "scrapy_redis.queue.SpiderQueue"         # 分别按(1)请求的优先级/(2)队列FIFO/(3)栈FILO 取出请求
#SCHEDULER_CLASS = "scrapy_redis.queue.SpiderStack"
# 允许暂停,redis请求记录不会丢失(重启爬虫不会重头爬取已爬过的页面)
SCHEDULER_PERSIST = True                                    REDIS_HOST = "200.200.200.200"    # 这两项是Redis连接设置,如果注释或不写会默认将数据存放到本机的Redis中
REDIS_PORT = 6379      # 注意:master端的Redis需要允许远程连接--配置中注释掉bind 127.0.0.1         #----------注册RedisPipeline/自定义pipeline------------------
#注意:自定义pipeline的优先级需高于Redispipeline,因为RedisPipeline不会返回item,
#所以如果RedisPipeline优先级高于自定义pipeline,那么自定义pipeline无法获取到item
ITEM_PIPELINES = {"youyuan.pipelines.YouyuanJsonPipeline":300,  # 自定义pipeline视情况选择性注册(可选)"scrapy_redis.pipelines.RedisPipeline":400    # 将RedisPipeline注册到pipeline组件中(这样才能将数据存入Redis)
}    

2. 修改爬虫文件--- vim yy.py

import scrapy
from scrapy linkextractor import LinkExtractor
from scrapy.Spiders import CrawlSpider,Rule
from youyuan.items import YouyuanItem
import re
from scrapy_redis.Spiders import RedisCrawlSpider    # 变化1:从scrapy_redis.Spiders中引入RedisCrawlSpiderclass YySpider(RedisCrawlSpider):          # 变化2:爬虫类所继承的父类变为RedisCrawlSpider类name = "yy"redis_key = "yyspider:start_urls"   # 变化3:多了一个对所有爬虫发号施令的redis_key,少了allowed_domain和start_urlsdef __init__(self,*args,**kwargs):                      # 变化4:重写__init__方法:动态获取限制域domain = kwargs.pop('domain','')self.allowed_domain = filter(None,domain,split(','))super(YySpider,self).__init__(*args,**kwargs)       # 注意super()里面的参数因爬虫类名不同而不同page_links = LinkExtractor(allow=(r'youyuan.com/find/beijing/mm18-25/advance-0-0-0-0-0-0-0/p\d+/')) person_links = LinkExtractor(allow =(r'youyuan.com/\d+-profile/'))  ..............      # 后面的代码都相同

3.爬虫的执行方式改变
       <1>将项目代码复制到给台slave上(可更改爬虫名)并启动爬虫--- scrapy runspider yy.py
   <2>在master端redis上发号施令--- lpush yyspider:start_urls http://www.youyuan.com/find/beijing/mm18-25/advance-0-0-0-0-0-0-0/p1/    

 

 

 

 

5.Scrapy-Redis-project: sina2  

(Scrapy-Redis分布式爬虫框架----新浪分类资讯:非分布式基于scrapy.Spider的scrapy项目 ----> 改写为:RedisSpider类的Scrapy-Redis分布式爬虫项目 )  改写该项目注意:由于改写后数据存放到Redis,所以需要去掉"目录存储路径"相关的代码

1.创建项目--- scrapy startproject sina2
        sina2/
        ├── scrapy.cfg
        └── sina2
            ├── __init__.py
            ├── items.py
            ├── middlewares.py
            ├── pipelines.py
            ├── settings.py     <----对settings.py文件做部分添加修改(使用Scrapy-Redis的去重组件/调度器/取出请求策略/
                                     允许暂停/指明远程Redis主机,并注册RedisPipeline组件)
            └── spiders
                ├── __init__.py
                └── xinlang.py  <----对爬虫文件进行部分修改(引入RedisCrawlSpider爬虫类/去掉allowed_domain
                                     /去掉start_urls/增加redis_key/改写init方法动态获取限制域)

2.明确目标--- vim items.py 
    vim items.py
        import scrapy

class SinaItem(scrapy.Item):parent_title = scrap.Field()       # 大类标题parent_url = scrap.Field()         # 大类URLsub_title = scrap.Field()          # 小类标题sub_url = scrap.Field()            # 小类URL#sub_filename = scrapy.Field()     # 小类目录存储路径(数据存放到Redis,不在需要这块代码)son_url = scrap.Field()            # 小类的子链接article_title = scrap.Field()      # 文章标题article_content = scrap.Field()    # 文章内容

 

3.制作爬虫

(1)生成爬虫--- scrapy genspider xinlang "sina.com.cn"
(2)设置爬虫--- vim xinlang.py

vim xinlang.pyimport scrapyimport osfrom sina.items import SinaItemimport sysfrom scrapy_redis.Spiders import RedisSpider                 ###<-----变化1:从scrapy_redis.Spiders中引入RedisSpiderreload(sys)sys.setdefaultencoding('utf-8')class XinlangSpider(RedisSpider):         ###<-----变化2:爬虫类所继承的父类变为RedisSpider类name = 'xinlang'              ##<-----变化3:多了一个对所有爬虫发号施令的redis_key,少了allowed_domain和start_urls                               redis_key = "xinlangspider:start_urls"                   def __init__(self,*args,**kwargs):                       ###<-----变化4:重写__init__方法:动态获取限制域domain = kwargs.pop('domain','')self.allowed_domain = filter(None,domain,split(','))super(XinlangSpider,self).__init__(*args,**kwargs)   # 注意super()里面的参数因爬虫类名不同而不同     def parse(self,response):items = []parent_title_list = response.xpath('//div[@id=\"tab01\"]/div/h3/a/text()').extract()parent_url_list = response.xpath('//div[@id=\"tab01\"]/div/h3/a/@href').extract()sub_title_list = response.xpath('//div[@id=\"tab01\"]/div/ul/li/a/text()').extract()sub_url_list = response.xpath('//div[@id=\"tab01\"]/div/ul/li/a/@href').extract()for i in range(0,len(parent_title_list)):   # 创建大类的存放目录(若存在则不创建,若不存在则重新创建)#parent_filename = "./Data/" + parent_title_list[i]   #if(not os.path.exists(parent_filename)):     # (数据存放到Redis,不在需要这块代码)#   os.makedirs(parent_filename) for j in range(0,len(sub_url_list)):           # 实例化SinaItem()并保存大类的URL和标题item = SinaItem()item['parent_title'] = parent_title_list[i]item['parent_url'] = parent_url_list[i]# 判断小类URL是否以大类URL开头(即判断小类是否属于大类)if_belong = sub_url_list[i].startwith(item['parent_url']) if (if_belong):                                             # 如果属于该大类,则判断小类存放目录是否存在,不存在则新建该目录#sub_filename = parent_filename + "/" + sub_title_list[j]   # (数据存放到Redis,不在需要这块代码)#if (not os.path.exists(sub_filename)):                     #   os.makedirs(sub_filename)# 保存小类的标题/URL/存放目录,并将目前所获取item信息追加到items列表中保存item['sub_title'] = sub_title_list[j]       item['sub_url'] = sub_url_list[j]item['sub_filename'] = sub_filenameitems.append(item)# 逐一取出子类的url,并附带上meta信息(即item),将其加入请求队列,使用second_parse()函数处理其返回的响应for item in items:              yield scrapy.Request(url=item['sub_url'],meta={'meta_1':item},callback=self.second_parse)def second_parse(self,response):meta_1 = response.meta['meta_1']                      # 将meta对应的item信息赋值给meta_1(即,meta_1 = item)son_url_list = response.xpath('//a/@href').extract()  # 匹配获取返回的孙类的URL列表items = []# 循环取出孙类URL判断其是否属于某个大类(以大类的URL开头)和是否是文章(以.shml结尾),# 如果属于则将该孙类URL保存起来for i in range(0,len(son_url_list)):    if_belong = son_url_list[i].endwith('.shtml') and sub_url_list[i].startwith(meta_1['parent_url'])if (if_belong):item = SinaItem()item['parent_title'] = meta_1['parent_title']item['parent_url'] = meta_1['parent_url']item['sub_title'] = meta_1['sub_title']item['sub_url'] = meta_1['sub_url']#item['sub_filename'] = meta_1['sub_filename']  # (数据存放到Redis,不在需要这块代码)item['son_url'] = son_url_list[i]items.append(item)# 逐一取出孙类的url,并附带上meta信息(即第二次的item),将其加入请求队列,使用third_parse()函数处理其返回的响应for item in items:              yield scrapy.Request(url=item['son_url'],meta={'meta_2':item},callback=self.third_parse)def third_parse(self,response):item = response.meta['meta_2']  # 将meta对应的(第二次获取更新的item信息)赋值给这里的item(即,item = item)article_content = ""            # 从孙类URL返回响应中匹配出文章标题和文章内容并保存进itemarticle_title_list = response.xpath('//hi[@id=\"main_title\"]/text()').extract()article_content_list = response.xpath('//div[@id=\"artibody\"]/p/text()').extract()for content_part in article_content_list:article_content += content_part               # 通过循环拼接成完整的文章内容item['article_title'] = article_title_list[0]item['article_content'] = article_contentyield item                                        # 将数据收集完整的item传递给pipeline处理

4.编写item pipelines

--- vim pipelines.py (忽略)  # (数据存放到Redis,不再需要这块代码,不需要自定义pipeline保存数据到本地)

 

5.启用上述pipeline组件--- vim settings.py

DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"  # 使用Scrapy-Redis的去重组件,不再使用scrapy的去重组件
SCHEDULER = "scrapy_redis.scheduler.Scheduler"              # 使用Scrapy-Redis的调度器,不再使用scrapy的调度器
# 使用Scrapy-Redis的从请求集合中取出请求的方式,三种方式择其一即可:
SCHEDULER_CLASS = "scrapy_redis.queue.SpiderPriorityQueue"  #SCHEDULER_CLASS = "scrapy_redis.queue.SpiderQueue"    # 分别按(1)请求的优先级/(2)队列FIFO/(3)栈FILO 取出请求
#SCHEDULER_CLASS = "scrapy_redis.queue.SpiderStack"
SCHEDULER_PERSIST = True           # 允许暂停,redis请求记录不会丢失(重启爬虫不会重头爬取已爬过的页面)REDIS_HOST = "200.200.200.200"     # 这两项是Redis连接设置,如果注释或不写会默认将数据存放到本机的Redis中
REDIS_PORT = 6379                  # 注意:master端的Redis需要允许远程连接--配置中注释掉bind 127.0.0.1         # (数据存放到Redis,不再需要这块代码,不需要自定义pipeline保存数据到本地)
ITEM_PIPELINES = {  #"sina.pipelines.SinaSavePipeline":300,     # 将RedisPipeline注册到pipeline组件中(这样才能将数据存入Redis)"scrapy_redis.pipelines.RedisPipeline":400  
}

6.爬虫的执行方式改变

 

<1>将项目代码复制到给台slave上(可更改爬虫名)并启动爬虫--- scrapy runspider xinlang.py
<2>在master端redis上发号施令--- lpush yyspider:start_urls http://news.sina.com.cn/guide/   

 

 

 

6.Scrapy-Rdis-project: youyuan 

 (Scrapy-Redis分布式爬虫框架----将Redis中数据持久化存储到MongoDB/MySQL中----> 将有缘网分布式爬取到Redis中的数据转存到MongoDB/MySQL中)

要将Scrapy-Redis项目爬取到Redis中的数据转存到Mongodb/MySQL中,只需要在项目一级目录下创建两个转存的脚本文件即可

有缘网Scrapy-Redis项目树形图
        youyuan/
        ├── scrapy.cfg
        ├── process_item_for_mongodb.py     <----项目一级目录下创建将Redis数据转存到mongodb的python脚本
        ├── process_item_for_mysql.py       <----项目一级目录下创建将Redis数据转存到mysql的python脚本
        └── youyuan
            ├── __init__.py
            ├── items.py
            ├── middlewares.py
            ├── pipelines.py
            ├── settings.py 
            └── spiders
                ├── __init__.py
                └── yy.py   

1.将Redis数据转存到mongodb中 ----- vim process_item_for_mongodb.py

#!/usr/bin/env python
#coding:utf-8import redis
import pymongo
import json
def process_item():rediscli = redis.Redis(host='200.200.200.200',port=6379,db=0)       # 创建Redis连接对象mongocli = pymongo.MongoClient(host='200.200.200.202',port=27017)   # 创建MongoDB连接对象db_name = mongocli['youyuan']     # 利用MongoDB连接对象在MongoDB中创建名为youyuan的数据库对象sheet_name = db_name['beijing18-24']  # 利用该数据库对象在youyuan数据库中创建名为beijing18-24的表对象count = 0 while True:# 使用循环通过redis连接对象的blpop()方法,不断取出redis中的数据(blpop即FIFO,rlpop即FILO)source,data = rediscli.blpop("yy:items")                        data = json.loads(data)   # 将取出的json字符串类型的数据转化为python类型的对象sheet_name.insert(data)   # 利用mongodb的表对象的insert()方法,向表中插入(刚才转化的python对象)count += 1print "已经成功从redis转移" + str(count) + "条数据到mongodb"if __name__ == "__main__":process_item()

注意:MongoDB中可以自动创建键,所以直接执行该脚本就可转移数据

 

2.将Redis数据转存到mysql中 ----- vim process_item_for_mysql.py

import redis
import MySQLdb
import json
def process_item():rediscli = redis.Redis(host='200.200.200.200',port=6379,db=0)     # 创建Redis连接对象# 创建MySQL连接对象mysqlcli = MySQLdb.connect(host='200.200.200.204',port 3306,db='youyuan',user='zhangsan',password='123456') count = 0while True:# 使用循环通过redis连接对象的blpop()方法,不断取出redis中的数据(blpop即FIFO,rlpop即FILO)source,data = rediscli.blpop('yy:items')           data = json.loads(data)      # 将取出的json字符串类型的数据转化为python类型的对象try:# 利用mysql连接对象创建cursor操作游标,并使用该操作游标向Mysql表中插入数据,数据通过python对象获取其值cursor = mysqlcli.cursor()                      cursor.execute("insert into beijing18-24 (username,age,spider,crawled)values(%s,%s,%s,%s)",[data['username'],data['age'],data['spider'],data['crawled']])mysqlcli.commit()                               # 插入完成后需提交事务cursor.close()                                  # 关闭操作游标count += 1print "已经成功从redis转移" + str(count) + "条数据到mongodb"except:passif __name__ == "__main__":process_item()

注意:MySQL中不能自动创建字段,所以在执行该脚本前,需要自行在数据库中创建好响应的数据库/表/字段,然后才能执行该脚本,转移数据到MySQL中

 

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/495913.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java8 Stream详解~归约(reduce)

归约&#xff0c;也称缩减&#xff0c;顾名思义&#xff0c;是把一个流缩减成一个值&#xff0c;能实现对集合求和、求乘积和求最值操作。 「案例一&#xff1a;求Integer集合的元素之和、乘积和最大值。」 public class StreamTest {public static void main(String[] args) …

人工智能除了创造新材料还能预测化学反应性能

来源&#xff1a; 材料牛摘要&#xff1a; 在材料化学领域人工智能也在发挥着越来越重要的作用&#xff0c;往往研究人员想尽脑汁做不出来的东西它可以经过成千上万次的计算给出最优答案。【引言】机器学习方法正在成为众多学科科学探究的一部分。 机器学习&#xff08;ML&…

推荐|深度学习领域引用量最多的前20篇论文简介

来源&#xff1a;全球人工智能作者&#xff1a;Pedro Lopez&#xff0c;数据科学家&#xff0c;从事金融与商业智能。译者&#xff1a;海棠&#xff0c;审阅&#xff1a;袁虎。深度学习是机器学习和统计学交叉领域的一个子集&#xff0c;在过去的几年里得到快速的发展。强大的开…

Java8 Stream详解~收集(collect)

collect&#xff0c;收集&#xff0c;可以说是内容最繁多、功能最丰富的部分了。从字面上去理解&#xff0c;就是把一个流收集起来&#xff0c;最终可以是收集成一个值也可以收集成一个新的集合。 1 归集(toList/toSet/toMap) 因为流不存储数据&#xff0c;那么在流中的数据完…

英国上议院AI报告:没中美有钱,但我可以主导道德游戏规则设定

来源&#xff1a;网络大数据随着全球各国政府纷纷计划推出 AI 驱动下的未来&#xff0c;英国正准备承担一些学术和道德上的责任。最近&#xff0c;英国上议院 (House of Lords) 发布了一份183页的 报告《AI in the UK: ready, willing and able?》(《人工智能在英国&#xff1…

Java8 Stream详解~ 提取/组合

流也可以进行合并、去重、限制、跳过等操作。 public class StreamTest {public static void main(String[] args) {String[] arr1 { "a", "b", "c", "d" };String[] arr2 { "d", "e", "f", "g&…

Scrapy 下载器 中间件(Downloader Middleware)

Scrapy 下载器中间件官方文档&#xff1a;https://scrapy-chs.readthedocs.io/zh_CN/1.0/topics/downloader-middleware.html 官方 英文 文档&#xff1a;http://doc.scrapy.org/en/latest/topics/downloader-middleware.html#topics-downloader-middleware Scrapy 扩展中间件…

15 个 JavaScript Web UI 库

转载http://news.csdn.net/a/20100519/218442.html 几乎所有的富 Web 应用都基于一个或多个 Web UI 库或框架&#xff0c;这些 UI 库与框架极大地简化了开发进程&#xff0c;并带来一致&#xff0c;可靠&#xff0c;以及高度交互性的用户界面。本文介绍了 15 个非常强大的 Java…

2018年技术展望--中文版

来源&#xff1a;199IT互联网数据中心每年&#xff0c;《埃森哲技术展望》报告融合顶尖技术研究团队、行业领袖以及全球数据调研结果&#xff0c;发布未来三年内或将对各行各业产生重大影响的技术趋势判断&#xff0c;作为企业布局新战略的指导。2018年的《埃森哲技术展望》报告…

彻底搞懂 Scrapy 的中间件

彻底搞懂Scrapy的中间件&#xff08;一&#xff09;&#xff1a;https://www.cnblogs.com/xieqiankun/p/know_middleware_of_scrapy_1.html 彻底搞懂Scrapy的中间件&#xff08;二&#xff09;&#xff1a;https://www.cnblogs.com/xieqiankun/p/know_middleware_of_scrapy_2.h…

华为:5G技术前景堪忧,运营商将很难从5G赚钱

来源&#xff1a;FT中文网、5G作者&#xff1a;卢卡斯、法尔兹丨英国《金融时报》。未来智能实验室是人工智能学家与科学院相关机构联合成立的人工智能&#xff0c;互联网和脑科学交叉研究机构。未来智能实验室的主要工作包括&#xff1a;建立AI智能系统智商评测体系&#xff0…

解决log4j多个日志都写到一个文件

之前客户端程序由于Websockt包依赖的log4j&#xff0c;就用log4j写日志了&#xff0c;Web用的log4j2没毛病。用log4j的多个logger的日志都写到一个文件里了&#xff0c;查了很多资料都没解决。今天闲了解决一下。 最后好使的配置 # 设置日志根 log4j.rootLogger INFO,Except…

Scrapy 爬虫去重效率优化之 Bloom Filter的算法的对接

From&#xff1a;https://cloud.tencent.com/developer/article/1084962 Python分布式爬虫打造搜索引擎Scrapy精讲—将bloomfilter(布隆过滤器)集成到scrapy-redis中https://www.cnblogs.com/adc8868/p/7442306.html scrapy redis bloomfilter &#xff1a;https://github.co…

为什么 AI 工程师要懂一点架构?

作者 | 王咏刚&#xff08;公众号ID&#xff1a;ban-qing-ren&#xff09;AI 时代&#xff0c;我们总说做科研的 AI 科学家、研究员、算法工程师离产业应用太远&#xff0c;这其中的一个含义是说&#xff0c;搞机器学习算法的人&#xff0c;有时候会因为缺乏架构&#xff08;In…

CompletableFuture详解~CompletionStage

CompletableFuture 分别实现两个接口 Future与 CompletionStage。 Future 接口大家都比较熟悉&#xff0c;这里主要讲讲 CompletionStage。 CompletableFuture 大部分方法来自CompletionStage 接口&#xff0c;正是因为这个接口&#xff0c;CompletableFuture才有如此强大功能…

Python 异步 IO 、协程、asyncio、async/await、aiohttp

From &#xff1a;廖雪峰 异步IO &#xff1a;https://www.liaoxuefeng.com/wiki/1016959663602400/1017959540289152 Python Async/Await入门指南 &#xff1a;https://zhuanlan.zhihu.com/p/27258289 Python 生成器 和 yield 关键字&#xff1a;https://blog.csdn.net/free…

智能语音简史:这场技术革命从哪开始?

来源&#xff1a;与非网1952年&#xff0c;贝尔实验室&#xff08;Bell Labs&#xff09;制造一台6英尺高自动数字识别机“Audrey”&#xff0c;它可以识别数字0&#xff5e;9的发音&#xff0c;且准确度高达90&#xff05;以上。并且它对熟人的精准度高&#xff0c;而对陌生人…

精益创业~如何驾驭愿景

开发-测量-认知 反馈循环 循环中把总时间缩至最短 要把科学的方法运用到新创企业中&#xff0c;我们必须找到哪些假设是需要测试的。这是新创企业计划中风险最大的部分&#xff0c;这部分内容依赖 信念飞跃 Leap-of-Faith 式的大胆假设。其中最重要的两个假设是 价值假设 和 增…

Python 中 异步协程 的 使用方法介绍

静觅 崔庆才的个人博客&#xff1a;Python中异步协程的使用方法介绍&#xff1a;https://cuiqingcai.com/6160.html Python 异步 IO 、协程、asyncio、async/await、aiohttp&#xff1a;https://blog.csdn.net/freeking101/article/details/85286199 1. 前言 在执行一些 IO 密…

半导体终极武器光刻机:为何中国难望ASML项背?!有了全套图纸也做不出来

来源&#xff1a; XuS风险创投行指甲盖大小的芯片&#xff0c;密布千万电线&#xff0c;纹丝不乱&#xff0c;需要极端精准的照相机——光刻机。光刻机精度&#xff0c;决定了芯片的上限。EUV半导体业的终极武器这全靠总部后头那栋最高机密的巨型厂房&#xff0c;里头身穿无尘衣…