bigywater.tech
Scrapy · 3

CrawlerProcess 与 CrawlerRunner:谁来启动和关闭 reactor

在脚本里启动 Scrapy,有 CrawlerProcess、CrawlerRunner 和 2.19 里新出现的 Async 版本。谁装 reactor、谁停 reactor、能不能跑第二次、Ctrl-C 之后发生什么——这篇一项项实测。

ReactorNotRestartable
同一个进程里第二次 start() 抛出的异常
0.62 vs 1.39 s
两个 spider:CrawlerProcess 并发与 CrawlerRunner 顺序的总耗时
2 次 SIGINT
第一次优雅停止(等在途请求),第二次强制退出

start() 只能一次

实测 · 本机执行输出
  $ CrawlerProcess(SET); p.crawl(A); p.start(); p.crawl(A); p.start()
    第一次 start() 返回,耗时 0.62 s
    第二次: twisted.internet.error.ReactorNotRestartable
  $ AsyncCrawlerProcess(SET); p.crawl(A); p.start(); p.crawl(A); p.start()
    第一次 start() 返回,耗时 0.78 s
    第二次: twisted.internet.error.ReactorNotRestartable

reactor 停下后不能再启动,所以脚本里要么把所有 crawl() 排好再 start() 一次,要么自己管 reactor。

并发与顺序

实测 · 本机执行输出
  CrawlerProcess:crawl(A); crawl(B); start()  总耗时 0.62 s
  CrawlerRunner:yield crawl(A); yield crawl(B)  总耗时 1.39 s
  两种方式服务器收到的请求数:20、20(都是 2 × 10)

CrawlerProcess 把多个 crawler 交给同一个 reactor 并发跑;CrawlerRunner 只给你 Deferred,想顺序就自己链起来。

Runner 不管 reactor

实测 · 本机执行输出
  (a) 没有先 install_reactor 就 CrawlerRunner(SET).crawl(A):退出码 0
      builtins.RuntimeError: We expected a Twisted reactor to be installed but it isn't.
  (b) 装了 reactor,crawl 结束后没有 reactor.stop():5 秒内输出 'crawl 的 Deferred 触发了\n[超时被杀]',退出码 -9(-9 = 超时被杀:Deferred 触发了,但 reactor.run() 一直不返回,进程不会自己退出)
  (c) d.addBoth(lambda _: reactor.stop()):'reactor.run() 返回了',退出码 0,耗时 1.14 s

CrawlerRunner 要求 reactor 已经装好、爬完以后由你停掉;忘了 stop 进程不会退出。

asyncio 程序里

实测 · 本机执行输出
  装了 asyncio reactor + asyncio.run(main()):退出码 -9,耗时 8.0 s;输出 [超时被杀](8 秒超时被杀,卡死)
  TWISTED_REACTOR_ENABLED=False + asyncio.run(main()):退出码 0,耗时 1.7 s;输出 crawl 1 完成 / crawl 2 完成 / asyncio.run 返回
  装了 asyncio reactor + Deferred.fromFuture(main()) + reactor.run():退出码 0,耗时 1.7 s;输出 crawl 1 完成 / crawl 2 完成 / reactor.run 返回

2.19 的 AsyncCrawlerRunner 返回可 await 的任务,但事件循环要和 reactor 是同一个,或者干脆用无 reactor 模式。

信号

实测 · 本机执行输出
  发一次 SIGINT:进程退出码 0,从启动到退出共 3.5 s(第 1.5 s 发的信号)
      [scrapy.crawler] INFO: Received SIGINT, shutting down gracefully. Send again to force
      [scrapy.core.engine] INFO: Closing spider (shutdown)
      start() 返回
      finish_reason=shutdown;response_received_count=4;服务器收到 4 个请求
  间隔 0.5 s 连发两次 SIGINT:进程退出码 0,从启动到退出共 2.5 s(第 1.5 s 发的信号)
      [scrapy.crawler] INFO: Received SIGINT, shutting down gracefully. Send again to force
      [scrapy.core.engine] INFO: Closing spider (shutdown)
      [scrapy.crawler] INFO: Received SIGINT twice, forcing unclean shutdown
      start() 返回
      finish_reason=None;response_received_count=None;服务器收到 4 个请求

CrawlerProcess 装了 SIGINT/SIGTERM 处理:第一次让引擎优雅关闭,第二次强制退出。

分步演示

启动与停止
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / Scrapy 2.19.0 / Twisted 26.4.0 / lxml 6.1.3 / parsel 1.12.1(临时虚拟环境);所有请求只发给本机自己起的测试服务器(127.0.0.1,端口 9960 以上,不访问任何真实网站);服务器可以按实验需要制造延迟、重定向、压缩、断连等行为,并记录收到的原始请求;每个实验在临时目录里新建 Scrapy 项目或单文件 spider,命令输出来自真实的 scrapy 进程;计时用 time.perf_counter,数字每次运行会有小幅抖动。每个情形是独立进程里的一个小脚本(10 个页面,服务器端 time.sleep 制造延迟);第五节用 subprocess.Popen 启动脚本再 send_signal(SIGINT)
    • 没有验证:SIGTERM 的行为;在 Windows 上的信号处理;和别的 asyncio 库(aiohttp 服务器、FastAPI)放在同一个事件循环里的细节;CrawlerRunner 并发多个 crawler 时的 settings 隔离;AsyncCrawlerProcess 的 stop_after_crawl=False 用法;强制退出时 pipeline、feed 是否丢数据
    京ICP备2026067502号-1京公网安备11010502063487号