用 curl_cffi 绕过 TLS/JA3 指纹检测

发布:2026 年 7 月 23 日 · 阅读约 7 分钟

一句话总结: 如果你从干净的住宅 IP 出去还是被封,几乎可以肯定目标站在对你的 TLS 握手(JA3/JA4)和 HTTP/2 帧做指纹。Python 的 requestshttpx 产生的指纹是任何真实浏览器都没有的,所以内容还没加载你就被判成自动化了。curl_cffi 能模拟真实浏览器的 TLS 签名。它要和住宅代理一起用 —— 一个修 IP 信誉,一个修指纹,难搞的目标站两样都查。

那种和你 IP 毫无关系的封锁

你换上一个全新住宅 IP、设好真实的 Chrome User-Agent、从浏览器网络面板把请求头照抄过来 —— 目标站还是回 403 或者没完没了的验证码。请求看起来哪儿都没问题。问题出在一个你从应用层根本设不了的地方:TLS 握手本身。

你的客户端打开 HTTPS 连接时,会发一个 ClientHello,列出它支持的加密套件、扩展、椭圆曲线,而且有特定顺序。这个模式被哈希成 JA3(或更新的 JA4)指纹。真实 Chrome 是一个签名;走系统 OpenSSL 的 Python requests 是完全不同的另一个。反爬厂商维护着「哪些指纹属于真浏览器、哪些属于脚本库」的名单,并在握手期间就核对 —— 早于评估你的请求头或 IP。

所以你可以有完美的住宅 IP、完美的请求头,照样被标记,因为你的 TLS 指纹写着「Python」。

解法:用 curl_cffi 模拟真实浏览器

curl_cffi 是 curl-impersonate 的 Python 绑定 —— curl-impersonate 是一个打了补丁的 curl,能复现真实浏览器的 TLS 和 HTTP/2 指纹。它的 API 刻意做得和 requests 一样,所以切过去基本就是改一行。

pip install curl_cffi

一个模拟 Chrome 的基础请求:

from curl_cffi import requests

r = requests.get("https://example.com", impersonate="chrome")
print(r.status_code, r.text[:200])

impersonate 参数就是关键 —— 它让 curl_cffi 按 Chrome 的加密套件/扩展顺序发包,使 JA3 哈希对上真实浏览器。你可以钉一个具体版本(chrome124chrome131safari17_0firefox133),也可以传 chrome 用较新的默认值。

加上住宅代理

指纹现在对了,但难搞的目标站还盯着出口 IP。用你在 requests 里就熟悉的 proxies= 参数,把请求走住宅代理:

from curl_cffi import requests

proxies = {"https": "http://USER:PASS@gw.roamproxy.com:41080"}

r = requests.get(
    "https://example.com",
    impersonate="chrome124",
    proxies=proxies,
)
print(r.status_code)

现在握手看起来像 Chrome,而且连接从真实家庭宽带 IP 出去。这正是难搞的目标站真正在测的组合。curl_cffi 也支持 SOCKS5 代理,把值写成 socks5://USER:PASS@gw.roamproxy.com:41080 即可。

代理密码尽量不含特殊字符。代理值按 URL 解析,密码里的 #@/: 会破坏解析。做百分号编码(# 写成 %23),或者干脆重新生成一个不含这些字符的密码。

大量请求用 Session

正经采集要复用 session,连接和指纹只设一次:

from curl_cffi import requests

session = requests.Session(impersonate="chrome124")
session.proxies = {"https": "http://USER:PASS@gw.roamproxy.com:41080"}

for url in urls:
    r = session.get(url)
    ...

要把大任务分散到很多出口 IP,用轮换会话,让每个请求从不同住宅 IP 出去;要在登录流程里保持同一个 IP,用粘性会话。该用哪种、会话串怎么控制,见 什么是轮换代理

验证指纹确实变了

改前改后各查一次 JA3。一个指纹回显接口会返回服务器看到的哈希:

from curl_cffi import requests

print(requests.get("https://tls.browserleaks.com/json", impersonate="chrome").json()["ja3_hash"])

用普通 requests 跑同一条,你会得到不同的哈希 —— 这个差异正是目标站拿来封你的东西。

常见错误

请求为什么被封、还该查什么的全景,见 如何避免采集时被封

常见问题

住宅代理很新很干净,为什么还是被封?

因为 IP 只是众多信号之一。Cloudflare、DataDome、Akamai 这类反爬系统在连接刚打开时就对 TLS ClientHello(JA3/JA4)和 HTTP/2 帧顺序做指纹 —— 早于它看你的 IP 信誉和请求头。Python 默认技术栈产生的指纹是任何真实浏览器都不会有的,所以无论出口 IP 多干净,你都会被判成自动化。住宅代理和浏览器级指纹是针对两项不同检测的两个独立修复;难搞的目标站两项都跑。

JA3 指纹到底是什么?

JA3 是把 TLS ClientHello 里若干字段做的哈希 —— 你的客户端提供的加密套件、扩展、椭圆曲线,以及它发送这些的确切顺序。不同 HTTP 客户端排列方式不同,所以这个哈希实际上就标识了发请求的软件。真实 Chrome 是一个 JA3;Python requests(走 OpenSSL)是完全不同的另一个。JA4 是更新、更细的后继版本。curl_cffi 复现真实浏览器的排列顺序,让哈希对上 Chrome 或 Safari,而不是「Python」。

只用 curl_cffi、不配代理够吗?

防护弱的站,有时够。稍微认真的站,不够。指纹模拟能过 TLS/HTTP2 那道检查,但你从一个机房 IP 发几百个请求,照样会因为 IP 信誉被限速或封禁。可靠的组合是 curl_cffi 修指纹 + 轮换住宅代理修 IP —— IP 类型为什么关键,见 住宅代理 vs 机房代理

requests、httpx、curl_cffi 该用哪个?

requestshttpx 用来打 API、打不做指纹的站没问题。一旦目标站在 Cloudflare/DataDome/Akamai 后面、你请求头和 IP 都对却还是被封,就换 curl_cffi —— 它的 API 几乎和 requests 一样,迁移通常就是改一行 import 再加个 impersonate= 参数。

curl_cffi 修指纹,干净出口 IP 修信誉。Roam 提供轮换住宅 $2/GB、静态住宅 $4/IP/月,HTTP 与 SOCKS5 都支持,直接塞进下面的 proxies= 参数即可。注册账号即送 300MB 试用流量,拿你自己的目标站把这套组合跑通。