# Scrapy 2.19 代理配置：认证、轮换与常见错误

Source: https://ipvolt.com/zh/guides/scrapy-proxy-setup
Markdown: https://ipvolt.com/zh/guides/scrapy-proxy-setup.md
Language: zh-CN

[ipvolt 首页](https://ipvolt.com/zh.md) / [指南](https://ipvolt.com/zh/guides.md) / Scrapy 2.19 代理配置：认证、轮换与常见错误

集成
发布于: 2026-10-07
阅读约 8 分钟
作者： ipvolt

Scrapy 2.19 代理配置：把凭据写进 meta["proxy"]、按请求轮换、HTTP_PROXY 环境变量，以及 TunnelError 407、被忽略的 407 响应和 socks5 报错的处理方法。

把凭据写进代理 URL，再通过 `meta["proxy"]` 传入：

```python
import scrapy

PROXY = "http://user:pass@proxy.example.com:8080"


class ExampleSpider(scrapy.Spider):
    name = "example"

    async def start(self):
        yield scrapy.Request("https://example.com/", meta={"proxy": PROXY})

    def parse(self, response):
        self.logger.info("%s via %s", response.status, response.meta["proxy"])
```

`HttpProxyMiddleware` 默认启用（`HTTPPROXY_ENABLED = True`）。它会把 URL 里的用户名和密码移到 `Proxy-Authorization` 请求头中（[文档](https://docs.scrapy.org/en/latest/topics/downloader-middleware.html#module-scrapy.downloadermiddlewares.httpproxy)），所以 `response.meta["proxy"]` 里是不带凭据的 URL。

在我们的实验环境中，这种写法让 `https://` 和 `http://` 请求都通过了一个要求 Basic 认证的本地代理。下文的所有结果记录于 2026 年 10 月 7 日，使用 Scrapy 2.19.0、Twisted 26.4.0 和 Python 3.14.4。

讲错误之前，先说三个细节：

- **对密码做百分号编码**。`p@ss:w/rd` 写成 `p%40ss%3Aw%2Frd` 后可以使用。不编码时，请求以 `ValueError: Port could not be cast to integer value as 'w'` 失败。
- **在回调里创建的请求不会继承代理**。用 `response.follow()` 创建的请求直接连到了目标。重定向则保留了代理。请在每个请求上设置 `meta["proxy"]`，或者像下文那样放进下载器中间件。
- **使用 `async def start()`**。只定义了 `start_requests()` 的爬虫启动后没有发出任何请求，随后以 `finish_reason: finished` 关闭，没有任何警告。Scrapy 2.16.0 移除了这个方法：它“is removed and no longer called”（[发行说明](https://docs.scrapy.org/en/latest/news.html#release-2-16-0)）。替代它的是 [`Spider.start()`](https://docs.scrapy.org/en/latest/topics/spiders.html#scrapy.Spider.start)。迁移方法（包括 spider 中间件）见 [Scrapy 抓取 0 页：start_requests() 已移除](/zh/guides/scrapy-start-requests-not-called)。

## 轮换代理：为每个请求设置 meta["proxy"]

轮换是一个下载器中间件，它在 `HttpProxyMiddleware`（顺序 750）之前运行，并设置带凭据的代理 URL：

```python
from itertools import cycle

PROXIES = cycle([
    "http://user:pass@proxy-a.example.com:8080",
    "http://user:pass@proxy-b.example.com:8080",
])


class RotateProxyMiddleware:
    def process_request(self, request):
        request.meta["proxy"] = next(PROXIES)
```

```python
DOWNLOADER_MIDDLEWARES = {"myproject.middlewares.RotateProxyMiddleware": 740}
```

在实验中，一个被重定向的请求两次经过这个中间件，分别去了两个代理，并且在两个代理上都通过了认证。

会失败的写法是设置不带凭据的 URL，再自己写请求头：

```python
request.meta["proxy"] = next(PROXIES)  # "http://proxy-a.example.com:8080"
request.headers["Proxy-Authorization"] = "Basic dXNlcjpwYXNz"
```

第一个请求带着请求头到达代理 A。目标返回 301，重定向后的请求再次经过各个中间件。轮换选中了代理 B 并再次设置了请求头，但代理 B 收到的请求里没有 `Proxy-Authorization`，于是返回 407。被重试的 `https://` 请求也以同样的方式丢掉了请求头。

`HttpProxyMiddleware` 会记录手动设置的请求头是随哪个代理 URL 发出的。同一个请求再次经过时，如果代理 URL 变了而且不带凭据，它就删除这个请求头。[2.6.2 发行说明](https://docs.scrapy.org/en/latest/news.html#release-2-6-2)给出了原因：轮换中间件更换了代理却留下旧的请求头，结果一个代理的凭据被发给了另一个代理。从 [2.7.1](https://docs.scrapy.org/en/latest/news.html#release-2-7-1) 起，手动设置的请求头只在“for as long as that proxy URL remains the same”时保留。URL 中的凭据总是会替换它。

同一段代码还有两个结果：

- 第一次经过时，请求头还没有和某个代理绑定。一个在 `HttpProxyMiddleware` 运行之前把 `meta["proxy"]` 从 A 改成 B 的中间件，把手动设置的请求头发给了代理 B。
- 完全没有代理时，请求头在请求到达目标之前就被删除了。

## 环境变量：HTTP_PROXY 默认生效

Scrapy 在爬取启动时读取代理环境变量，不需要任何设置来开启。设置了 `HTTP_PROXY=http://user:pass@127.0.0.1:8888` 且没有 `meta["proxy"]` 时，实验中的请求经过了代理并通过了认证。

- `HTTP_PROXY` 和小写的 `http_proxy` 作用于 `http://` URL，`HTTPS_PROXY` 作用于 `https://` URL。只设置 `HTTP_PROXY` 时，`https://` 请求直接连接。`ALL_PROXY` 被忽略。
- `NO_PROXY=127.0.0.1` 让请求直接连接。`meta["proxy"]` 的优先级高于这些变量，并且不理会 `NO_PROXY`，这与文档的说明一致。
- 在设置了变量的情况下，`meta={"proxy": None}` 让单个请求直接连接。
- `HTTPPROXY_ENABLED = False` 让 Scrapy 不再读取这些变量。它同时也关掉了凭据处理：此时 `meta["proxy"] = "http://user:pass@127.0.0.1:8888"` 对 `http://` URL 抛出 `ValueError: invalid hostname: user:pass@127.0.0.1`，对 `https://` 则是 407 的 `TunnelError`。

想忽略环境变量又让 `meta["proxy"]` 继续可用，可以用一个丢弃已读取内容的子类替换这个中间件：

```python
from scrapy.downloadermiddlewares.httpproxy import HttpProxyMiddleware


class ExplicitProxyMiddleware(HttpProxyMiddleware):
    def __init__(self, auth_encoding="latin-1"):
        super().__init__(auth_encoding)
        self.proxies = {}
```

```python
DOWNLOADER_MIDDLEWARES = {
    "scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware": None,
    "myproject.middlewares.ExplicitProxyMiddleware": 750,
}
```

`proxies` 不是文档中记载的属性，所以升级之后请重新验证。aiohttp 正好相反：除非传入 `trust_env=True`，否则它会忽略这些变量（[aiohttp 3.14 代理配置](/zh/guides/aiohttp-proxy-setup)）。[代理环境变量](/zh/guides/proxy-environment-variables)比较了其他客户端。

## TunnelError: Could not open CONNECT tunnel with proxy

```text
scrapy.core.downloader.handlers.http11.TunnelError: Could not open CONNECT tunnel with proxy 127.0.0.1:8888 [{'status': 407, 'reason': b'Proxy Authentication Required'}]
```

代理拒绝了 `https://` URL 的 `CONNECT` 请求。没有凭据和密码错误时的消息完全相同，而且消息里不含凭据，所以它无法告诉你是哪一种情况。先检查失败请求上 `meta["proxy"]` 里的 URL，再按[修复代理错误 407](/zh/guides/fix-proxy-error-407) 中的顺序逐项排查。

- `TunnelError` 在 `RETRY_EXCEPTIONS` 中。使用默认设置时，实验代理收到了三次被拒绝的 `CONNECT`。日志依次显示 `Retrying <GET https://127.0.0.1:8443/> (failed 1 times)`、`(failed 2 times)` 和 `Gave up retrying`。在 `meta` 中加上 `"dont_retry": True` 后，代理只收到一次。
- `handle_httpstatus_list` 在这里不起作用。这里没有响应，只有异常，所以要在 `errback` 中处理。

## Ignoring response 407: HTTP status code is not handled or not allowed

```text
Ignoring response <407 http://127.0.0.1:8080/>: HTTP status code is not handled or not allowed
```

对 `http://` URL 来说，代理返回的 407 是一个普通响应，`HttpErrorMiddleware` 会把它丢弃。回调从未被调用，这行 INFO 日志是唯一的迹象，爬取以 `finish_reason: finished` 结束。设置了 `errback` 时不会记录这行日志，errback 收到的是 `scrapy.spidermiddlewares.httperror.HttpError: Ignoring non-200 response`。Scrapy 没有重试这个请求。

要在回调中拿到这个响应，请为该请求放行这个状态码：

```python
yield scrapy.Request(url, meta={"proxy": PROXY, "handle_httpstatus_list": [407]})
```

爬虫属性 `handle_httpstatus_list = [407]` 和设置项 `HTTPERROR_ALLOWED_CODES = [407]` 效果相同（[HttpErrorMiddleware](https://docs.scrapy.org/en/latest/topics/spider-middleware.html#module-scrapy.spidermiddlewares.httperror)）。之后回调收到了状态码 407 以及 `Proxy-Authenticate: Basic realm="lab"`。三者都不用时，被丢弃的响应会计入爬取统计中的 `httperror/response_ignored_status_count/407`。

## UnsupportedURLSchemeError: Unsupported scheme: b'socks5'

```text
scrapy.exceptions.UnsupportedURLSchemeError: Unsupported scheme: b'socks5'
```

Scrapy 默认的下载处理器不支持 SOCKS。设置 `meta["proxy"] = "socks5://127.0.0.1:1080"` 后，`http://` 请求抛出了一次这个错误，没有重试。`socks5h://` 抛出 `Unsupported scheme: b'socks5h'`，把 SOCKS URL 放进 `HTTP_PROXY` 也抛出同样的错误。

`https://` 请求没有抛出它。Scrapy 把 `CONNECT 127.0.0.1:8443` 当作普通 HTTP 发到了 SOCKS 端口，实验中的 SOCKS 服务器关闭了连接，Scrapy 等满了三次下载超时才报告 `DownloadTimeoutError: Getting https://127.0.0.1:8443/ took longer than 15.0 seconds.` 实验把 `DOWNLOAD_TIMEOUT` 设为 15。默认值是 180。

有两条出路：

- 如果你的提供商有 HTTP 代理入口，就用它。上文的内容全部适用。
- 换用 `HttpxDownloadHandler`。它随 Scrapy 一起发布，[文档](https://docs.scrapy.org/en/latest/topics/download-handlers.html#httpx-handler)写明它支持 SOCKS5，并把它标为实验性、暂不建议用于生产环境。

```sh
python -m pip install "scrapy[httpx]"
```

```python
DOWNLOAD_HANDLERS = {
    "http": "scrapy.core.downloader.handlers._httpx.HttpxDownloadHandler",
    "https": "scrapy.core.downloader.handlers._httpx.HttpxDownloadHandler",
}
```

使用这个处理器时，`socks5://` 对两种协议都返回了 200。对 `http://localhost:8080/`，SOCKS 服务器记录到的是域名，可见该处理器把主机名交给代理解析。`socks5h://` 同样可用。[socks5 与 socks5h](/zh/blog/socks5-vs-socks5h) 展示了其他客户端如何处理这两种协议。它的报错写法不同：被拒绝的 `CONNECT` 表现为 `scrapy.exceptions.DownloadConnectionRefusedError: 407 Proxy Authentication Required`。

## 重试：RetryMiddleware 会重发 POST 请求

`RetryMiddleware` 重试 `POST` 的方式和重试 `GET` 一样。在实验中，一个 `POST` 经过代理到达目标，目标读完请求后没有应答就关闭了连接。Scrapy 记录了 `Retrying <POST http://127.0.0.1:8081/jobs> (failed 1 times)` 并再次发送。目标两次收到相同的请求体，而回调只看到一个 201。

对不能安全重复的请求，请在 `meta` 中设置 `"dont_retry": True`（[RetryMiddleware](https://docs.scrapy.org/en/latest/topics/downloader-middleware.html#module-scrapy.downloadermiddlewares.retry)）。这样目标只收到一次 `POST`，errback 收到 `scrapy.exceptions.DownloadFailedError`。[代理重试：丢失一个响应，创建了两个任务](/zh/blog/proxy-retries-duplicate-jobs)展示了幂等键能改变什么。

每次重试都是又一个经过代理的请求。[代理流量需要多少 GB](/zh/blog/how-many-gb-of-proxies-do-i-need) 实测了只抓取 HTML 时的流量消耗。如果想在编码代理中获得诊断帮助，[Proxy Toolkit MCP](/mcp) 接受经过清理的错误描述。不要把凭据放进它的参数里。

## 测试方法

在 Ubuntu 26.04.1 上，使用 Python 3.14.4、Scrapy 2.19.0、Twisted 26.4.0 和 httpx2 2.13.1，一切都在 `127.0.0.1` 上：`python -m http.server` 作为 HTTP 和 HTTPS 目标，三个要求 Basic 认证并记录每个请求及其请求头名称的代理，一个记录每个目的地的 SOCKS5 服务器，以及一个会丢掉第一次 `POST` 响应的目标。62 个用例中的每一个都在新的解释器和干净的环境中运行 `scrapy runspider`。没有使用任何代理账号。在 Python 3.15.0 上，没有 C 编译器时 `pip install scrapy` 会失败，因为截至 2026 年 10 月 14 日 brotli 还没有 cp315 wheel；经过测试的解决办法见 [Python 3.15 网页抓取实测](/zh/blog/python-3-15-web-scraping)。

[实验归档](https://ipvolt.com/downloads/scrapy-proxy-setup/scrapy-proxy-lab.zip)包含[运行脚本](https://ipvolt.com/downloads/scrapy-proxy-setup/run_lab.py)、[用例](https://ipvolt.com/downloads/scrapy-proxy-setup/cases.py)、[代理](https://ipvolt.com/downloads/scrapy-proxy-setup/auth_proxy.py)、[SOCKS5 服务器](https://ipvolt.com/downloads/scrapy-proxy-setup/socks5_server.py)、[README](https://ipvolt.com/downloads/scrapy-proxy-setup/README.md) 和[记录的结果](https://ipvolt.com/downloads/scrapy-proxy-setup/results.json)。

未测试：macOS 和 Windows、Digest 和 NTLM 代理认证、需要认证的 SOCKS 代理、需要通过 TLS 连接的代理、HTTP/2，以及 Scrapy 基于 aiohttp 的下载处理器。

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

## 参考来源与延伸阅读

- [Scrapy documentation: HttpProxyMiddleware and HTTPPROXY_ENABLED](https://docs.scrapy.org/en/latest/topics/downloader-middleware.html#module-scrapy.downloadermiddlewares.httpproxy)
- [Scrapy documentation: Request.meta special keys (proxy, dont_retry, handle_httpstatus_list)](https://docs.scrapy.org/en/latest/topics/request-response.html#topics-request-meta)
- [Scrapy documentation: HttpErrorMiddleware and HTTPERROR_ALLOWED_CODES](https://docs.scrapy.org/en/latest/topics/spider-middleware.html#module-scrapy.spidermiddlewares.httperror)
- [Scrapy documentation: RetryMiddleware](https://docs.scrapy.org/en/latest/topics/downloader-middleware.html#module-scrapy.downloadermiddlewares.retry)
- [Scrapy documentation: Spider.start()](https://docs.scrapy.org/en/latest/topics/spiders.html#scrapy.Spider.start)
- [Scrapy documentation: HttpxDownloadHandler](https://docs.scrapy.org/en/latest/topics/download-handlers.html#httpx-handler)
- [Scrapy release notes: 2.16.0, start_requests() removed](https://docs.scrapy.org/en/latest/news.html#release-2-16-0)
- [Scrapy release notes: 2.6.2, Proxy-Authorization security fix](https://docs.scrapy.org/en/latest/news.html#release-2-6-2)
- [Scrapy release notes: 2.7.1, explicit Proxy-Authorization header](https://docs.scrapy.org/en/latest/news.html#release-2-7-1)

## 相关指南

- [不靠猜测修复代理错误 407](https://ipvolt.com/zh/guides/fix-proxy-error-407.md)
- [Scrapy 抓取 0 页：start_requests() 已移除](https://ipvolt.com/zh/guides/scrapy-start-requests-not-called.md)
- [代理环境变量：HTTP_PROXY 与 NO_PROXY](https://ipvolt.com/zh/guides/proxy-environment-variables.md)

## 关于 ipvolt

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

[阅读英文原文](https://ipvolt.com/guides/scrapy-proxy-setup.md)

## 了解何时开放体验。

ipvolt · 开发中

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

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

[申请抢先体验](https://ipvolt.com/zh/guides/scrapy-proxy-setup#waitlist-closing)

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

