1. 分布式爬虫原理
Scrapy单机爬虫有一个本地爬取队列Queue,如果新的Request生成就会放到队列里面,随后Request被Scheduler调度,之后Request交给Downloader执行。分布式爬虫有多个Scheduler和多个Downloader,而爬取队列始终为一个,也就是共享爬取队列,这样才能保证Scheduler从队列里调度某个Request之后,其他的Scheduler不会重复调取此Request,就可以做到多个Scheduler同步爬取。
我们需要做的就是在多台主机上同时运行爬虫任务,共享一个爬取队列,各台主机有自己的Scheduler和Downloader,这个共享的爬取队列就是使用Redis来完成的。
2.scrapy-redis 源码解析
Github地址:https://github.com/rmax/scrapy-redis/tree/master/src
各个组件功能介绍。
#####2.1 connection.py
负责根据setting中配置实例化redis连接。被dupefilter和scheduler调用,总之涉及到redis存取的都要使用到这个模块。
# 这里引入了redis模块,这个是redis-python库的接口,用于通过python访问redis数据库,
# 这个文件主要是实现连接redis数据库的功能,这些连接接口在其他文件中经常被用到
import redis
import six
from scrapy.utils.misc import load_object
DEFAULT_REDIS_CLS = redis.StrictRedis
# 可以在settings文件中配置套接字的超时时间、等待时间等
# Sane connection defaults.
DEFAULT_PARAMS = {
'socket_timeout': 30,
'socket_connect_timeout': 30,
'retry_on_timeout': True,
}
# 要想连接到redis数据库,和其他数据库差不多,需要一个ip地址、端口号、用户名密码(可选)和一个整形的数据库编号
# Shortcut maps 'setting name' -> 'parmater name'.
SETTINGS_PARAMS_MAP = {
'REDIS_URL': 'url',
'REDIS_HOST': 'host',
'REDIS_PORT': 'port',
}
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
``DEFAULT_PARAMS`` global as defaults values for the parameters. You can
override them using the ``REDIS_PARAMS`` setting.
Parameters
----------
settings : Settings
A scrapy settings object. See the supported settings below.
Returns
-------
server
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_PARAMS : dict, optional
Additional client parameters.
"""
params = DEFAULT_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'])
# 返回的是redis库的Redis对象,可以直接用来进行数据操作的对象
return get_redis(**params)
# Backwards compatible alias.
from_settings = get_redis_from_settings
def get_redis(**kwargs):
"""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 = kwargs.pop('redis_cls', DEFAULT_REDIS_CLS)
url = kwargs.pop('url', None)
if url:
return redis_cls.from_url(url, **kwargs)
else:
return redis_cls(**kwargs)
#####2.2 duperfilter.py
负责执行Request的去重。Scrapy单机去重的过程就是利用集合元素的不重复性来实现,有一个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)
检测指纹是否存在于fingerprints变量中,该变量为一个集合,如果指纹存在就返回True,否则把这个指纹加入到集合中。
对于分布式爬虫来说,我们可以利用redis得集合作为指纹集合,那么这样去重集合也是利用Redis共享的。每台主机将新生成的Request指纹与集合对比,如果指纹已经存在,就说明Request是重复的。这样,利用同样的原理不同的存储结构就实现了分布式Request的去重。
duperfilter.py
import logging
import time
from scrapy.dupefilters import BaseDupeFilter
from scrapy.utils.request import request_fingerprint
from .connection import get_redis_from_settings
DEFAULT_DUPEFILTER_KEY = "dupefilter:%(timestamp)s"
logger = logging.getLogger(__name__)
# TODO: Renam