集成阅读约 8 分钟

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

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 请求头中(文档),所以 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”(发行说明)。替代它的是 Spider.start()。迁移方法(包括 spider 中间件)见 Scrapy 抓取 0 页:start_requests() 已移除。

轮换代理:为每个请求设置 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 发行说明给出了原因:轮换中间件更换了代理却留下旧的请求头,结果一个代理的凭据被发给了另一个代理。从 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 代理配置)。代理环境变量比较了其他客户端。

TunnelError: Could not open CONNECT tunnel with proxy

code
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 中的顺序逐项排查。

  • 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

code
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)。之后回调收到了状态码 407 以及 Proxy-Authenticate: Basic realm="lab"。三者都不用时,被丢弃的响应会计入爬取统计中的 httperror/response_ignored_status_count/407。

UnsupportedURLSchemeError: Unsupported scheme: b'socks5'

code
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 一起发布,文档写明它支持 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 展示了其他客户端如何处理这两种协议。它的报错写法不同:被拒绝的 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)。这样目标只收到一次 POST,errback 收到 scrapy.exceptions.DownloadFailedError。代理重试:丢失一个响应,创建了两个任务展示了幂等键能改变什么。

每次重试都是又一个经过代理的请求。代理流量需要多少 GB 实测了只抓取 HTML 时的流量消耗。如果想在编码代理中获得诊断帮助,Proxy Toolkit 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 网页抓取实测。

实验归档包含运行脚本、用例、代理、SOCKS5 服务器、README 和记录的结果。

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

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

参考来源与延伸阅读

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