curl_cffi 代理指南:设置、认证、轮换和异步
curl_cffi 为您提供浏览器形状的 TLS 握手,而代理为您提供干净的出口 IP。以下是如何将两者结合在一起的具体方法——以及为什么您的代理字典中的 https:// 前缀会引发 ErrCode 35。
curl_cffi 是 curl-impersonate 分支的 Python 绑定:它再现了真实浏览器的 TLS/JA3 和 HTTP/2 指纹,而不是将自己标识为 urllib3。这解决了阻止的一个方面。另一个方面是出口 IP,这就是 curl_cffi 代理的用武之地——也是文档较少的地方。官方代理部分大约有十五行,而 2023 年 2 月的一个 GitHub 问题仍然在该主题的前五名中。本指南涵盖了整个表面:proxy 参数、requests 风格的字典及其真实键名、proxy_auth、会话、每次请求的轮换、异步、SOCKS5——以及您将粘贴到搜索框中的确切错误字符串。
curl_cffi 代理语法:优先使用 proxy= 而不是代理字典
curl_cffi 接受两种形式。原生形式是一个单一的 proxy= 字符串,在 v0.6.0 中添加;proxies= 字典存在是为了兼容 requests,文档推荐使用单一参数,除非您确实需要为每个方案使用不同的代理。内部它们合并为同一事物——proxy="..." 变为 {"all": "..."}——并且两者都适用于模块助手、Session、AsyncSession 和单个请求。
# pip install curl_cffi --upgrade (Python 3.10+ since v0.14)
import curl_cffi
PROXY = "http://USER:PASS@gate.quantumproxies.io:PORT"
# Native form — one string, applies to every scheme
r = curl_cffi.get(
"https://tls.browserleaks.com/json",
impersonate="chrome",
proxy=PROXY,
timeout=30,
)
print(r.status_code, r.json()["ja3n_hash"])
# requests-compatible form
r = curl_cffi.get(
"https://httpbin.org/ip",
impersonate="chrome",
proxies={"http": PROXY, "https": PROXY},
timeout=30,
)
print(r.json()) # {'origin': '<proxy exit IP>'}
关于该字典有四件事值得了解,因为这些都不是从 README 中显而易见的:
- 有效的键是
all、http、https、ws和wss。all是通用键;websocket 键仅对 WebSocket 客户端有意义。 - 每个主机的键也有效。
https://api.example.com或all://example.com仅通过给定代理路由该主机——这对于通过住宅 IP 发送一个困难的域名并让其余的直接访问很有用。 - 您不能同时传递两者。 在同一次调用中使用
proxy=加上proxies=会引发TypeError: Cannot specify both 'proxy' and 'proxies',并且在会话级别也会进行相同的检查。 - 环境变量被尊重——
http_proxy、https_proxy、ws_proxy、wss_proxy。当公司变量劫持您的爬虫时,将trust_env=False传递给Session。
关于导入的一个注意事项:自 v0.10.0 起,包可以直接调用(curl_cffi.get,curl_cffi.Session)。较旧的教程使用 from curl_cffi import requests,这仍然有效,但在真正的 requests 库旁边读起来很糟糕——这也解释了为什么网上的一半代码片段看起来像是不同的项目。
https:// 陷阱:ErrCode 35 和 WRONG_VERSION_NUMBER
这个单一的错误比其他所有问题加起来生成了更多的 curl_cffi 代理问题。项目跟踪器中的问题 #6——于 2023 年 2 月同一天开启并关闭——仍然排名第一页,因为它产生的错误看起来像是 TLS 错误而不是配置错误:
# WRONG: this asks curl to open a TLS connection *to the proxy itself*
proxies = {"https": "https://USER:PASS@gate.quantumproxies.io:PORT"}
# Failed to perform, ErrCode: 35, Reason:
# 'error:100000f7:SSL routines:OPENSSL_internal:WRONG_VERSION_NUMBER'
# RIGHT: plain HTTP CONNECT, then the TLS tunnel runs through to the target
proxies = {"https": "http://USER:PASS@gate.quantumproxies.io:PORT"}
# Or skip the dict entirely
proxy = "http://USER:PASS@gate.quantumproxies.io:PORT"
键名是 目标 的协议;值名是您如何到达 代理。普通的 HTTPS-over-HTTP 代理接受明文 CONNECT,然后通过未修改的方式隧道您的加密流量——因此代理 URL 以 http:// 开头,即使您获取的每个 URL 都是 HTTPS。HTTPS-over-HTTPS 代理存在但很少见,必须由网关明确支持。Requests 更加有帮助地描述了相同的失败——您的代理似乎只使用 HTTP 而不是 HTTPS——这就是为什么相同的配置看起来像是 curl_cffi 特有的错误。最近的版本确实会发出警告并链接到问题 #6,但这只是一个警告:请求仍然失败。
认证:URL 凭证或 proxy_auth
认证网关接受通常的嵌入形式,http://USER:PASS@host:port,但有一个常见问题:密码中未转义的 @、: 或 / 会在错误的位置拆分 URL,导致身份验证失败,看起来像是代理失效。curl_cffi 提供了一个请求没有的逃生舱口——一个传递给 libcurl 的 proxy_auth 元组,作为单独的用户名和密码选项,因此完全不涉及编码。
import curl_cffi
from urllib.parse import quote
# Option A — credentials in the URL, password URL-encoded
pw = quote("p@ss:word", safe="")
r = curl_cffi.get(
"https://httpbin.org/ip",
proxy=f"http://USER:{pw}@gate.quantumproxies.io:PORT",
impersonate="chrome",
timeout=30,
)
# Option B — keep credentials out of the URL entirely
r = curl_cffi.get(
"https://httpbin.org/ip",
proxy="http://gate.quantumproxies.io:PORT",
proxy_auth=("USER", "PASS"),
impersonate="chrome",
timeout=30,
)
print(r.json())
第三种选择完全消除了这一类错误:IP 白名单。每个 QuantumProxies 住宅计划 允许您授权服务器的 IP,而不是发送用户:密码,因此代理 URL 成为一个简单的 http://gate.quantumproxies.io:PORT——无需编码,源码树中没有秘密。如果凭证本身被拒绝,我们的指南 407 Proxy Authentication Required 的每个原因 涵盖了其余部分。

会话、cookie 和凭证重用细节
一个 Session 将 cookie、连接池和您的默认设置集中在一个地方,这正是您想要的多步骤操作。设置 impersonate 和 proxy 一次,每个请求都会继承它们:
from curl_cffi import Session
with Session(
impersonate="chrome",
proxy="http://USER-session-a1b2:PASS@gate.quantumproxies.io:PORT",
timeout=30,
retry=3,
) as s:
s.get("https://httpbin.org/cookies/set/foo/bar")
r = s.get("https://httpbin.org/cookies")
print(r.json(), s.cookies.get_dict())
有两个行为值得注意。首先,每当配置了代理时,curl_cffi 会打开 libcurl 的 proxy-credential-no-reuse 选项:当代理用户名更改时会强制新连接,并且 TLS 会话缓存基于代理地址进行键控,因此以前的出口 IP 不会通过重用的会话泄漏到后续请求中。如果您在用户名中编码了粘性会话 ID,就像大多数旋转网关那样,您将免费获得这种隔离。其次,retry(一个整数,或来自 curl_cffi.requests 的 RetryStrategy,具有延迟、回退和抖动)仅在传输异常时重新运行。它不会像 urllib3 的 status_forcelist 那样重试 403 或 429——这个循环仍然需要您编写。兼容性文档将重试列为不支持,这已经过时:该参数在 v0.15.0 中引入。
每次请求的轮换和异步
curl_cffi 宣传 asyncio 每次请求的代理轮换,这是真实的:单个调用上的 proxy= 参数覆盖会话持有的任何内容。您很少需要代理列表来利用它——旋转网关在每次连接时分配一个新的出口服务器,因此一个端点加上并发已经是轮换了。如果您确实需要控制(每个工作者、每个账户、每个购物车一个稳定的 IP),请在用户名中放置一个会话令牌,让网关固定该出口。
import asyncio
from curl_cffi import AsyncSession
GATE = "gate.quantumproxies.io:PORT"
URLS = ["https://httpbin.org/ip"] * 20
async def fetch(session, url, worker):
# one sticky exit IP per worker; drop the -session- suffix for full rotation
proxy = f"http://USER-session-{worker}:PASS@{GATE}"
r = await session.get(url, proxy=proxy, timeout=30)
return r.status_code, r.json()["origin"]
async def main():
async with AsyncSession(impersonate="chrome", max_clients=10) as s:
return await asyncio.gather(
*(fetch(s, u, i % 5) for i, u in enumerate(URLS))
)
for status, ip in asyncio.run(main()):
print(status, ip)
max_clients 限制池中的并发 curl 句柄(默认 10),因此它是您真正的并发调节器——将信号量附加到无限制的 gather 通常是错误。相同的大小逻辑适用于任何异步客户端,我们在 使用 httpx 和 aiohttp 的异步 Python 抓取 中介绍过。是否每次请求轮换或固定会话取决于站点是否在请求之间跟踪状态;权衡在 粘性会话与旋转代理 中。

SOCKS5、HTTP/3 和安全开关
SOCKS 不需要额外安装——libcurl 已编译在内,因此不像 requests 那样没有 [socks] 额外需要记住。使用 socks5h://USER:PASS@gate.quantumproxies.io:PORT:h 将 DNS 解析推送到代理,防止从您自己的网络泄漏并从出口位置解析地理围栏的主机名。curl_cffi 检测到 socks 前缀并跳过 HTTP 隧道标志,因为 SOCKS 协议自行处理。这里的每个计划都在同一网关上公开 HTTP 和 SOCKS5 端点,因此切换是方案交换而不是新订单。
- HTTP/3 通过代理 在 v0.15.0 中与 http/3 指纹一起到来,但它需要一个支持 UDP 的 SOCKS5 服务器,而不是普通的 HTTP 网关。除非您的目标奖励 QUIC,否则这是小众的。
- SSRF 加固。 同一版本发布了一项建议:如果您获取其他人提供的 URL,重定向可能会进入您的内部网络。设置
allow_redirects="safe",或关闭重定向。 - 调试。 v0.15 发布了一个 CLI:
curl-cffi get tls.browserleaks.com/json --impersonate chrome一行告诉您模拟是否成功着陆,然后再责怪代理。
当 curl_cffi 加上代理就足够时
比人们预期的更常见。如果目标从内部 API 提供 JSON 或服务器渲染的 HTML,并且唯一的障碍是指纹检查,匹配的握手加上住宅出口可以以浏览器成本和延迟的一小部分解决它。项目的常见问题明确指出了上限:指纹是几个因素之一,除了 IP 质量、请求速率和 JavaScript 检查外,还有更高的保护级别需要更好的代理池和真实的浏览器自动化。当模拟配置正确但仍被阻止时,剩余的变量几乎总是出口 IP——在五分钟内隔离它是 curl_cffi vs requests 的主题。如果您宁愿不运行任何一个,Scraper API 在一个调用后处理指纹、代理和可选的 JS 渲染。
常见问题
如何在 curl_cffi 中使用代理?
将 proxy="http://USER:PASS@host:port" 传递给任何请求方法、会话或异步会话。requests 风格的 proxies={"http": ..., "https": ...} 字典也有效,但项目建议使用单一参数,除非您需要为每个方案使用不同的代理。传递两者会引发 TypeError。
为什么 curl_cffi 会抛出 ErrCode 35 WRONG_VERSION_NUMBER?
因为代理 URL 以 https:// 开头。标准代理期望明文的 CONNECT 请求,然后通过隧道传输您的 TLS;https:// 前缀使 curl 尝试与代理本身进行 TLS 握手,而代理以明文 HTTP 响应。将值更改为 http://——https 键指的是目标,而不是跳跃。
curl_cffi 支持 SOCKS5 代理吗?
是的,原生支持——libcurl 已捆绑,因此没有可选的额外安装。使用 socks5h:// 方案,以便主机名由代理解析而不是您的机器。SOCKS4、SOCKS4a 和普通 socks5:// 也被接受;库会跳过任何方案以 socks 开头的代理的 HTTP 隧道。
curl_cffi 能在每次请求时轮换代理吗?
是的。单个调用上的 proxy= 参数覆盖会话默认值,包括在 AsyncSession 中,这就是 README 所说的 asyncio 每次请求轮换。使用旋转网关,您通常不需要任何逻辑:相同的端点在每次连接时分配不同的出口 IP。
curl_cffi 能绕过 Cloudflare 吗?
有时可以。它移除了 TLS 和 HTTP/2 指纹提示,这对于基本保护级别来说已经足够。它无法执行 JavaScript 挑战、解决 Turnstile 或修复声誉数据库已标记的数据中心 IP。将模拟视为三个要求之一,而不是答案。
整个配置比其声誉更小:一个 proxy 字符串、会话上设置一次的 impersonate、每次调用的超时,以及 URL 编码或作为 proxy_auth 元组传递的凭证。正确设置这些,剩下的变量就是 IP 质量——来自被标记的数据中心地址的完美 Chrome 握手仍然是被标记的数据中心地址。我们对 JA3 和 JA4 指纹 的分析解释了为什么这两个检查是独立的。