把凭据写进代理 URL,再通过 meta["proxy"] 传入:
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:
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)DOWNLOADER_MIDDLEWARES = {"myproject.middlewares.RotateProxyMiddleware": 740}在实验中,一个被重定向的请求两次经过这个中间件,分别去了两个代理,并且在两个代理上都通过了认证。
会失败的写法是设置不带凭据的 URL,再自己写请求头:
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"] 继续可用,可以用一个丢弃已读取内容的子类替换这个中间件:
from scrapy.downloadermiddlewares.httpproxy import HttpProxyMiddleware
class ExplicitProxyMiddleware(HttpProxyMiddleware):
def __init__(self, auth_encoding="latin-1"):
super().__init__(auth_encoding)
self.proxies = {}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
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
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 没有重试这个请求。
要在回调中拿到这个响应,请为该请求放行这个状态码:
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'
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,并把它标为实验性、暂不建议用于生产环境。
python -m pip install "scrapy[httpx]"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 是一项面向开发者的代理服务,目前尚未开放;加入早期访问名单,开放时你会收到一封邮件。
参考来源与延伸阅读
本指南参考的技术资料。请以你所安装版本的文档以及代理服务商支持的配置为准。
- Scrapy documentation: HttpProxyMiddleware and HTTPPROXY_ENABLED
- Scrapy documentation: Request.meta special keys (proxy, dont_retry, handle_httpstatus_list)
- Scrapy documentation: HttpErrorMiddleware and HTTPERROR_ALLOWED_CODES
- Scrapy documentation: RetryMiddleware
- Scrapy documentation: Spider.start()
- Scrapy documentation: HttpxDownloadHandler
- Scrapy release notes: 2.16.0, start_requests() removed
- Scrapy release notes: 2.6.2, Proxy-Authorization security fix
- Scrapy release notes: 2.7.1, explicit Proxy-Authorization header