# Scrapy 抓取 0 页：start_requests() 已移除

Source: https://ipvolt.com/zh/guides/scrapy-start-requests-not-called
Markdown: https://ipvolt.com/zh/guides/scrapy-start-requests-not-called.md
Language: zh-CN

[ipvolt 首页](https://ipvolt.com/zh.md) / [指南](https://ipvolt.com/zh/guides.md) / Scrapy 抓取 0 页：start_requests() 已移除

故障排查
发布于: 2026-10-10
阅读约 6 分钟
作者： ipvolt

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

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

```text
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()`。[发行说明](https://docs.scrapy.org/en/latest/news.html#release-2-16-0)写道，它“is removed and no longer called”（[pull request #7490](https://github.com/scrapy/scrapy/pull/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`，退出码 1 | spider 中间件的 `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()`](https://docs.scrapy.org/en/latest/topics/spiders.html#scrapy.Spider.start) 是异步生成器，也就是带 `yield` 的 `async def`。产出的请求本身不用改。有两种疏忽会带来另一种症状：一行 ERROR，仍然没有请求：

```text
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 代理配置](/zh/guides/scrapy-proxy-setup)介绍了轮换和代理环境变量，[修复代理错误 407](/zh/guides/fix-proxy-error-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()`](https://docs.scrapy.org/en/latest/topics/spider-middleware.html#scrapy.spidermiddlewares.SpiderMiddleware.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：

```text
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()` 的最后一个版本系列：

```text
scrapy>=2.15,<2.16
```

在实验中它解析为 2.15.2：它用未修改的爬虫完成了抓取，并输出一条 `ScrapyDeprecationWarning`，其中写明了爬虫类名，并说明该方法“will stop being called in a future version of Scrapy”。请把这种锁定当作临时措施：发行说明在 [2.17.0](https://docs.scrapy.org/en/latest/news.html#release-2-17-0) 中列出了安全修复。迁移完成后，请锁定你测试过的版本。在没有 C 编译器的 Python 3.15 上，`pip install --only-binary brotli scrapy` 会悄悄装上 Scrapy 2.17.0 而不是 2.19.0；[Python 3.15 网页抓取实测](/zh/blog/python-3-15-web-scraping)介绍了如何发现这种情况。

## 在 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 运行。没有使用任何代理账号。

[实验压缩包](https://ipvolt.com/downloads/scrapy-start-requests-not-called/scrapy-start-requests-lab.zip)包含[运行脚本](https://ipvolt.com/downloads/scrapy-start-requests-not-called/run_lab.py)、各个爬虫、[pytest 检查](https://ipvolt.com/downloads/scrapy-start-requests-not-called/ci/test_crawl.py)、附有全部结果表格的 [README](https://ipvolt.com/downloads/scrapy-start-requests-not-called/README.md) 以及[记录的结果](https://ipvolt.com/downloads/scrapy-start-requests-not-called/results.json)。

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

爬虫重新发出请求后，每个请求都会计入代理套餐的流量。[代理流量需要多少 GB](/zh/blog/how-many-gb-of-proxies-do-i-need)实测了只抓取 HTML 时的用量。

ipvolt 是一项面向开发者的代理服务，目前尚未开放；[加入早期访问名单](https://ipvolt.com/#waitlist-closing)，开放时你会收到一封邮件。

## 参考来源与延伸阅读

- [Scrapy release notes: 2.16.0, deprecation removals](https://docs.scrapy.org/en/latest/news.html#release-2-16-0)
- [Scrapy documentation: Spider.start()](https://docs.scrapy.org/en/latest/topics/spiders.html#scrapy.Spider.start)
- [Scrapy documentation: SpiderMiddleware.process_start()](https://docs.scrapy.org/en/latest/topics/spider-middleware.html#scrapy.spidermiddlewares.SpiderMiddleware.process_start)
- [Scrapy documentation: SpiderMiddleware.process_spider_output()](https://docs.scrapy.org/en/latest/topics/spider-middleware.html#scrapy.spidermiddlewares.SpiderMiddleware.process_spider_output)
- [scrapy/scrapy pull request #7490: Drop start_requests() support](https://github.com/scrapy/scrapy/pull/7490)
- [scrapy/scrapy pull request #7504: Remove support for sync process_spider_output()](https://github.com/scrapy/scrapy/pull/7504)
- [Scrapy release notes: 2.17.0, security bug fixes](https://docs.scrapy.org/en/latest/news.html#release-2-17-0)

## 相关指南

- [Scrapy 2.19 代理配置：认证、轮换与常见错误](https://ipvolt.com/zh/guides/scrapy-proxy-setup.md)
- [不靠猜测修复代理错误 407](https://ipvolt.com/zh/guides/fix-proxy-error-407.md)
- [代理环境变量：HTTP_PROXY 与 NO_PROXY](https://ipvolt.com/zh/guides/proxy-environment-variables.md)

## 关于 ipvolt

示例使用通用的代理设置，并附上原始技术文档链接。具体产品的行为请向你的服务商确认。ipvolt 仍在开发中。

[阅读英文原文](https://ipvolt.com/guides/scrapy-start-requests-not-called.md)

## 了解何时开放体验。

ipvolt · 开发中

我们正在为开发者和数据团队打造代理基础设施。加入意向名单，ipvolt 就绪时第一时间收到通知。

开放体验时仅发一封通知邮件，不发送其他邮件。

[申请抢先体验](https://ipvolt.com/zh/guides/scrapy-start-requests-not-called#waitlist-closing)

[隐私政策](https://ipvolt.com/privacy)

