故障排查阅读约 6 分钟

Scrapy 抓取 0 页:start_requests() 已移除

Scrapy 2.16 移除了 start_requests():爬虫输出 Crawled 0 pages 后以 finished 关闭且无警告。改用 async def start(),已在 2.19 实测。

本页内容

升级到 Scrapy 2.16 或更高版本后,只定义了 start_requests() 的爬虫会正常启动,不发出任何请求,然后像完成了一样关闭:

code
2026-10-09 12:33:33 [scrapy.core.engine] INFO: Spider opened
2026-10-09 12:33:33 [scrapy.extensions.logstats] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2026-10-09 12:33:33 [scrapy.core.engine] INFO: Closing spider (finished)
2026-10-09 12:33:33 [scrapy.core.engine] INFO: Spider closed (finished)

这是 Scrapy 2.19.0 一次运行中引擎和 logstats 输出的行。日志里没有任何 WARNING 或 ERROR 行,退出码是 0,parse() 从未执行。统计信息里有 'finish_reason': 'finished',没有 downloader/request_count 这个键。

原因:Scrapy 2.16.0(2026 年 5 月 19 日)移除了自 2.13.0 起弃用的 Spider.start_requests()。发行说明写道,它“is removed and no longer called”(pull request #7490)。解决办法是把这个方法改名为 async def start():

python
# Before: Scrapy 2.16 and later never call this
def start_requests(self):
    yield scrapy.Request("https://example.com/", callback=self.parse)
python
# After
async def start(self):
    yield scrapy.Request("https://example.com/", callback=self.parse)

只做这一处修改,同一个爬虫就在 Scrapy 2.16.0 和 2.19.0 上抓到了页面。

在 2.16+ 上看到的现象原因解决办法
Crawled 0 pages,随后 Spider closed (finished),没有警告爬虫只定义了 start_requests()改名为 async def start()
抓取正常进行,但中间件不再修改起始请求spider 中间件只定义了 process_start_requests()添加 async def process_start()
TypeError: Middleware ... doesn't support asynchronous spider output,退出码 1spider 中间件的 process_spider_output() 是同步方法改成异步生成器

下面的全部结果记录于 2026 年 10 月 9 日,环境为 Scrapy 2.19.0、Twisted 26.4.0 和 Python 3.14.4,并在 Scrapy 2.16.0、2.15.2 和 2.12.0 上重复。只设置 start_urls、两个方法都不定义的爬虫不受影响:它在四个版本上都能抓取。

把 start_requests() 迁移到 async def start()

Spider.start() 是异步生成器,也就是带 yield 的 async def。产出的请求本身不用改。有两种疏忽会带来另一种症状:一行 ERROR,仍然没有请求:

code
TypeError: e1_start_not_async.LabSpider.start() must be an asynchronous generator, i.e. an async def method with yield statements, got <class 'generator'>

这是没有写 async 的 def start()。如果 async def start() 返回的是列表,错误信息的结尾是 got <class 'coroutine'>。两种情况下 Scrapy 2.19.0 都以 'finish_reason': 'start_error' 关闭爬虫,退出码仍然是 0。

如果爬虫重写 start_requests() 是为了给 start_urls 里的请求加请求头或 meta,请保留这个循环:

python
async def start(self):
    for url in self.start_urls:
        yield scrapy.Request(
            url,
            headers={"Accept-Language": "en-AU"},
            meta={"page": 1},
            dont_filter=True,
        )

默认的 start() 就是同样的循环,只带 dont_filter=True,所以请保留这个参数。在实验中,回调从 response.request.headers 读到了请求头,从 response.meta 读到了 page。

代理仍然写在 meta 里:

python
async def start(self):
    yield scrapy.Request(
        "https://example.com/",
        meta={"proxy": "http://user:pass@proxy.example.com:8080"},
    )

实验里的 http:// 请求带着有效凭据到达了一个要求 Basic 认证的本地代理。Scrapy 2.19 代理配置介绍了轮换和代理环境变量,修复代理错误 407介绍了代理拒绝凭据时的处理。

同时支持 2.13 以下和 2.16+:两个方法都定义

需要在这次变更两侧都能运行的代码,要同时定义两个方法。发行说明是这样写的:“Use start() instead, or both to maintain support for lower Scrapy versions.”

python
async def start(self):
    for request in self.start_requests():
        yield request

def start_requests(self):
    yield scrapy.Request("https://example.com/", callback=self.parse)

Scrapy 2.12.0 调用了 start_requests(),2.15.2、2.16.0 和 2.19.0 调用了 start()。每个版本都抓到了页面,2.15.2 没有输出弃用警告。反过来的错误同样没有提示:只有 async def start() 的爬虫在 2.12.0 上没有发出任何请求,并以 finished 关闭。

spider 中间件:process_start_requests() 被跳过且没有警告

同一个版本还移除了 spider 中间件对 process_start_requests() 的支持。实验用的中间件会给每个起始请求设置 meta["tagged"] = True。在 2.19.0 上配合已迁移的爬虫,页面被抓取,回调输出 tagged=None,日志里没有警告:这个方法从未被调用。Scrapy 2.15.2 会用 ValueError 终止同样的组合,所以升级把一个错误变成了沉默。

把它换成 process_start():

python
class TagStartMiddleware:
    async def process_start(self, start):
        async for item_or_request in start:
            if isinstance(item_or_request, scrapy.Request):
                item_or_request.meta["tagged"] = True
            yield item_or_request

这样回调在 2.15.2、2.16.0 和 2.19.0 上都输出了 tagged=True。如果还要支持 2.13 以下的 Scrapy,请把 process_start_requests() 留在旁边。中间件和爬虫都同时定义两个方法时,请求在四个版本上都被打上了标记。

TypeError: Middleware doesn't support asynchronous spider output

同步的 process_spider_output() 会直接报错。抓取在 Spider opened 之前就停止,退出码为 1:

code
TypeError: Middleware d2_mw_sync_output.StampMiddleware doesn't support asynchronous spider output. Its process_spider_output() method should be an async generator function or it should additionally define a process_spider_output_async() method.

d2_mw_sync_output.StampMiddleware 是实验用的中间件,你会在这个位置看到自己的中间件。Scrapy 2.15.2 能运行同一个中间件,并警告它“will stop working in a future version of Scrapy”。把这个方法改成异步生成器:

python
class StampMiddleware:
    async def process_spider_output(self, response, result):
        async for item_or_request in result:
            if isinstance(item_or_request, dict):
                item_or_request["stamped"] = True
            yield item_or_request

这个签名里没有 spider 参数。带上它时,2.19.0 仍会运行中间件,并输出 ScrapyDeprecationWarning: StampMiddleware.process_spider_output() requires a spider argument。已经同时有同步 process_spider_output() 和 process_spider_output_async() 的中间件加载时没有报错,2.19.0 执行的是异步方法。如果错误信息里的类来自已安装的第三方包,请查找支持你所用 Scrapy 版本的新版本。

确认正在运行的 Scrapy 版本

sh
scrapy version
python -m pip show scrapy

在实验中,第一条命令输出 Scrapy 2.19.0,第二条输出 Version: 2.19.0。抓取日志的第一行也会写明版本:Scrapy 2.19.0 started (bot: scrapybot)。请在实际运行抓取的环境里执行这些命令,例如虚拟环境、容器镜像或 CI 任务。

requirements.txt 里只写 scrapy 的一行,会在下次构建时安装最新版本。如果今天无法迁移,可以锁定仍会调用 start_requests() 的最后一个版本系列:

code
scrapy>=2.15,<2.16

在实验中它解析为 2.15.2:它用未修改的爬虫完成了抓取,并输出一条 ScrapyDeprecationWarning,其中写明了爬虫类名,并说明该方法“will stop being called in a future version of Scrapy”。请把这种锁定当作临时措施:发行说明在 2.17.0 中列出了安全修复。迁移完成后,请锁定你测试过的版本。在没有 C 编译器的 Python 3.15 上,pip install --only-binary brotli scrapy 会悄悄装上 Scrapy 2.17.0 而不是 2.19.0;Python 3.15 网页抓取实测介绍了如何发现这种情况。

在 CI 中发现零请求的抓取

退出码为 0 且 finish_reason 为 finished 时,只检查抓取是否成功的任务会通过。请对请求数做断言:

python
from scrapy.crawler import CrawlerProcess

from myproject.spiders.quotes import QuotesSpider


def test_spider_sends_requests():
    process = CrawlerProcess(settings={"LOG_LEVEL": "WARNING"})
    crawler = process.create_crawler(QuotesSpider)
    process.crawl(crawler)
    process.start()
    assert crawler.stats.get_value("downloader/request_count", 0) > 0

在 2.16.0 和 2.19.0 上,这个测试对使用 start_requests() 的爬虫失败,信息为 AssertionError: assert 0 > 0,改名后通过。在 2.15.2 上两者都通过。实验中每个 pytest 进程只运行一次抓取,目标是本地的 python -m http.server,所以请让你的测试指向测试服务器或你有权请求的页面。

对定时任务,可以从日志末尾的统计信息里读取同一个数字。发出过请求的运行里有 'downloader/request_count' 这一项,失败的那次运行里没有。

测试方法

环境为 Ubuntu 26.04.1 和 Python 3.14.4,全部运行在 127.0.0.1 上:python -m http.server 作为目标,另有一个要求 Basic 认证的转发代理。四个虚拟环境分别安装了 Scrapy 2.19.0 和 2.16.0(Twisted 26.4.0)、2.15.2(Twisted 25.5.0)以及 2.12.0(Twisted 24.11.0)。17 个爬虫各自在每个版本上用 scrapy runspider 在全新的解释器和干净的环境中运行:共 68 次运行,另有 8 次 pytest 运行。没有使用任何代理账号。

实验压缩包包含运行脚本、各个爬虫、pytest 检查、附有全部结果表格的 README 以及记录的结果。

未测试:macOS 和 Windows、Scrapy 2.13、2.14、2.17 和 2.18、CrawlSpider 规则以及第三方中间件。

爬虫重新发出请求后,每个请求都会计入代理套餐的流量。代理流量需要多少 GB实测了只抓取 HTML 时的用量。

ipvolt 是一项面向开发者的代理服务,目前尚未开放;加入早期访问名单,开放时你会收到一封邮件。

参考来源与延伸阅读

本指南参考的技术资料。请以你所安装版本的文档以及代理服务商支持的配置为准。