3.3 Signals:框架的广播频道


文档摘要

3.3 Signals:框架的广播频道 本节摘要:信号是框架在旅程关键节点发出的广播:引擎启动、请求调度、下载失败、Item 落库、爬虫关闭,每个事件都有对应信号可订阅。本节讲订阅模式与高频信号的实战用法,让你在不侵入框架源码的前提下给旅程安插眼线。 本章前两节管住了"票怎么排、拨盘怎么调",还有一个盲区:运行中的事件怎么感知?Spider 回调只覆盖"到站"这一种事件,而工程上你关心的远不止于此——爬虫为什么停了、失败率什么时候升的、Item 落了多少。信号系统就是框架对这些问题的正规回答:每个旅程节点广播一个事件,你订阅感兴趣的频道。

3.3 Signals:框架的广播频道

本节摘要:信号是框架在旅程关键节点发出的广播:引擎启动、请求调度、下载失败、Item 落库、爬虫关闭,每个事件都有对应信号可订阅。本节讲订阅模式与高频信号的实战用法,让你在不侵入框架源码的前提下给旅程安插眼线。

本章前两节管住了"票怎么排、拨盘怎么调",还有一个盲区:运行中的事件怎么感知?Spider 回调只覆盖"到站"这一种事件,而工程上你关心的远不止于此——爬虫为什么停了、失败率什么时候升的、Item 落了多少。信号系统就是框架对这些问题的正规回答:每个旅程节点广播一个事件,你订阅感兴趣的频道。

订阅模式:三行代码接入

信号通过调度器连接,处理函数与信号签名匹配即可:

from scrapy import signals from scrapy.signalmanager import dispatcher def on_item_scraped(item, response, spider): # Item 通过了全部管道:落库成功一次,广播一次 print("[统计] 已产出:", item.get("title")) def on_spider_closed(spider, reason): # reason 是关闭原因字符串:finished 表示正常跑完 print("[收尾] 爬虫关闭,原因:", reason) dispatcher.connect(on_item_scraped, signal=signals.item_scraped) dispatcher.connect(on_spider_closed, signal=signals.spider_closed)

每个信号携带的参数固定:item_scraped 带 item、response、spider 三个实参;spider_closed 带 spider 与 reason。签名不匹配会在触发时报错,所以订阅前先查该信号的参数表。

工程里更常见的做法是把订阅写进扩展类——从 SpiderManager 拿到 crawler 对象后在其 signals 上挂接。下面这个收尾统计扩展是实战模板:

class CloseStatsExtension: def __init__(self, crawler): crawler.signals.connect(self.spider_closed, signal=signals.spider_closed) crawler.signals.connect(self.item_scraped, signal=signals.item_scraped) self.count = 0 @classmethod def from_crawler(cls, crawler): return cls(crawler) def item_scraped(self, item, response, spider): self.count += 1 def spider_closed(self, spider, reason): spider.logger.info("本次旅程共产出 %s 条数据,关闭原因 %s", self.count, reason)

在 settings 的扩展清单里登记即启用:

EXTENSIONS = { "myproject.extensions.CloseStatsExtension": 0, }

高频信号对照:挂在旅程的哪个节点

信号 触发节点 典型用途
engine_started / engine_stopped 引擎开关 进程级监控、资源清理
spider_opened / spider_closed 爬虫开关 收尾统计、失败原因上报
request_scheduled 票进候车大厅 调度审计、延迟观测
request_left_downloader 票离开下行隧道 与调度时刻相减算排队耗时
response_downloaded 下载完成 体量统计、慢站发现
item_scraped Item 过完全部管道 落库计数、增量告警
item_error 管道抛错 数据质量告警

这张表的用法不是背下来,而是遇到问题时查"这个事件有没有对应的频道"。比如想回答"请求从排队到发出花了多久",取 request_scheduled 与 request_left_downloader 的时间差即可——这比翻框架源码打补丁便宜太多。

# 运行日志中,信号驱动的扩展输出长这样 2026-08-29 12:40:11 [bookstation] INFO: 本次旅程共产出 340 条数据,关闭原因 finished

信号与中间件怎么选

两者都能"在旅程中插手",分界线是:中间件能改数据流,信号只能看。要改请求头、拦响应,用中间件;要计数、告警、收尾,用信号。把观测逻辑写进中间件是常见的过度设计——观测代码不该有能力影响主流程,这是权限最小化原则在爬虫工程里的投影。

⚠️ 常见坑:在信号处理函数里抛异常不会中断爬虫,但会污染日志并可能吞掉后续订阅者的执行。观测代码必须自带兜底——它自己是工具,不该成为新的故障点。

实战案例:用信号定位一次"慢得莫名其妙"

信号最出彩的场景是回答"时间都花在哪了"。某次排查:爬虫跑得比平时慢一倍,统计里响应延迟却正常。用两个调度相关信号打点,把每张票的排队耗时算出来:

import time class LatencyProbeExtension: def __init__(self, crawler): self.crawler = crawler self.scheduled = {} # 请求指纹 到 调度时刻 crawler.signals.connect(self.on_scheduled, signal=signals.request_scheduled) crawler.signals.connect(self.on_left, signal=signals.request_left_downloader) @classmethod def from_crawler(cls, crawler): return cls(crawler) def on_scheduled(self, request, spider): self.scheduled[request.fingerprint] = time.monotonic() def on_left(self, request, spider): t0 = self.scheduled.pop(request.fingerprint, None) if t0: wait = time.monotonic() - t0 if wait > 5: spider.logger.warning("票排队 %s 秒才发车: %s", round(wait, 1), request.url)

运行结果很快给出答案:大量票排队超过 5 秒,且集中在同一域名。排队久而下载快,说明瓶颈在候车侧的并发闸门——单域并发太保守。把 CONCURRENT_REQUESTS_PER_DOMAIN 上调后恢复正常。这次排查的模式值得记下:先分清"慢在排队还是慢在下载",再决定调哪组参数——两类慢的药方完全不同,而信号是把两者分开的唯一干净手段。

本节要点回顾

  • 信号是只读广播:看可以、改不行,改数据流是中间件的职权;
  • 扩展类是标准挂载方式:from_crawler 工厂加 signals.connect,随爬虫生命周期存亡;
  • 高频三信号:spider_closed 收尾、item_scraped 计数、response_downloaded 观速;
  • 处理函数要自带兜底:观测代码不许成为新故障源。

候车大厅到此走完:排队、查重、拨盘、广播,你已能解释请求发出前的一切。下一章,票真正上路——下行隧道里的中间件与下载器,是全框架对抗最激烈的一段。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U