如果你用 Python 的 requests 或 urllib3 抓取一个被 Cloudflare、Akamai 或 DataDome 保护的站点,即便你把 User-Agent 改成 Mozilla/5.0 (Windows NT 10.0; Win64; x64)、把 Accept-Language 头拼得天衣无缝,仍然会在第一个 TLS 握手就被拦下。原因不在 HTTP 层,而在 TCP 之上的 ClientHello——你的 Python 客户端发出的 TLS 握手包,JA3/JA4 指纹一眼就暴露了“我不是浏览器”。curl_cffi TLS 模拟正是为解决这一问题而生:它把 curl-impersonate 的 BoringSSL 引擎封装进异步 Python 接口,让你的请求在 TLS 层看起来与真实 Chrome 一致。本文面向 Python 抓取工程师与反爬研究员,讲清指纹原理、curl_cffi 的实现机制、Chrome 110+ 的 ClientHello 排列、为什么住宅代理仍然不可替代,并给出一个跑通 ProxyHat 的可运行示例。
curl_cffi TLS 模拟:为什么你的 ClientHello 会暴露身份
TLS 握手的第一条消息 ClientHello 携带了大量“身份信号”:密码套件(Cipher Suites)列表、扩展(Extensions)列表、支持的椭圆曲线(Named Curves)、签名算法、ALPN、key_share 等。这些字段的取值与顺序构成了 JA3 指纹——一个由 Salesforce 工程师在 2017 年提出的 MD5 哈希,把 SSLVersion,Cipher,SSLExtension,EllipticCurve,EllipticCurvePointFormat 拼成字符串后哈希。详见 Salesforce 关于 JA3 的原始文章。
Python 标准库 ssl 默认链接 OpenSSL,其 ClientHello 与 Chrome 的差异是结构性的:
- 密码套件顺序:OpenSSL 倾向于把 TLS 1.3 套件(
TLS_AES_256_GCM_SHA384、TLS_CHACHA20_POLY1305_SHA256、TLS_AES_128_GCM_SHA256)放在最前,再接一组 ECDHE 套件;Chrome 的 BoringSSL 则把TLS_AES_128_GCM_SHA256放在最前,并穿插 GREASE 值。 - GREASE:Google 在 RFC 8701 中引入的“保留占位值”,如
0x0a0a、0x1a1a,用于防止中间件对未知套件的僵化处理。Chrome 在 cipher、extension、group、version 多处都插入 GREASE,OpenSSL 默认不加,这是最致命的暴露点之一。 - 扩展顺序:Chrome 的扩展顺序是
server_name (0)→extended_master_secret (23)→renegotiation_info (65281)→supported_groups (10)→ ... →key_share (51)→psk_key_exchange_modes (45),且signature_algorithms (13)的位置与 OpenSSL 完全不同。顺序差异会让 JA3 哈希完全不同。 - supported_groups:Chrome 报告
x25519 (29)、secp256r1 (23)、secp384r1 (24)三条曲线,并在 key_share 里实际发送 x25519 的公钥;OpenSSL 默认曲线集合与顺序都不一致。 - TLS 1.3 ClientHello 形状:Chrome 在 ClientHello 里直接带
supported_versions (43)列出 TLS 1.3、key_share (51)预填 x25519 公钥、psk_key_exchange_modes (45)设为psk_dhe_ke。OpenSSL 1.1.1 之后的形状接近但 GREASE 与扩展顺序仍不匹配。
结果就是:一个普通的 requests.get('https://example.com') 产生的 JA3 哈希,与任何主流浏览器都不重合。反爬系统维护一个 JA3 白名单,凡是哈希不在“已知浏览器”集合里的,直接在 TCP 层就 RST 或返回 403。RFC 8446 定义了 TLS 1.3 的协议规范,但没有规定字段顺序,这给指纹留下了空间,参见 RFC 8446。
curl-impersonate 与 curl_cffi 如何复刻 Chrome 指纹
curl-impersonate 项目把 libcurl 的 TLS 后端从 OpenSSL 换成 BoringSSL(Chrome 使用的同一份代码),并打补丁让 curl 在发送 ClientHello 时按 Chrome 的精确顺序排列 cipher、extension、group。它还复刻了 HTTP/2 的 SETTINGS 帧顺序、WINDOW_UPDATE 增量、HEADERS 帧的伪头排列(:method、:authority、:scheme、:path),因为 Akamai 的指纹检测会读到 HTTP/2 帧层。
curl_cffi 是 curl-impersonate 的 Python 绑定,API 设计贴近 requests 与 httpx,支持同步与异步。核心用法是 impersonate 参数:
from curl_cffi import requests
r = requests.get(
"https://nowsecure.nl",
impersonate="chrome",
)
print(r.status_code)
impersonate="chrome" 会加载一组预设:特定的 cipher 列表、extension 顺序、supported_groups、ALPN(h2,http/1.1)、HTTP/2 SETTINGS 帧内容。curl_cffi 内置了多个目标:chrome、chrome110、chrome116、chrome120、safari、edge、firefox 等,对应不同版本浏览器的指纹。
当预设不够用时,curl_cffi 暴露了三个底层覆盖项:
ja3:直接传入 JA3 字符串(771,4865-4866-...,0-23-65281...,29-23-24,0),curl_cffi 会据此重建 ClientHello。适合精确复刻某个抓包到的真实浏览器。akamai:传入 Akamai 指纹字符串,控制 HTTP/2 帧的SETTINGS与伪头顺序。Akamai 的2号指纹比 JA3 更严格,因为它覆盖了 h2 层。extra_fp:一个字典,可单独覆盖tls_signature_algorithms、tls_record_version、http2_settings、http2_window_update等子项,用于微调而不必重写整个 JA3。
这种“预设 + 覆盖”的设计让你可以在 95% 场景下直接用 impersonate="chrome",剩下 5% 的硬目标再用 ja3/akamai 精修。
Chrome 110+ 的 ClientHello 排列与 JA4 的“顺序稳定”
从 Chrome 110 起,Google 引入了 ClientHello 扩展顺序的每次连接随机化:每次握手都会对扩展列表做一次随机置换(保留少数关键扩展的位置)。这意味着同一个 Chrome 实例发出的两次 ClientHello,JA3 哈希可能不同。这是对 JA3 的直接打击——JA3 假设顺序是稳定的身份特征。
JA4 是 FoxIO 在 2023 年提出的改进指纹,专门应对这种排列随机化。它的核心设计是先排序再哈希:把 cipher、extension、ALPN 等字段按数值升序排列后再拼接,从而对顺序不敏感。JA4 的格式为 ta13d1516h2_8daaf6152771_b186095e22b6,其中 t 表示 TCP,a13 表示 TLS 1.3,d 表示 SNI 存在,1516 表示 15 个 cipher、16 个 extension,h2 表示 ALPN。详见 JA4 规范仓库。
对 curl_cffi 用户而言,这意味着:
- 如果你
impersonate="chrome120",curl_cffi 已经按 Chrome 120 的实际 ClientHello 发送,包括扩展随机化逻辑,JA4 会落在 Chrome 的已知区间内。 - 如果你手写
ja3字符串,JA3 可能对,但 JA4 会因为缺少 Chrome 特有的扩展集合(如application_settings、encrypted_client_hello)而偏离。 - 因此优先用
impersonate预设,而非手搓 JA3,除非你有抓包数据要精确复刻。
为什么完美的 TLS 指纹仍需住宅代理
一个常见误区是:“只要 TLS 指纹像 Chrome,就能过 Cloudflare。”错。反爬系统是多信号融合的,TLS 指纹只是其中一维。另一维是IP 信誉。一个来自 AWS us-east-1 的 EC2 IP(54.x.x.x),即便 ClientHello 与 Chrome 120 完全一致,仍然会被打上“数据中心”标签。Cloudflare、DataDome、PerimeterX 都维护 ASN 数据库,对 AS14618(Amazon)、AS15169(Google Cloud)等数据中心 ASN 的请求,默认风险分就高。
更具体地说,信誉评分通常综合:
- ASN 类型:ISP/住宅 vs hosting/datacenter。住宅 ASN(如德国电信
AS3320、ComcastAS7922)默认低风险。 - IP 历史行为:同一 IP 是否在过去 24-72 小时内触发过 captcha、被多个站点标记。
- 地理一致性:IP 的 GeoIP 与 Accept-Language、时区头是否吻合。一个德国 IP 配
zh-CN的 Accept-Language 是高信号。 - 并发与速率:同一 IP 在 10 秒内发 100 个请求到同一域名,几乎必然触发限流。
所以正确的组合是:curl_cffi 提供 TLS 层的浏览器一致性,住宅代理提供 IP 层的信誉一致性。两者缺一不可。一个完美的 Chrome TLS 指纹从一个干净住宅 IP 发出,才是反爬系统眼中的“普通用户”。
ProxyHat 的住宅出口覆盖 190+ 国家,支持按国家、城市定位,并支持 sticky session 让同一会话保持同一 IP。配合 curl_cffi,你可以让每个请求在 TLS 和 IP 两层都像真实用户。查看可用位置见 ProxyHat 位置列表,定价见 ProxyHat 定价。
实战:curl_cffi AsyncSession + ProxyHat 住宅出口
下面是一个可运行的示例:用 curl_cffi 的 AsyncSession,impersonate="chrome120",通过 ProxyHat 住宅代理以德国 IP 访问目标站点,并演示 sticky session 与轮换两种模式。
import asyncio
from curl_cffi.requests import AsyncSession
PROXY = "http://user-country-DE:pass@gate.proxyhat.com:8080"
async def fetch_one(session, url):
r = await session.get(url, impersonate="chrome120", proxy=PROXY, timeout=20)
return r.status_code, r.text[:200]
async def main():
async with AsyncSession() as s:
# 轮换模式:每个请求自动换 IP(默认行为)
results = await asyncio.gather(
fetch_one(s, "https://api.ipify.org?format=json"),
fetch_one(s, "https://api.ipify.org?format=json"),
fetch_one(s, "https://api.ipify.org?format=json"),
)
for code, body in results:
print(code, body)
asyncio.run(main())
如果需要 sticky session(同一会话保持同一 IP,适合多步登录、分页抓取),把用户名改成带 -session- 标识:
PROXY_STICKY = "http://user-country-DE-session-myjob01:pass@gate.proxyhat.com:8080"
async with AsyncSession() as s:
# 这 5 个请求会走同一个德国住宅 IP
for i in range(5):
r = await s.get(
"https://example.com/page",
impersonate="chrome120",
proxy=PROXY_STICKY,
)
print(i, r.status_code)
对于需要 SOCKS5 的场景(部分目标只接受 SOCKS),端口切换为 1080:
PROXY_SOCKS5 = "socks5://user-country-DE:pass@gate.proxyhat.com:1080"
r = await s.get(url, impersonate="chrome120", proxy=PROXY_SOCKS5)
在生产环境里,建议加上重试与并发控制。下面是一个带指数退避的完整模式:
import asyncio, random
from curl_cffi.requests import AsyncSession
CONCURRENCY = 20
TARGETS = [f"https://example.com/p/{i}" for i in range(200)]
def proxy_for(i):
# 每个并发 worker 用不同 sticky session,分散 IP
return f"http://user-country-DE-session-w{i}:pass@gate.proxyhat.com:8080"
async def fetch_with_retry(s, url, idx, max_retry=4):
for attempt in range(max_retry):
try:
r = await s.get(
url,
impersonate="chrome120",
proxy=proxy_for(idx),
timeout=15,
)
if r.status_code == 200:
return r
if r.status_code in (403, 429):
await asyncio.sleep(2 ** attempt + random.random())
continue
return r
except Exception:
await asyncio.sleep(2 ** attempt)
return None
async def main():
sem = asyncio.Semaphore(CONCURRENCY)
async with AsyncSession() as s:
async def bounded(i, url):
async with sem:
return await fetch_with_retry(s, url, i)
results = await asyncio.gather(*[bounded(i, u) for i, u in enumerate(TARGETS)])
ok = sum(1 for r in results if r and r.status_code == 200)
print(f"成功 {ok}/{len(TARGETS)}")
asyncio.run(main())
这个模式的关键点:每个 worker 用独立的 sticky session,避免单 IP 承载过高并发;遇到 403/429 时退避并换 session(即换 IP);并发控制在 20 左右,避免压垮目标。更多抓取场景参考 ProxyHat 网页抓取用例 与 SERP 跟踪用例。完整代理参数文档见 ProxyHat 官方文档。
curl_cffi 的边界:它解决不了 JS 挑战
必须明确:curl_cffi 只解决 TLS + HTTP/2 帧层的指纹问题。它不执行 JavaScript。当一个站点在 200 响应里返回一段 JS 挑战(如 Cloudflare 的 cf-chl-bypass、DataDome 的 dd.js、Akamai 的 _abck cookie 计算),curl_cffi 拿到的是这段 JS 源码,而不是执行后的 cookie。此时你必须切换到真实浏览器方案:
- Playwright / Puppeteer + stealth 插件:在真实 Chromium 里加载页面,让 JS 挑战自然执行。配合 Playwright 官方仓库 的 stealth 扩展,可隐藏
navigator.webdriver、canvas 指纹等。 - patchright / undetected-chromedriver:对 Chromium 做底层补丁,移除 CDP 检测特征。
- curl_cffi 仍可用于挑战之后的纯 API 调用:先用浏览器拿到
cf_clearance或_abckcookie,再把 cookie 灌入 curl_cffi 做高并发抓取。这是常见的混合架构。
另一个边界是行为指纹:鼠标轨迹、滚动事件、按键间隔、TLS 之上的 WebSocket 帧时序。这些 curl_cffi 一律不模拟。如果你的目标在做行为分析,纯 HTTP 客户端无论如何都过不了。
伦理与法律边界
TLS 模拟技术本身是中性的——安全研究员用它做授权渗透测试、反爬研究员用它评估检测能力、合规数据团队用它访问公开数据。但“公开可访问”不等于“可以任意抓取”。需要注意:
- robots.txt 与 ToS:即便技术上能抓,也应尊重目标的 robots.txt 与服务条款。违反 ToS 在部分司法辖区可能构成违约或计算机滥用。
- CFAA(美国计算机欺诈与滥用法):在美国,绕过技术访问控制措施可能被解释为“未授权访问”。Van Buren v. United States (2021) 案后,最高法院缩小了 CFAA 的适用范围,但绕过明确的技术屏障仍有风险。
- GDPR(欧盟通用数据保护条例):如果抓取的数据包含欧盟自然人的个人信息(姓名、邮箱、IP 等),需有合法依据,且可能需要数据保护影响评估。
- 授权原则:对自有资产做安全测试、对明确公开且无登录墙的数据做合规采集,是合理使用场景。对第三方付费墙后的内容、需要登录才能访问的私有数据,应获得书面授权。
ProxyHat 代理服务用于合法的网络访问、安全研究与合规自动化,不鼓励任何违反目标站点条款或当地法律的使用。
关键要点
- TLS 指纹(JA3/JA4)是反爬的第一道闸门,Python 默认 OpenSSL 的 ClientHello 与 Chrome 差异巨大,先被拦在 TCP 层。
- curl_cffi 通过 BoringSSL + curl-impersonate 复刻 Chrome 的 cipher、extension、group 顺序与 HTTP/2 SETTINGS 帧,
impersonate="chrome120"一行即可启用。- Chrome 110+ 的扩展随机化使 JA3 不再稳定,JA4 通过“先排序再哈希”保持顺序稳定——优先用预设而非手搓 JA3。
- TLS 指纹完美但 IP 是数据中心,仍会被信誉评分拦下;住宅代理是 IP 层的必要补充。
- curl_cffi 不执行 JS,遇到 JS 挑战需切换真实浏览器;混合架构(浏览器拿 cookie + curl_cffi 高并发)是常见解法。
- 始终在授权、合规、尊重 robots.txt 与 ToS 的前提下使用。
把 curl_cffi 的 TLS 一致性与 ProxyHat 住宅代理的 IP 信誉叠加,你的自动化请求才能在反爬系统的多信号融合中真正“像个人”。从 ProxyHat 定价 选择合适的住宅套餐,或查看 全球位置 开始构建你的合规抓取管线。






