bigywater.tech
Scrapy · 5

Spider 的起始请求:start() 与 start_requests 的惰性消费

Spider 的起始请求怎么发出去?2.13 起 start() 取代了 start_requests(),2.19 里旧的方法已经不再被调用。这篇实测 start() 的默认行为、惰性消费的真相、await 与 yield item、起始请求的出队顺序和出错时的收尾。

0 个请求
只写 start_requests 的 spider 在 2.19 里发出的请求数(无警告)
295
start() 直接 yield 300 个请求时调度器里的最大积压
start_error
start() 抛异常时的 finish_reason

默认 start()

实测 · 本机执行输出
  start_urls = [/a, /a, /b](/a 重复):服务器收到 ['/a', '/a', '/b'],共 3 个请求
  parse 看到的:a(is_start_request=True, dont_filter=True);b(is_start_request=True, dont_filter=True);a(is_start_request=True, dont_filter=True)
  dupefilter/filtered=0

Spider.start() 的默认实现遍历 start_urls,对每个 URL yield Request(url, dont_filter=True),所以重复的起始 URL 都会被请求。StartSpiderMiddleware 给这些请求的 meta 加上 is_start_request。

start_requests 已失效

实测 · 本机执行输出
  只定义 start_requests:服务器收到 0 个请求;start_requests 被调用:False;finish_reason=finished;response_received_count=0
  日志里 WARNING / ERROR 行数:0(没有任何警告或提示,spider 安安静静地以 finished 结束)

2.19 的引擎只调用 start(),旧的 start_requests() 不再被调用,而且不会有任何提示。需要兼容老版本时两个方法都写。

不是惰性的

实测 · 本机执行输出
  直接 yield(300 个)
      前 8 次 yield 时 已完成 [0, 0, 0, 0, 0, 0, 0, 0] / 调度器积压 [0, 0, 0, 0, 0, 1, 2, 3]
      调度器积压的最大值 295;第 150 次 yield 在 163 ms,最后一次(第 300 次)在 173 ms;总耗时 8.1 s(理想 7.5 s);300 个响应
  每次 yield 前 while engine.needs_backout(): await asyncio.sleep(0.01)(300 个)
      前 8 次 yield 时 已完成 [0, 0, 0, 0, 4, 4, 4, 4] / 调度器积压 [0, 0, 0, 0, 0, 0, 0, 0]
      调度器积压的最大值 0;第 150 次 yield 在 4296 ms,最后一次(第 300 次)在 8432 ms;总耗时 8.4 s(理想 7.5 s);300 个响应

引擎循环里,下载器已满时直接继续取 start() 的下一个值,不等待。所以大量起始请求会一次性进入调度器。想按下载节奏出请求,在 start() 里自己检查 engine.needs_backout()。

可以 await

实测 · 本机执行输出
      158 ms  yield      i=0
      161 ms  parse      p0
      660 ms  yield      i=1
      665 ms  parse      p1
     1163 ms  yield      i=2
     1166 ms  parse      p2
     1666 ms  yield      i=3
     1668 ms  parse      p3
     2168 ms  start() 结束
     2168 ms  spider_idle
  finish_reason=finished(spider_idle 只在 start() 结束之后才出现)

start() 是异步生成器,可以在 yield 之间 await(比如从外部队列取任务),已经 yield 的请求马上被处理;引擎在 start() 耗尽之前不会认为 spider 空闲。

可以直接 yield item

实测 · 本机执行输出
  pipeline 看到的 item:[{'from': 'start'}, {'from': 'parse'}];item_scraped_count=2;服务器收到 1 个请求

start() 里 yield 的 item 不经过回调,直接进 pipeline。

起始请求的顺序

实测 · 本机执行输出
  默认设置:服务器收到的顺序 /a /b /a2 /b2 /b1 /a1 /c /c2 /c1
  SCHEDULER_START_MEMORY_QUEUE 改成 LifoMemoryQueue:服务器收到的顺序 /a /c /a2 /c2 /c1 /a1 /b /b2 /b1

起始请求按 yield 顺序走 FIFO 队列,其它请求默认后进先出且优先于起始请求(同优先级时)。设置 SCHEDULER_START_MEMORY_QUEUE 可以改。

start() 出错

实测 · 本机执行输出
  抛 RuntimeError:服务器收到 ['/first'];finish_reason=start_error;spider_exceptions/count=1;parse 被调用 1 次;日志:[scrapy.core.engine] ERROR: Error while reading start items and requests: boom i
  抛 CloseSpider(reason="my_reason"):服务器收到 ['/first'];finish_reason=my_reason;spider_exceptions/count=0;parse 被调用 1 次

异常不会让已 yield 的请求作废。普通异常记 ERROR 并把 finish_reason 设为 start_error;CloseSpider 则使用你给的原因。

分步演示

start() 的行为
本步的对象

    本文用到的工具与边界

    • 本机 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,数字每次运行会有小幅抖动。每个情形是一个单文件 spider(scrapy runspider),事件用 EV() 写到 events.jsonl;第三节读取 engine.scheduler 的长度(2.19 新增的公开属性)和 downloader.active
    • 没有验证:真正几十万个起始请求时的内存占用和事件循环被占住的时间;needs_backout 轮询间隔(0.01 s)对吞吐的影响;JOBDIR(磁盘队列)下 start 请求的顺序;start() 里同时有 await 外部队列(比如 Redis)的用法;start_requests 兼容层是否在更早的 2.x 版本里存在(只在 2.19.0 上确认)
    京ICP备2026067502号-1京公网安备11010502063487号