Scrapy 各组件 __init__ 触发机制深度解析

本文从一段 Scrapy Pipeline 代码出发,系统梳理 Spider、Pipeline、Middleware、Item 四大组件的实例化机制,以及 __init__from_crawler 的关系。

一、引子:Pipeline 中为何先用 __init__ 初始化为 None?

先看一段常见的 Pipeline 写法:

class TxWorkInfoPipeline:
    def __init__(self):
        self.mongo_client = None
        self.redis_client = None
        self.mongo_db = None

    def open_spider(self, spider):
        self.redis_client = redis.Redis()
        self.mongo_client = pymongo.MongoClient()
        self.mongo_db = self.mongo_client['py_spider']['tx_work']

    def process_item(self, item, spider):
        if spider.name == 'tx_work':
            self.db.insert_one(item)
            print('保存成功:', item)
        return item

既然真正的数据库连接是在 open_spider 里完成的,__init__ 里赋值 None 是否多余?

不多余,这是一种防御性编程的好习惯,原因如下:

1. 明确声明实例属性

Python 约定俗成的做法是把所有实例属性都在 __init__ 中声明。这样一眼就能看出这个类有哪些属性,代码可读性更强。

2. 防止 close_spider 报 AttributeError

def close_spider(self, spider):
    if self.mongo_client:   # 若没有 __init__ 里的初始化,open_spider 未执行时此处直接崩溃
        self.mongo_client.close()

如果 open_spider 因某些异常没有执行,而 close_spider 里又访问了 self.mongo_client,就会抛出 AttributeError。提前在 __init__ 里初始化为 None 可以避免这个问题。

结论: __init__ 里赋 None 并非必须,但是良好实践。如果你确定 open_spider 一定会被调用,直接在 open_spider 里初始化也完全没有问题。


二、Pipeline 的 __init__ 是如何被触发的?

Scrapy 在启动时,读取 settings.pyITEM_PIPELINES 的配置,然后对每个管道类直接调用 cls() 来实例化:

pipeline_instance = TxWorkInfoPipeline()  # 框架内部的行为

这自然就触发了 __init__

如果你想在 Pipeline 中访问 crawler(比如读取 settings), 可以自定义 from_crawler

@classmethod
def from_crawler(cls, crawler):
    instance = cls()   # 触发 __init__
    instance.settings = crawler.settings
    return instance

如果定义了 from_crawler,Scrapy 会优先调用它;否则直接 cls() 实例化。


三、各组件 __init__ 触发机制全对比

Spider

Spider 的实例化是由 CrawlerProcess / CrawlerRunner 触发的,Scrapy 内部会调用:

spider = MySpider.from_crawler(crawler, *args, **kwargs)

注意:Spider 默认就有 from_crawler,它定义在 scrapy.Spider 基类里:

@classmethod
def from_crawler(cls, crawler, *args, **kwargs):
    spider = cls(*args, **kwargs)   # 触发 __init__
    spider._set_crawler(crawler)
    return spider

所以 Spider 和中间件一样,也是通过 from_crawler 间接触发 __init__,只不过这个 from_crawler 是基类帮你写好的,你感知不到而已。

Middleware(中间件)

中间件需要自己实现 from_crawler,由框架调用后,在其内部触发 __init__

@classmethod
def from_crawler(cls, crawler):
    s = cls(crawler)   # 触发 __init__
    return s

Pipeline(管道)

框架直接 cls() 实例化,触发 __init__。除非你自定义了 from_crawler,否则无法直接访问 crawler 对象。

Item

Item 完全不参与框架的生命周期管理,是开发者在 parse 方法里手动 new 出来的

def parse(self, response):
    item = MyItem()   # 你自己触发的 __init__
    item['title'] = response.css('h1::text').get()
    yield item

四、总结对比表

组件__init__ 触发方式是否经过 from_crawler
Spider基类的 from_crawlercls()✅ 是(基类已实现,对开发者透明)
Middleware自定义 from_crawlercls()✅ 是(需自己实现)
PipelineScrapy 框架直接 cls()❌ 否(除非自定义 from_crawler
Item开发者在 parse 中手动 cls()❌ 否,框架完全不介入

五、延伸:在自己的 Spider 中重写 __init__ 时需要 super().__init__() 吗?

分两种情况:

情况一:没有重写 __init__(最常见)

Python 直接使用父类的 __init__,一切正常,无需任何额外操作。

情况二:重写了 __init__

此时父类的 __init__ 被覆盖,必须手动调用 super().__init__(*args, **kwargs) 来补回父类的初始化逻辑,否则 self.nameself.crawlerself.settings 等属性会丢失,运行时报 AttributeError

class MySpider(scrapy.Spider):
    name = 'my_spider'

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)   # 必须!补回父类初始化逻辑
        self.my_custom_data = []            # 自己额外的初始化

一句话总结:重写了就要 super,没重写就不用管。 这是 Python 面向对象的通用原则,并非 Scrapy 特有。


结语

理解 Scrapy 各组件的实例化机制,不仅能帮助你写出更健壮的代码,也能在遇到奇怪报错时快速定位问题根源。核心要点:

  • Pipeline 是框架直接 cls() 的,最简单
  • Spider 走了 from_crawler,但基类帮你封装好了
  • Middleware 的 from_crawler 需要自己实现
  • Item 完全由开发者自己管理,框架不介入

标签: none

添加新评论