scrapy-redis
为什么要用scrapy-redis?
Scrapy_redis在scrapy的基础上实现了更多,更强大的功能,具体体现在:reqeust去重,爬虫持久化,和轻松实现分布式,实现一个任务多台服务器执行,大大的提高了效率
安装
pip3 install scrapy-redis
scrapy-redis 执行流程
源码
scrapy-redis .connrction
connect 文件引入了redis 模块,这个是 redis-python库的接口,用于通过python访问redis数据库,可见,这个文件主要是实现连接redis数据库的功能(返回的是redis库的Redis对象或者StrictRedis对象,这俩都是可以直接用来进行数据操作的对象)。这些连接接口在其他文件中经常被用到。其中,我们可以看到,要想连接到redis数据库,和其他数据库差不多,需要一个ip地址、端口号、用户名密码(可选)和一个整形的数据库编号,同时我们还可以在scrapy工程的setting文件中配置套接字的超时时间、等待时间等。
其实这个模块的功能:
1. 从 settings 里面获取 redis 的链接配置
2. 获取 redis 的 链接 实例
import six
from scrapy.utils.misc import load_object
from . import defaults
#Shortcut maps 'setting name' -> 'parmater name'.
#redis://127.0.0.1:6379/0
SETTINGS_PARAMS_MAP = {
'REDIS_URL': 'url', #以url的方式链接数据库
'REDIS_HOST': 'host', #指定redis数据库的host
'REDIS_PORT': 'port', #指定redis数据库的port
'REDIS_ENCODING': 'encoding', #指定redis数据库的编码
}
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. You
can override them using the ``REDIS_PARAMS`` setting.
Parameters
----------
settings : Settings :scrapy设置文件
A scrapy settings object. See the supported settings below.
Returns
-------
server:redis客户端链接对象
Redis client instance.
Other Parameters
----------------
REDIS_URL : str, optional
Server connection URL.
REDIS_HOST : str, optional
Server host.
REDIS_PORT : str, optional
Server port.
REDIS_ENCODING : str, optional
Data encoding.
REDIS_PARAMS : dict, optional
Additional client parameters.
"""
params = defaults.REDIS_PARAMS.copy()
#从scrapy的设置文件中获取REDIS_PARAMS,覆盖defaults.REDIS_PARAMS的默认值
params.update(settings.getdict('REDIS_PARAMS'))
# XXX: Deprecate REDIS_* settings.
#
# SETTINGS_PARAMS_MAP = {
# 'REDIS_URL': 'url', # 以url的方式链接数据库
# 'REDIS_HOST': 'host', # 指定redis数据库的host
# 'REDIS_PORT': 'port', # 指定redis数据库的port
# 'REDIS_ENCODING': 'encoding', # 指定redis数据库的编码
# }
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'])
##调用get_redis方法返回redis数据库链接
return get_redis(**params)
#Backwards compatible alias.
from_settings = get_redis_from_settings
def get_redis(**kwargs):
# 方法返回redis数据库链接对象
"""Returns a redis client instance.
Parameters
----------
redis_cls : class, optional
Defaults to ``redis.StrictRedis``.
url : str, optional
If given, ``redis_cls.from_url`` is used to instantiate the class.
**kwargs
Extra parameters to be passed to the ``redis_cls`` class.
Returns
-------
server
Redis client instance.
"""
#redis_cls -> redis.StrictRedis
redis_cls = kwargs.pop('redis_cls', defaults.REDIS_CLS)
url = kwargs.pop('url', None)
if url:
#根据url方式创建redis数据库链接,并返回
return redis_cls.from_url(url, **kwargs)
else:
#根据host、port创建redis数据库链接,并返回
return redis_cls(**kwargs)
scrapy-redis .defaults
redis 的一些基础的默认的设置。其实就是一些默认配置:
import redis
# redis.StrictRedis()
# For standalone use.
DUPEFILTER_KEY = 'dupefilter:%(timestamp)s'
#redis数据库中保留item的key
PIPELINE_KEY = '%(spider)s:items'
#REDIS_CLS:redis客户端链接
REDIS_CLS = redis.StrictRedis
REDIS_ENCODING = 'utf-8'
# Sane connection defaults.
#链接redis数据库时设置的默认参数
REDIS_PARAMS = {
'socket_timeout': 30,
'socket_connect_timeout': 30,
'retry_on_timeout': True,
'encoding': REDIS_ENCODING,
}
#redis数据库中保存待爬取任务(request)的key
SCHEDULER_QUEUE_KEY = '%(spider)s:requests'
#默认指定了有优先级的任务队列存储方式(PriorityQueue)
SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.PriorityQueue'
#redis数据库中保留去重指纹的key
SCHEDULER_DUPEFILTER_KEY = '%(spider)s:dupefilter'
#默认设置了scrapy_redis的去重组件
SCHEDULER_DUPEFILTER_CLASS = 'scrapy_redis.dupefilter.RFPDupeFilter'
#默认起始任务的key
START_URLS_KEY = '%(name)s:start_urls'
START_URLS_AS_SET = False
scrapy-redis .dupefilter
这个主要是用来去重的。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 time
from scrapy.dupefilters import BaseDupeFilter
from scrapy.utils.request import request_fingerprint
from . import defaults
from .connection import get_redis_from_settings
logger = 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 = logger
def __init__(self, server, key, debug=False):
"""Initialize the duplicates filter.
Parameters
----------
server : redis.StrictRedis
#redis客户端链接
The redis server instance.
key : str
#redis数据库中保留去重指纹的key
Redis key Where to store fingerprints.
debug : bool, optional
Whether to log filtered requests.
"""
self.server = server
self.key = key
self.debug = debug
self.logdupes = True
@classmethod
def 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 as
it needs to pass the spider name in the key.
Parameters
----------
settings : scrapy.settings.Settings
Returns
-------
RFPDupeFilter
A 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)
@classmethod
def from_crawler(cls, crawler):
"""Returns instance from crawler.
Parameters
----------
crawler : scrapy.crawler.Crawler
Returns
-------
RFPDupeFilter
Instance of RFPDupeFilter.
"""
return cls.from_settings(crawler.settings)
def request_seen(self, request):
"""Returns True if request was already seen.
Parameters
----------
request : scrapy.http.Request #请求对象
Returns
-------
bool :True:表示已添加到任务队列, False:表示未添加到任务队列
"""
#根据request对象生成指纹
fp = self.request_fingerprint(request)
# This returns the number of values added, zero if already exists.
# 返回0:已存在集合中 返回1:指纹不存在集合中
added = self.server.sadd(self.key, fp)
return added == 0
def request_fingerprint(self, request):
"""Returns a fingerprint for a given request.
Parameters
----------
request : scrapy.http.Request
Returns
-------
str
"""
return request_fingerprint(request)
def close(self, reason=''):
"""Delete data on close. Called by Scrapy's scheduler.
Parameters
----------
reason : str, optional
"""
self.clear()
def clear(self)