2.9 Signals (信号) Scrapy 核心组件详解:Signals (信号) 在 Scrapy 框架中,Signals (信号) 是一种事件通知机制,允许开发者在特定的事件发生时执行自定义的操作。 它们提供了一种解耦 Scrapy 组件的方式,使得我们可以在不修改核心代码的情况下,扩展和定制 Scrapy 的行为。 信号机制基于观察者模式,允许不同的组件监听特定事件,并在事件发生时执行相应的处理函数。 信号机制的核心概念 信号 (Signals): 代表 Scrapy 内部发生的特定事件,例如 spider 启动、item 被 scraped、请求完成等。Scrapy 定义了许多内置信号,开发者也可以自定义信号。 信号发送者 (Signal Sender): 负责发出信号的对象。
在 Scrapy 框架中,Signals (信号) 是一种事件通知机制,允许开发者在特定的事件发生时执行自定义的操作。 它们提供了一种解耦 Scrapy 组件的方式,使得我们可以在不修改核心代码的情况下,扩展和定制 Scrapy 的行为。 信号机制基于观察者模式,允许不同的组件监听特定事件,并在事件发生时执行相应的处理函数。
信号 (Signals): 代表 Scrapy 内部发生的特定事件,例如 spider 启动、item 被 scraped、请求完成等。Scrapy 定义了许多内置信号,开发者也可以自定义信号。
信号发送者 (Signal Sender): 负责发出信号的对象。 通常是 Scrapy 框架内部的组件,例如 engine, downloader, spider 等。
信号接收者 (Signal Receiver): 监听特定信号并执行相应处理函数 (handler) 的对象。 接收者可以是 Scrapy 的任何组件,甚至是自定义的扩展。
信号处理函数 (Signal Handler): 也称为回调函数,当信号被触发时,接收者执行的函数。 处理函数可以接收信号发送者传递的参数,并根据需要执行相应的操作。
Scrapy 提供了丰富的内置信号,涵盖了爬虫生命周期的各个阶段。 以下是一些常用的内置信号:
spider_opened: 在 spider 启动时发送。
spider_closed: 在 spider 关闭时发送。
item_scraped: 在 item 被成功 scraped 时发送。
item_dropped: 在 item 被 pipeline 丢弃时发送。
request_scheduled: 在请求被调度器调度时发送。
request_dropped: 在请求被调度器丢弃时发送。
response_received: 在 response 被 downloader 下载后发送。
spider_error: 在 spider 发生错误时发送。
engine_started: 在 Scrapy 引擎启动时发送。
engine_stopped: 在 Scrapy 引擎停止时发送。
解耦性: 信号机制允许不同的组件独立地工作,而无需直接依赖彼此。 这使得 Scrapy 的架构更加灵活和可维护。
可扩展性: 通过监听信号,开发者可以在不修改 Scrapy 核心代码的情况下,添加自定义的功能和行为。
可定制性: 信号机制允许开发者在 Scrapy 的各个阶段插入自定义的逻辑,从而定制爬虫的行为。
事件驱动: 信号机制使得 Scrapy 能够响应内部发生的事件,并根据事件执行相应的操作。
使用 Scrapy 的信号机制需要以下几个步骤:
定义信号处理函数 (Signal Handler): 创建一个函数,用于处理特定的信号。该函数可以接收信号发送者传递的参数。
连接信号和处理函数: 使用 signals.connect() 方法将信号处理函数与特定的信号连接起来。
指定信号发送者 (可选): 可以指定信号的发送者,以便只监听来自特定对象的信号。
下面是一个简单的例子,演示如何使用 spider_opened 和 spider_closed 信号:
import scrapy from scrapy import signals from scrapy.crawler import CrawlerProcess class MySpider(scrapy.Spider): name = "myspider" start_urls = ["http://example.com"] def parse(self, response): yield {"title": response.xpath("//title/text()").get()} def spider_opened(spider): print(f"Spider {spider.name} opened!") def spider_closed(spider, reason): print(f"Spider {spider.name} closed with reason: {reason}") def item_scraped(item, spider): print(f"Item scraped: {item} from spider: {spider.name}") return item process = CrawlerProcess() spider = MySpider() # 连接信号和处理函数 process.signals.connect(spider_opened, signal=signals.spider_opened) process.signals.connect(spider_closed, signal=signals.spider_closed) process.signals.connect(item_scraped, signal=signals.item_scraped) process.crawl(spider) process.start()
代码解释:
定义信号处理函数:
spider_opened(spider): 在 spider 启动时被调用,接收 spider 对象作为参数。
spider_closed(spider, reason): 在 spider 关闭时被调用,接收 spider 对象和关闭原因作为参数。
item_scraped(item, spider): 在 item 被成功 scraped 时被调用,接收 item 对象和 spider 对象作为参数。
连接信号和处理函数:
process.signals.connect(spider_opened, signal=signals.spider_opened): 将 spider_opened 函数与 spider_opened 信号连接起来。
process.signals.connect(spider_closed, signal=signals.spider_closed): 将 spider_closed 函数与 spider_closed 信号连接起来。
process.signals.connect(item_scraped, signal=signals.item_scraped): 将 item_scraped 函数与 item_scraped 信号连接起来。
运行爬虫:
process.crawl(spider): 启动爬虫。
process.start(): 启动 Scrapy 引擎。
运行结果:
当运行这个爬虫时,你会在控制台中看到以下输出:
Spider myspider opened! Item scraped: {'title': 'Example Domain'} from spider: myspider Spider myspider closed with reason: finished
这个例子展示了如何使用 spider_opened 和 spider_closed 信号来在 spider 启动和关闭时执行自定义的操作。 同样,item_scraped 信号则是在成功爬取到 Item 时被触发。
更常见的是,信号被用于创建 Scrapy 扩展。 下面是一个使用信号来记录请求和响应的示例:
from scrapy import signals from scrapy.exceptions import NotConfigured import logging class RequestResponseLogger: def __init__(self, crawler): self.crawler = crawler self.logger = logging.getLogger(__name__) @classmethod def from_crawler(cls, crawler): # 获取 settings 中定义的配置 if not crawler.settings.getbool('REQUEST_RESPONSE_LOGGER_ENABLED'): raise NotConfigured ext = cls(crawler) # 连接信号 crawler.signals.connect(ext.spider_opened, signal=signals.spider_opened) crawler.signals.connect(ext.spider_closed, signal=signals.spider_closed) crawler.signals.connect(ext.request_scheduled, signal=signals.request_scheduled) crawler.signals.connect(ext.response_received, signal=signals.response_received) return ext def spider_opened(self, spider): self.logger.info(f"Spider {spider.name} started.") def spider_closed(self, spider, reason): self.logger.info(f"Spider {spider.name} closed with reason: {reason}") def request_scheduled(self, request, spider): self.logger.debug(f"Request scheduled: {request.url} from spider: {spider.name}") def response_received(self, response, request, spider): self.logger.debug(f"Response received: {response.url} for request: {request.url} from spider: {spider.name}")
代码解释:
定义扩展类:
RequestResponseLogger: 定义一个扩展类,用于记录请求和响应。from_crawler 方法:
@classmethod from_crawler(cls, crawler): 这是一个类方法,用于创建扩展实例。 Scrapy 使用这个方法来加载扩展。
if not crawler.settings.getbool('REQUEST_RESPONSE_LOGGER_ENABLED'): raise NotConfigured: 根据 settings 中的配置决定是否启用扩展。
crawler.signals.connect(...): 将扩展的方法与相应的信号连接起来。
信号处理函数:
spider_opened(self, spider): 在 spider 启动时被调用,记录 spider 启动信息。
spider_closed(self, spider, reason): 在 spider 关闭时被调用,记录 spider 关闭信息。
request_scheduled(self, request, spider): 在请求被调度时被调用,记录请求的 URL。
response_received(self, response, request, spider): 在接收到响应时被调用,记录响应的 URL 和对应的请求 URL。
配置 settings.py:
REQUEST_RESPONSE_LOGGER_ENABLED = True SPIDER_MIDDLEWARES = { 'your_project.extensions.RequestResponseLogger': 543, # 替换 your_project 为你的项目名称 }
运行结果:
启用此扩展后,您将在日志中看到有关已调度请求和收到的响应的信息。
除了使用 Scrapy 提供的内置信号外,我们还可以自定义信号。 自定义信号允许我们在特定的业务逻辑中触发事件,并执行相应的处理函数.
自定义信号需要使用 scrapy.signals.Signal 类。 例如,我们可以定义一个 item_validated 信号,在 item 被验证后发送:
from scrapy import signals item_validated = signals.Signal()
要发送自定义信号,需要使用 send_catch_log 方法。 例如,在 item 被验证后,我们可以发送 item_validated 信号:
from scrapy import signals from scrapy.utils.misc import load_object from scrapy.exceptions import NotConfigured item_validated = signals.Signal() class ValidationMiddleware: def __init__(self, crawler): self.crawler = crawler @classmethod def from_crawler(cls, crawler): mw = cls(crawler) crawler.signals.connect(mw.spider_opened, signal=signals.spider_opened) return mw def spider_opened(self, spider): self.validator = load_object(spider.settings.get('ITEM_VALIDATOR'))(spider=spider) def process_item(self, item, spider): try: self.validator.validate(item) except Exception as e: spider.logger.error(f"Item validation failed: {e}") raise DropItem(e) else: # 发送自定义信号 spider.crawler.signals.send_catch_log(item_validated, item=item, spider=spider) return item
代码解释:
定义自定义信号:
item_validated = signals.Signal(): 定义一个名为 item_validated 的自定义信号。在 process_item 方法中发送信号:
spider.crawler.signals.send_catch_log(item_validated, item=item, spider=spider): 在 item 被成功验证后,发送 item_validated 信号。
item=item, spider=spider: 将 item 对象和 spider 对象作为参数传递给信号处理函数。
接收自定义信号的方式与接收内置信号相同。 例如,我们可以创建一个扩展来监听 item_validated 信号:
from scrapy import signals from scrapy.exceptions import NotConfigured import logging class ItemValidatedLogger: def __init__(self, crawler): self.crawler = crawler self.logger = logging.getLogger(__name__) @classmethod def from_crawler(cls, crawler): if not crawler.settings.getbool('ITEM_VALIDATED_LOGGER_ENABLED'): raise NotConfigured ext = cls(crawler) crawler.signals.connect(ext.item_validated, signal=item_validated) return ext def item_validated(self, item, spider): self.logger.info(f"Item validated: {item} from spider: {spider.name}")
代码解释:
定义扩展类:
ItemValidatedLogger: 定义一个扩展类,用于记录 item 被验证的信息。from_crawler 方法:
crawler.signals.connect(ext.item_validated, signal=item_validated): 将扩展的 item_validated 方法与 item_validated 信号连接起来。信号处理函数:
item_validated(self, item, spider): 在 item 被成功验证后被调用,记录 item 的信息。可以使用 mermaid 绘制一个简单的流程图,说明信号机制的工作原理:
流程图解释:
事件发生: Scrapy 内部发生一个事件,例如 spider 启动、item 被 scraped 等。
信号发送者: 信号发送者检测到事件发生,并发出相应的信号。
信号: 信号代表发生的事件。
信号接收者: 信号接收者监听特定的信号。
信号处理函数: 当信号被触发时,信号接收者执行相应的信号处理函数。
执行操作: 信号处理函数执行自定义的操作,例如记录日志、修改 item 等。
Scrapy 的 Signals 机制是一种强大的工具,用于扩展和定制爬虫的行为。 通过理解和使用 Signals,开发者可以更好地控制爬虫的各个阶段,并实现更复杂的功能。 无论是使用内置信号还是自定义信号,都可以使 Scrapy 爬虫更加灵活、可维护和可扩展。 掌握 Signals 是成为 Scrapy 高级开发者的关键一步。