背景
当你用scrapy写好一个爬虫后,惬意的坐在凳子上看它在运动,老板走过来...
老板: 东西做好了吗?
你: 做好了做好了,每分钟能爬1000条数据.
老板: 那好,你爬1000万条数据给我!
你: 好好好,没问题!
老板: 那尽快给我!
你: 不就1000w吗?我一分钟爬1000条,那就要1000w/1000/60/24= 神马?要7天? 怎么办多进程吧,数据会重复爬取,怎么办怎么办?
还能怎么办,当然是分布式了!
我们为什么要用分布式系统
从上面的故事也可以看到了,我们有两个需求:
- 快速大规模抓取
- 不重复抓取
对于多进程来说我们如果同时开启多个进程来运行爬虫那么就会造成数据的重复,对于同以条信息,因为多个个进程之间并不知道是否抓取了这个信息,就会无论如何都会抓取,这样就造成了资源的浪费,也没有提升效率.所以我们要是能改造下scrapy的调度器就好了,这样就会让爬虫同时从一个队列里取用数据!这样就避免了重复,同时也可以开很多进程来运行同一个队列里的请求,然后再将获得的需要处理的请求同时放入调度器,让调度器统一分配!那么这个队列怎么实现呢? Scrapy-redis 别人已将造好了轮子了, 我们只要理解下原理然后拿来用就好了!
Scrapy-redis 分析
这里引用崔大神的博客来说明Scrapy-redis 的实现原理,先来张图:
原理分析
先来看下scrapy单机的工作原理:
所有request对象都是由一个核心从调度器中拿出来送给downloader来进行爬取,如果我们想多台主机协同爬取,那么必须有一个共享机制-request队列,那么这个队列就应该是 统一的, 可访问的 的队列,这样多台主机才能一致的协同操作!究其原理就是 共享爬取队列 ,这样就是分布式的基本原理.
如果要做分布式爬虫首先要有一个由一台主机维护request队列的也就是master主机,从机的作用就是 数据抓取,数据处理和存储.也就是说取request是多台主机从同一个队列中取数据!
当然master的队列应该是非常 高效的,易于维护的 ,比如说数据库或者特定的数据结构来存储,那么我们推荐使用redis,原因有以下几点:
- Redis是 非关系型数据库 ,key-value的形式存储,结构非常灵活,限制比较少,key和value存储比较灵活(关系型数据库字段是定义好的,redis可以自定义key)
- Redis是 基于内存 的数据库,相对于普通存储 速度更快,性能更高
- 另外redis还提供了队列,集合(去重必备),有序集合等多种数据结构,方便存储
怎么去重?
当然是用集合,看下网上对Redis的集合的注释:Redis的Set是string类型的 无序集合. 集合成员是 唯一的 ,这就意味着集合中不能出现重复的数据.Redis中集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。集合中最大的成员数为 4294967295(每个集合可存储40多亿个成员)。官方文档详情
对于每一个request我们的redis付给他唯一的指纹,相同的request有相同的指纹,并将这个指纹存储起来. 当有新的request对象进来的时候, redis会验证这个request的指纹是否存在,如果存在就将不添加进队列,如果不存在那么就将这个request对象的的指纹存储起来,并将request添加到集合中,这样就保证了所有的队列不重复!
保证中断后续传
每台从机Scrapy在启动的时候都会首先判断当前redis request队列是否为空,如果不为空,那么就从队列中继续取request进行爬取,如果为空则重新从主机中取数据进行爬取, 有了这个机制我们再也不用担心断线后如何继续爬取的问题了!
怎么实现这个架构
- 统一的request队列
- 多台从机同时从队列中取用数据
- 去重机制
那么我们开始写一个分布式吧!
开个玩笑,还是那句话:绝不重复造轮子,除非你有更好的方案! 别人已经写好了一个库Scrapy-redis 来供我们使用,下面我们来看下这个库:
这个库改写了Scrapy的调度器,队列等组件-官方网址点我. 我们不造轮子也要知道轮子原理是什么,至少要知道轮子怎么用吧,下面来看下scrapy-redis的源码解读:
connection.py---用来连接redis数据库
负责根据setting中配置实例化redis连接.被dupefilter和scheduler调用,总之涉及到redis存取的都要使用到这个模块.
import six
from scrapy.utils.misc import load_object
from . import defaults
# Shortcut maps 'setting name' -> 'parmater name'.
SETTINGS_PARAMS_MAP = {
'''声明了redis的数据库信息,用来连接redis,也是master主机的配置,从机配置和这个一样,这样就保证了所有从机都连接同一个主机'''
'REDIS_URL': 'url',
'REDIS_HOST': 'host',
'REDIS_PORT': 'port',
'REDIS_ENCODING': 'encoding',
}
def get_redis_from_settings(settings):
'''返回一个Scrapy中设定好的redis客户端对象'''
"""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
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_ENCODING : str, optional
Data encoding.
REDIS_PARAMS : dict, optional
Additional 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'])
# 返回的是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 = 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)
defaults.py---默认配置文件
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.
# 可以在settings文件中配置套接字的超时时间、等待时间等
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
dupefilter.py ---去重操作
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
The redis server instance.
key : str
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:``. 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
"""
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 == 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):
"""Clears fingerprints data."""
self.server.delete(self.key)
def log(self, request, spider):
"""Logs given request.
Parameters
----------
request : scrapy.http.Request
spider : 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
这里面最主要的是 request_seen 方法: 判断request是否已经加入到队列中->调用request_fingerprint方法来获取当前request的指纹然后调用self.server.sadd这个方法(redis的 sadd 方法),如果插入成功那么之前不存在就将request放入队列如果不成功那么就已经存在了! 通过added 返回值来判断是否插入成功的,如果等于0那么就已经存在了指纹,就不会将request插入队列.
request_fingerprint()这是获取指纹的方法
pipelines.py---管道重写
from scrapy.utils.misc import load_object
from scrapy.utils.serialize import ScrapyJSONEncoder
from twisted.internet.threads import deferToThread
from . import connection, defaults
default_serialize = ScrapyJSONEncoder().encode
class RedisPipeline(object):
"""Pushes serialized item into a redis list/queue
Settings
--------
REDIS_ITEMS_KEY : str
Redis key where to store items.
REDIS_ITEMS_SERIALIZER : str
Object path to serializer function.
"""
def __init__(self, server,
key=defaults.PIPELINE_KEY,
serialize_func=default_serialize):
"""Initialize pipeline.
Parameters
----------
server : StrictRedis
Redis client instance.
key : str
Redis key where to store items.
serialize_func : callable
Items serializer function.
"""
self.server = server
self.key = key
self.serialize = serialize_func
@classmethod
def 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)
@classmethod
def 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 item
def item_key(self, item, spider):
"""Returns redis key based on given spider.
Override this function to use a different key depending on the item
and/or spider.
"""
return self.key % {'spider': spider.name}
这个作用就是将管道里的信息统一送回到redis master主机里,一旦启用所有的数据都将存储在中心.可选择开启,开启后影响网速,但是可以统一管理数据.
queue.py--队列
from scrapy.utils.reqser import request_to_dict, request_from_dict
from . import picklecompat
class Base(object):
"""Per-spider base queue class"""
def __init__(self, server, spider, key, serializer=None):
"""Initialize per-spider redis queue.
Parameters
----------
server : StrictRedis
Redis client instance.
spider : Spider
Scrapy spider instance.
key: str
Redis key where to put and get messages.
serializer : object
Serializer object with ``loads`` and ``dumps`` methods.
"""
if serializer is None:
# Backward compatibility.
# TODO: deprecate pickle.
serializer = picklecompat
if 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 = server
self.spider = spider
self.key = key % {'spider': spider.name}
self.serializer = serializer
def _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 NotImplementedError
def push(self, request):
"""Push a request"""
raise NotImplementedError
def pop(self, timeout=0):
"""Pop a request"""
raise NotImplementedError
def 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 request
timeout not support in this queue class
"""
# use atomic range/remove using multi/exec
pipe = 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
这里有三种队列,PriorityQueue-优先级队列(存储时也会存储这个request的优先级,这样可以科学的调用request), FifoQueue(first in first out),LifoQueue(last in first out) ,主要是push和pop方法.并且在存储中会调用encode,将request序列化一下,拿出来的时候在decode一下还原.
scheduler.py---调度器
import importlib
import six
from scrapy.utils.misc import load_object
from . import connection, defaults
# TODO: add SCRAPY_JOB support.
class Scheduler(object):
"""Redis-based scheduler
Settings
--------
SCHEDULER_PERSIST : bool (default: False) # 这个用来持久化存储的选项,如果开启了,它在爬取完毕后redis队列内容不会清空
Whether to persist or clear redis queue.
SCHEDULER_FLUSH_ON_START : bool (default: False) # 是指为true后,每次启动后都会清空,也就是重新爬取
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 : str
Scheduler redis key.
SCHEDULER_QUEUE_CLASS : str
Scheduler queue class.
SCHEDULER_DUPEFILTER_KEY : str
Scheduler dupefilter redis key.
SCHEDULER_DUPEFILTER_CLASS : str
Scheduler dupefilter class.
SCHEDULER_SERIALIZER : str
Scheduler 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 : Redis
The redis server instance.
persist : bool
Whether to flush requests when closing. Default is False.
flush_on_start : bool
Whether to flush requests on start. Default is False.
queue_key : str
Requests queue key.
queue_cls : str
Importable path to the queue class.
dupefilter_key : str
Duplicates filter key.
dupefilter_cls : str
Importable path to the dupefilter class.
idle_before_close : int
Timeout before giving up.
"""
if idle_before_close < 0:
raise TypeError("idle_before_close cannot be negative")
self.server = server
self.persist = persist
self.flush_on_start = flush_on_start
self.queue_key = queue_key
self.queue_cls = queue_cls
self.dupefilter_cls = dupefilter_cls
self.dupefilter_key = dupefilter_key
self.idle_before_close = idle_before_close
self.serializer = serializer
self.stats = None
def __len__(self):
return len(self.queue)
@classmethod
# 从配置文件获取信息
def 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)
@classmethod
def from_crawler(cls, crawler):
instance = cls.from_settings(crawler.settings)
# FIXME: for now, stats are only supported from this constructor
instance.stats = crawler.stats
return instance
def open(self, spider): # 声明两个变量,默认使用优先级队列
self.spider = spider
try:
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)
try:
# 这里使用过滤器来去重
self.df = load_object(self.dupefilter_cls)(
server=self.server,
key=self.dupefilter_key % {'spider': spider.name},
debug=spider.settings.getbool('DUPEFILTER_DEBUG'),
)
except TypeError as e:
raise ValueError("Failed to instantiate dupefilter class '%s': %s",
self.dupefilter_cls, e)
# 如果设置了清空那么就会执行这个方法
if self.flush_on_start:
self.flush()
# notice if there are requests already in the queue to resume the crawl
if 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): # 向request队列中添加request
if not request.dont_filter and self.df.request_seen(request):
self.df.log(request, self.spider) # 判断是否需要去重
return False
if self.stats:
self.stats.inc_value('scheduler/enqueued/redis', spider=self.spider)
self.queue.push(request) # 将request添加到队列中
return True
def next_request(self): # 从队列中取request,如果为空那么就重新下载爬取
block_pop_timeout = self.idle_before_close
request = self.queue.pop(block_pop_timeout)
if request and self.stats:
self.stats.inc_value('scheduler/dequeued/redis', spider=self.spider)
return request
def has_pending_requests(self):
return len(self) > 0
Scrapy-redis设置的分析:
#启用Redis调度存储请求队列
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
#确保所有的爬虫通过Redis去重
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
#默认请求序列化使用的是pickle 但是我们可以更改为其他类似的。PS:这玩意儿2.X的可以用。3.X的不能用
#SCHEDULER_SERIALIZER = "scrapy_redis.picklecompat"
#不清除Redis队列、这样可以暂停/恢复 爬取
#SCHEDULER_PERSIST = True
#使用优先级调度请求队列 (默认使用)
#SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.PriorityQueue'
#可选用的其它队列
#SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.FifoQueue'
#SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.LifoQueue'
#最大空闲时间防止分布式爬虫因为等待而关闭
#SCHEDULER_IDLE_BEFORE_CLOSE = 10
#将清除的项目在redis进行处理
ITEM_PIPELINES = {
'scrapy_redis.pipelines.RedisPipeline': 300
}
#序列化项目管道作为redis Key存储
#REDIS_ITEMS_KEY = '%(spider)s:items'
#默认使用ScrapyJSONEncoder进行项目序列化
#You can use any importable path to a callable object.
#REDIS_ITEMS_SERIALIZER = 'json.dumps'
#指定连接到redis时使用的端口和地址(可选)
#REDIS_HOST = 'localhost'
#REDIS_PORT = 6379
#指定用于连接redis的URL(可选)
#如果设置此项,则此项优先级高于设置的REDIS_HOST 和 REDIS_PORT
#REDIS_URL = 'redis://user:pass@hostname:9001'
#自定义的redis参数(连接超时之类的)
#REDIS_PARAMS = {}
#自定义redis客户端类
#REDIS_PARAMS['redis_cls'] = 'myproject.RedisClient'
#如果为True,则使用redis的'spop'进行操作。
#如果需要避免起始网址列表出现重复,这个选项非常有用。开启此选项urls必须通过sadd添加,否则会出现类型错误。
#REDIS_START_URLS_AS_SET = False
#RedisSpider和RedisCrawlSpider默认 start_usls 键
#REDIS_START_URLS_KEY = '%(name)s:start_urls'
#设置redis使用utf-8之外的编码
#REDIS_ENCODING = 'latin1'
准备工作就到这里:那么我们要怎么改装我们之前的代码呢?
怎么使用 Scrapy-redis
在setting上加入需要的设置
当然你已经安装好了scrapy-redis 然后在设置中加入以下设置
SCHEDULER = "scrapy_redis.scheduler.Scheduler" # 启用redis调度器
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter" # 使用redis的过滤器
SCHEDULER_PERSIST = True #开启不清空队列,这样就可以暂停爬取
ITEM_PIPELINES = {
'scrapy_redis.pipelines.RedisPipeline': 301 # 开启这个管道后所有的数据都将被送往master主机,这样不利于网络的通畅,通常来说都是各自存在各自的机器让最后再汇总当然你也可以开启.
}
REDIS_URL = 'redis://user:pass@hostname:9001' # 配置master数据库信息,如果没有密码只要填写用户名就好,user默认为(root),hostname为弹性ip地址,redis的默认端口为6379
服务器配置
到这里基本的就配置完了,如果你是一台服务器,那么请在服务器中安装redis,然后开启redis.还有要在服务器的配置的安全组中将端口6379(redis的端口)开放,并且将ICMP服务开启.如图:
配置redis
服务器的redis安装好了后,在安装目录下找到一个redis.conf的文件,需要修改一下几个配置:
- bind 127.0.0.1 #绑定ip,默认是本机所有网络设备,这里我们要改为0.0.0.0这样就可以监听所有网络
- daemonize yes #---默认值no,该参数用于定制redis服务是否以守护模式运行,也就是后台运行
- protected-mode yes # 是否开启保护模式。默认开启,如果没有设置bind项的ip和redis密码的话,服务将只允许本地访问。
修改这几个就能使用啦!如果你想查看详细的配置,看这里 http://www.jianshu.com/p/41f393f594e8
到这里就完成了分布式爬取啦,在命令行中重新启动爬虫,就可以在redis中查看爬取到的数据了:
推荐使用redis Desktop Manager 来远程连接服务端的数据库,这样方便查看数据库中的数据.
到这里分布式的代码就写完了然后我们需要弄到服务器上
使用工具来部署项目
这里我们提供两种方案:
使用git
- 将项目传到GitHub上,记住自己是用哪个分支来操作的
- 进入服务器的终端安装好上篇提示的环境(redis,scrapy,pymongo,mongod,scrapy-redis,) mongodb的配置文件中的bindip需要修改为0.0.0.0 这样就可以远程访问了.具体请Google 如何配置mongod
- 然后使用git命令来clone 这个项目(创建一个单独的项目文件夹然后将项目clone)
- 然后使用命令
$ git clone https://github.com/renwenduan/zhihu.git -b renwen
这样我们就将文件clone到服务器了,如果没有权限那就使用HTTPS来clone,-b为指定分支
然后在服务器端终端输出:
$ scrapy crawl zhihu
这样我们就在服务器端也开启了一个蜘蛛,你可以依次在你的服务器端部署,这样就完成了服务器端的部署啦!想要更多的配置信息,请看上面的srapy-redis 的设置信息.如果你不放心,可以去看下服务器端的数据是否更新了,这里我就不截图了. 当然如果想要减小你的服务器压力,可以关闭 scrapy_redis.pipelines.RedisPipeline 这个管道,将数据分别存在各自的机器上,这样就减轻了服务器的网络传输压力.
还有另外一个问题.如果你有两三台需要部署还好,如果你有上百台服务器怎么办,下面就介绍一个官方提供的工具:scapyd!
使用Scrapyd
上官方文档 http://scrapyd.readthedocs.io/en/latest/index.html
-
首先在所有的主机上安装scrapyd 记得在安全组中开启端口6800:
$ pip install scrapyd
还有记得求该scrapyd的配置文件,通常在/etc/scrapyd/scrapyd.conf 可以找到scrapyd的配置文件,将bind改为0.0.0.0 否则远程无法访问(这样可能会有安全问题),具体请看: http://www.cnblogs.com/Irving/p/6752575.html -
然后在本机的项目目录下有个scrapy.cfg文件设置如下
# Automatically created by: scrapy startproject # # For more information about the [deploy] section see: # https://scrapyd.readthedocs.org/en/latest/deploy.html [settings] default = zhihu.settings [deploy] url = http://114.115.144.284:6800/addversion.json project = zhihu
这里ip写你的服务器的ip,端口为默认端口,记得在最后加上addversion.json
然后安装scrapyd-client:
$ pip install scrapyd-client
在本地终端中输入:
scrapyd-deploy就可以将项目远程部署到你的服务器了,如果你多次提交那就开启多个蜘蛛进行爬取.
然后在浏览器中输入你服务器地址:6800 就可以查看这个机器上所有的项目运行情况(盗图):
这里你可以查看日志文件也可以.- 停止任务
$ curl http://你的服务器ip地址:6800/cancel.json -d project=zhihi -d job=你的job代号
- 停止任务
更多操作可以查看官方文档提供的api: https://scrapyd.readthedocs.io/en/stable/api.html
到这里整个项目就做完了,如果有什么错误和不妥请留言或者发邮件给我[email protected]
本文参考了'静觅'崔庆才大神的博客非常感谢,侵删!他的更多博客请看:崔大神的博客