Scrapy 中的 from_crawler() 方法应该怎么用?

零、前言

Crawler 类是 Scrapy 中的一个核心组件, 很多其他组件通过实现 from_crawler() 方法与 Crawler 类建立起联系.
from_crawler() 方法为爬虫各组件提供了强大的功能扩展支持.
本文将结合源码,对from_crawler()方法进行梳理总结.

全文主要围绕以下几个问题:

  1. 哪些组件可以使用 from_crawler() 方法?
  2. from_crawler() 中都可以实现哪些功能?
  3. from_crawler() 方法是怎么被调用的?


一、哪些组件可以使用 from_crawler() 方法

首先,常用的组件有:

  • DupeFilter
  • Scheduler
  • Middlewares
  • Pipelines
  • Extensions
  • Spider

根据场景,不常用的组件有:

  • downloader.handlers
  • queues
  • dns resolver
  • log formatter

常用组件其实可以参考 scrapy_redis 的实现,很容易就可以看到相关的用法.

或者可以参考 scrapy 本身模块的实现,项目中可参考的模块有:

  • scrapy.dupefilter
  • scrapy.core.scheduler
  • scrapy.downloadermiddlewares
  • scrapy.spidermiddlewares
  • scrapy.core.downloader.handlers
  • scrapy.squeues
  • scrapy.resolver


二、在 from_crawler() 方法中都可以实现哪些功能

首先,from_crawler() 最核心的一个目的,是创建并返回其所在类的实例

比如自己实现 Spider 时,所继承的父类 scrapy.Spider中:

1
2
3
4
5
6
7
8
9
class Spider(object_ref):
...
@classmethod
def from_crawler(cls, crawler, *args, **kwargs):
# 创建所在类的实例并返回
spider = cls(*args, **kwargs)
spider._set_crawler(crawler)
return spider
...

这里有个相关的知识点:类方法

其次,由于必须传入crawler 实例作为参数,那么我们可以crawler 携带的属性加以利用

比如内置的爬虫中间件 scrapy.spidermiddlewares.depth.DepthMiddleware

from_crawler() 中获取 settings 中的配置项,将其作为实例属性绑定到中间件实例中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class DepthMiddleware:

def __init__(self, maxdepth, stats, verbose_stats=False, prio=1):
self.maxdepth = maxdepth
self.stats = stats
self.verbose_stats = verbose_stats
self.prio = prio

@classmethod
def from_crawler(cls, crawler):
# 可以获取 crawler.settings 中的配置项,在实例化时作为参数传入
settings = crawler.settings
maxdepth = settings.getint('DEPTH_LIMIT')
verbose = settings.getbool('DEPTH_STATS_VERBOSE')
prio = settings.getint('DEPTH_PRIORITY')
return cls(maxdepth, crawler.stats, verbose, prio)

...

另外,由于 scrapy 提供了信号的支持, 并且 Crawler 实例中绑定了一个 signals 属性。那么,我们可以 利用信号机制在特定时机执行特定的操作

比如 Scrapy 文档 - Signals 一节给出的爬虫示例:

from_crawler() 定义了在爬虫结束时,执行 spider_closed()方法,输出日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class DmozSpider(Spider):
name = "dmoz"
allowed_domains = ["dmoz.org"]
start_urls = [
"http://www.dmoz.org/Computers/Programming/Languages/Python/Books/",
"http://www.dmoz.org/Computers/Programming/Languages/Python/Resources/",
]

@classmethod
def from_crawler(cls, crawler, *args, **kwargs):
spider = super(DmozSpider, cls).from_crawler(crawler, *args, **kwargs)
# 绑定 spider_closed 方法到信号 signals.spider_closed
# 使其爬虫结束时触发执行
crawler.signals.connect(spider.spider_closed, signal=signals.spider_closed)
return spider

def spider_closed(self, spider):
spider.logger.info('Spider closed: %s', spider.name)
...

更多 Crawler 的 API 见文档 Crawler API

三、from_crawler() 方法是怎样被调用的

在上述示例中,直接在类里面声明了 from_crawler() 方法,它们会直接被 scrapy 内部流程中被调用。

那么具体调用过程是什么样的呢?

上面已经说过,类方法 from_crawler() 的主要功能是创建并返回类的实例,那么只要找到这些组件实例化的位置,就能看到 from_crawler() 被调用的过程。

这些组件被实例化,主要是通过两种方式:

  1. 直接调用 from_crawler() 进行实例化
  2. 通过 scrapy.util.misc.create_instance() 函数进行实例化

首先说 1. 直接调用 from_crawler() 进行实例化。

典型的示例如 scrapy.crawler.Crawler 中,对爬虫类 Spider 进行实例化的过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Crawler:
...

@defer.inlineCallbacks
def crawl(self, *args, **kwargs):
if self.crawling:
raise RuntimeError("Crawling already taking place")
self.crawling = True

try:
self.spider = self._create_spider(*args, **kwargs)
self.engine = self._create_engine()
start_requests = iter(self.spider.start_requests())
yield self.engine.open_spider(self.spider, start_requests)
yield defer.maybeDeferred(self.engine.start)
except Exception:
self.crawling = False
if self.engine is not None:
yield self.engine.close()
raise

def _create_spider(self, *args, **kwargs):
# 这里直接调用了 spidercls 的 from_crawler 方法,获取 spidercls 的实例
return self.spidercls.from_crawler(self, *args, **kwargs)

def _create_engine(self):
return ExecutionEngine(self, lambda _: self.stop())

...

在 scrapy 中用到最多的是第二种,通过create_instance() 函数进行实例化.

调用的实例如 scheduler 实例化 dupefilter 的过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Scheduler:
...

@classmethod
def from_crawler(cls, crawler):
settings = crawler.settings
dupefilter_cls = load_object(settings['DUPEFILTER_CLASS'])
# 这里使用 create_instance 对 dupefilter_cls 进行实例化
dupefilter = create_instance(dupefilter_cls, settings, crawler)
pqclass = load_object(settings['SCHEDULER_PRIORITY_QUEUE'])
if pqclass is PriorityQueue:
warnings.warn("SCHEDULER_PRIORITY_QUEUE='queuelib.PriorityQueue'"
" is no longer supported because of API changes; "
"please use 'scrapy.pqueues.ScrapyPriorityQueue'",
ScrapyDeprecationWarning)
from scrapy.pqueues import ScrapyPriorityQueue
pqclass = ScrapyPriorityQueue

dqclass = load_object(settings['SCHEDULER_DISK_QUEUE'])
mqclass = load_object(settings['SCHEDULER_MEMORY_QUEUE'])
logunser = settings.getbool('SCHEDULER_DEBUG')
return cls(dupefilter, jobdir=job_dir(settings), logunser=logunser,
stats=crawler.stats, pqclass=pqclass, dqclass=dqclass,
mqclass=mqclass, crawler=crawler)
...

那么我们来看下 create_instance 函数内部的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def create_instance(objcls, settings, crawler, *args, **kwargs):
"""Construct a class instance using its ``from_crawler`` or
``from_settings`` constructors, if available.

At least one of ``settings`` and ``crawler`` needs to be different from
``None``. If ``settings `` is ``None``, ``crawler.settings`` will be used.
If ``crawler`` is ``None``, only the ``from_settings`` constructor will be
tried.

``*args`` and ``**kwargs`` are forwarded to the constructors.

Raises ``ValueError`` if both ``settings`` and ``crawler`` are ``None``.

.. versionchanged:: 2.2
Raises ``TypeError`` if the resulting instance is ``None`` (e.g. if an
extension has not been implemented correctly).
"""
if settings is None:
if crawler is None:
raise ValueError("Specify at least one of settings and crawler.")
settings = crawler.settings
if crawler and hasattr(objcls, 'from_crawler'):
instance = objcls.from_crawler(crawler, *args, **kwargs)
method_name = 'from_crawler'
elif hasattr(objcls, 'from_settings'):
instance = objcls.from_settings(settings, *args, **kwargs)
method_name = 'from_settings'
else:
instance = objcls(*args, **kwargs)
method_name = '__new__'
if instance is None:
raise TypeError(f"{objcls.__qualname__}.{method_name} returned None")
return instance

逻辑非常清晰明了,根据条件判断调用类的 from_crawler() 方法 或 from_settings() 方法,对类进行实例化,并返回。

同时,这里也可以看到另一个常见的类方法—— from_settings() ,有着和 from_crawler() 相似的作用。

但是如果一个组件是通过 create_instance() 被实例化的, 并且组件内又同时实现了 from_crawler()from_settings() 方法,那么将只有 from_crawler() 方法被调用。

至此, 我们可以对 Scrapy 中常用的 from_crawler() 方法有一个基本的认识.


四、参考资料

[1] Scrapy documentation
[2] Scrapy 源码