2021SC@SDUSC
3.3 ExecutionEngine
看下 open_spider
# scrapy/core/engine.py
class ExecutionEngine(object):
# ...
@defer.inlineCallbacks
def open_spider(self, spider, start_requests=(), close_if_idle=True):
assert self.has_capacity(), "No free spider slot when opening %r" % \
spider.name
logger.info("Spider opened", extra={'spider': spider})
nextcall = CallLaterOnce(self._next_request, spider)
scheduler = self.scheduler_cls.from_crawler(self.crawler)
start_requests = yield self.scraper.spidermw.process_start_requests(start_requests, spider)
slot = Slot(start_requests, close_if_idle, nextcall, scheduler)
self.slot = slot
self.spider = spider
yield scheduler.open(spider)
yield self.scraper.open_spider(spider)
self.crawler.stats.open_spider(spider)
yield self.signals.send_catch_log_deferred(signals.spider_opened, spider=spider)
slot.nextcall.schedule()
slot.heartbeat.start(5)
- 首先检查 engine 的 slot 是否空闲,这里 1 个 engine 只能运行 1 个 spider
- 然后是一些属性的初始化以及启动,我们可以分成 3 部分来看:一是 slot 里面的属性,二是 crawler 的属性,最后就是 engine 的直接属性
先了解下上面出现的 CallLaterOnce 的用法
# scrapy/utils/reactor.py
class CallLaterOnce(object):
def __init__(self, func, *a, **kw):
self._func = func
self._a = a
self._kw = kw
self._call = None
def schedule(self, delay=0):
if self._call is None:
self._call = reactor.callLater(delay, self)
def cancel(self):
if self._call:
self._call.cancel()
def __call__(self):
self._call = None
return self._func(*self._a, **self._kw)
这个类的目的其实就是实现 callLater 的功能,不同的是我们可以 schedule 多次,在一定时间内只会调度 1 个任务到事件循环里面去,如果直接使用 callLater 的话,会重复添加很多个任务到事件循环里面去,影响执行效率。看个例子
def f(arg):
# 请求等 IO 操作
nextcall = CallLaterOnce(f, 'some arg')
nextcall.schedule(5)
nextcall.schedule(5)
nextcall.schedule(5)
这里 nextcall 虽说被 schedule 了多次,但是只有第 1 次的 schedule 是有效的,最终只添加了 1 个任务到事件循环。Scrapy 很多地方都会调用 _next_request
,内部又都是并发操作,难免会碰到短时间内同时调用这个方法,这里便使用了 CallLaterOnce 降低处理压力。
看下 Slot 的构造函数
# scrapy/core/engine.py
class Slot(object):
def __init__(self, start_requests, close_if_idle, nextcall, scheduler):
self.closing = False
self.inprogress = set() # requests in progress
self.start_requests = iter(start_requests)
self.close_if_idle = close_if_idle
self.nextcall = nextcall
self.scheduler = scheduler
self.heartbeat = task.LoopingCall(nextcall.schedule)
- inprogress,记录当前正在处理的请求
- start_requests,爬虫的初始化请求
- nextcall,封装了
_next_request
方法的 CallLaterOnce - scheduler,调度器,里面存放待爬取的请求
- heartbeat,心跳,每隔一段时间执行一次
_next_request
简单看下 Scheduler 的构造方法
# scrapy/core/scheduler.py
class Scheduler(object):
def __init__(self, dupefilter, jobdir=None, dqclass=None, mqclass=None,
logunser=False, stats=None, pqclass=None, crawler=None):
self.df = dupefilter
self.dqdir = self._dqdir(jobdir)
self.pqclass = pqclass
self.dqclass = dqclass
self.mqclass = mqclass
self.logunser = logunser
self.stats = stats
self.crawler = crawler
- df,去重器
- dqdir,启动爬虫时,我们指定的持久化目录 jobdir
- pqclass,优先级队列类型
- dqclass、mqclass 分别是磁盘和内存队列类型。当我们指定了 jobdir 后,主要是用磁盘队列
- stats,主要用于统计内存状态,在 Crawler 的构造函数中开始被创建,然后一级一级的传到这来的
上面的类具体是是指那些类,可以看 Scheduler 的 from_crawler
方法。关于怎么入队列、出队列等比较详细的操作,这里不展开,感兴趣的可以看下这个类。