什么是TLS指纹识别?JA3/JA4是怎么算出来的?

TLS指纹识别的核心是ClientHello包里的字段组合。同一个客户端库产生的组合几乎不变,因此可以用来识别请求方到底是浏览器还是脚本。

TLS握手第一步,客户端会发一个ClientHello数据包,里面包含五组字段:

  • TLS版本
  • 支持的加密套件列表:Cipher Suites
  • 扩展列表:Extensions
  • 支持的椭圆曲线:Elliptic Curves
  • 椭圆曲线格式:EC Point Formats

JA3把这五组字段按顺序拼接后取MD5,得到一个32字符的指纹字符串。JA4在JA3基础上做了升级,加入了ALPN、SNI、扩展排序稳定性等因素,抗混淆能力更强。

三种客户端的典型指纹表现:

客户端JA3示例特征识别难度
Chrome 120加密套件顺序稳定、GREASE值出现与真实用户流量混在一起
Firefox 121与Chrome显著不同的套件顺序与Chrome可区分
Python requestsOpenSSL默认套件列表、无GREASE一眼识别为脚本
Go net/httpGo原生TLS栈,另一套指纹一眼识别为脚本

对目标站来说,只要维护一份"浏览器指纹白名单"或"脚本指纹黑名单",就能在TLS握手阶段就做出判断,根本不用等到应用层。APP大数据分析、跨境选品这类要长期采集的业务,指纹校验命中率越来越高。

为什么requests+代理搞不定?根本差异在哪?

代理只换出口IP、加密流量转发,完全不参与TLS握手;TLS握手是客户端与目标之间的端到端过程,代理无法改变ClientHello的内容。

一次带代理的HTTPS请求实际流程:

[Python requests]  --CONNECT-->  [代理]  --TCP透传-->  [目标]
        |                                                  |
        +----------- TLS 握手(端到端加密)--------------+
        |                                                  |
        +----------- HTTP 请求(在 TLS 隧道内)-----------+

代理只在CONNECT阶段起作用,一旦隧道建立,客户端的TLS握手数据完全透传给目标。所以:

  • 换IP → 目标看到的源IP变了,但ClientHello没变
  • 换代理协议如HTTP或SOCKS5 → 只影响客户端到代理这段,不影响TLS指纹
  • 换User-Agent → 只在HTTP层生效,改不了TLS层

requests的TLS指纹由底层OpenSSL版本决定,全球用requests的开发者跑出来的指纹极其集中。目标站只需拉一份OpenSSL默认指纹列表进黑名单,就能拦下绝大多数requests流量。这不是IP质量能解决的问题。

curl_cffi是如何解决指纹一致性的?

curl_cffi的技术路径是"底层调用魔改版curl + Python绑定"。所谓魔改版curl,指的是curl-impersonate项目。

三层结构关系:

组件作用
C库curl-impersonate编译时把BoringSSL/nss等浏览器同款TLS栈静态链接进来,输出的ClientHello与浏览器逐字节一致
绑定cffiPython调用C库的桥
Python APIcurl_cffi.requests提供与requests几乎相同的Session、get、post接口

支持的impersonate目标,截至当前主流版本:

impersonate值对应浏览器
chrome99 / chrome100 / chrome101 / chrome104 / chrome107 / chrome110 / chrome116 / chrome119 / chrome120 / chrome123 / chrome124 / chrome131Chrome各版本
chrome99_androidChrome Android
edge99 / edge101Edge
safari15_3 / safari15_5 / safari17_0 / safari17_2_iosSafari桌面与iOS
firefox133Firefox

选impersonate版本的策略:目标站验证严格就用最新版如chrome131;要与真实用户流量分布对齐就取当前主流市占率最高的Chrome版本;要低调采集选chrome104这种"次新版",避免过于集中的最新版指纹。

安装与最小可运行示例怎么写?

安装一行搞定,用法几乎无缝迁移自requests。

pip install curl_cffi
# 或使用镜像加速
pip install curl_cffi -i https://pypi.tuna.tsinghua.edu.cn/simple

最小GET示例:

from curl_cffi import requests

r = requests.get("https://tls.browserleaks.com/json", impersonate="chrome131")
print(r.json())
# 输出的 ja3_hash 应与 Chrome 一致

POST示例:

from curl_cffi import requests

r = requests.post(
    "https://example.com/api/query",
    json={"keyword": "跨境选品", "page": 1},
    headers={"Accept": "application/json"},
    impersonate="chrome131",
    timeout=30,
)

Session复用,推荐生产用法:

from curl_cffi import requests

session = requests.Session(impersonate="chrome131")
session.headers.update({
    "Accept-Language": "zh-CN,zh;q=0.9",
})

# 复用同一 TLS 连接与 cookie jar
r1 = session.get("https://example.com/login")
r2 = session.post("https://example.com/api/list", json={"page": 1})

关键点:

  • impersonate可以在Session级设置一次,也可以在单次请求覆盖
  • 不显式指定impersonate时,curl_cffi会用默认Chrome指纹,但版本可能不稳定,生产环境务必显式指定
  • 请求头会自动补齐到浏览器同款顺序,无需手动指定sec-ch-uasec-fetch-*

如何搭配代理使用?

curl_cffi的proxies参数格式与requests完全相同。

from curl_cffi import requests

proxies = {
    "http":  "http://user:pass@proxy.example.com:8080",
    "https": "http://user:pass@proxy.example.com:8080",
}

r = requests.get(
    "https://example.com",
    proxies=proxies,
    impersonate="chrome131",
    verify=True,
    timeout=30,
)

SOCKS5代理

proxies = {
    "http":  "socks5h://user:pass@proxy.example.com:1080",
    "https": "socks5h://user:pass@proxy.example.com:1080",
}

注意socks5h前缀让DNS也走代理,避免DNS泄漏;如果只写socks5则本地解析DNS,走代理只做传输。

Session级代理配置:

session = requests.Session(
    impersonate="chrome131",
    proxies=proxies,
)

生产实践的一个关键点:代理侧要开长连接支持。curl_cffi默认启用HTTP keep-alive,如果代理侧强制短连接,指纹优势会被削弱,因为每次新TLS握手都增加开销和被采集概率。

并发抓取怎么做?异步支持如何?

curl_cffi从0.6版本开始提供AsyncSession,基于asyncio。

异步并发抓取模板:

import asyncio
from curl_cffi.requests import AsyncSession

async def fetch(session, url):
    r = await session.get(url, timeout=30)
    return r.status_code, len(r.content)

async def main(urls):
    async with AsyncSession(impersonate="chrome131") as session:
        tasks = [fetch(session, u) for u in urls]
        return await asyncio.gather(*tasks, return_exceptions=True)

urls = ["https://example.com/page/" + str(i) for i in range(100)]
results = asyncio.run(main(urls))

并发数控制,推荐用Semaphore而不是直接gather几千个task:

async def bounded_fetch(sem, session, url):
    async with sem:
        return await fetch(session, url)

async def main(urls, concurrency=20):
    sem = asyncio.Semaphore(concurrency)
    async with AsyncSession(impersonate="chrome131") as session:
        tasks = [bounded_fetch(sem, session, u) for u in urls]
        return await asyncio.gather(*tasks, return_exceptions=True)

同步版本用线程池也可以:

from concurrent.futures import ThreadPoolExecutor
from curl_cffi import requests

def fetch(url):
    return requests.get(url, impersonate="chrome131", timeout=30).status_code

with ThreadPoolExecutor(max_workers=20) as pool:
    results = list(pool.map(fetch, urls))

性能对比参考,数值为同规模抓取任务的相对量级:

方案并发100请求耗时相对量级内存占用相对量级
requests + 线程池1.0(基准)1.0
curl_cffi 同步 + 线程池0.9-1.11.1-1.3
curl_cffi 异步0.3-0.50.6-0.8

生产环境批量抓取推荐curl_cffi异步方案。

遇到"impersonate失效"该如何排查?

impersonate失效的信号:请求返回403、challenge页面、或跳转到验证码。这时不要盲目换IP,先按四步定位。

第一步:验证指纹是否真的被复刻

r = requests.get("https://tls.browserleaks.com/json", impersonate="chrome131")
print(r.json()["ja3_hash"])
# 与 https://check.ja3.zone/ 上真实 Chrome 131 的 ja3_hash 对比

第二步:检查impersonate版本是否落后

浏览器每几个月更新一次TLS栈,curl_cffi需要跟进。看当前版本支持的最新Chrome是否覆盖近半年发布:

pip show curl_cffi | grep Version
# 到 https://github.com/lexiforest/curl-impersonate/releases 对照

如果落后半年以上,升级curl_cffi版本或换到更新的impersonate值。

第三步:检查HTTP层是否有泄漏

TLS指纹对齐了但HTTP header顺序、值不对,同样会被识别。用抓包工具对比curl_cffi发出的请求与真实Chrome DevTools里的请求,重点看:

  • header出现顺序:Accept-Language、Sec-Fetch-*等
  • User-Agent版本号是否匹配:不能用chrome131指纹配chrome100的UA
  • HTTP/2的伪header顺序::method:path:scheme:authority

第四步:判断是否触发了目标的行为分析

如果TLS和HTTP都对齐但仍然403,问题往往在业务层:

信号可能原因
首次请求200,第二次开始403Cookie/会话缺失,被行为分析识别为新客户端
慢慢从可用到不可用IP池被目标标记,需要更换代理源
只在特定时段403目标在特定时段收紧访问频率控制
随机403目标做了概率抽样,无法完全避免

信号级别的应对:加Cookie持久化、降低同IP的请求密度、拉长请求间隔加随机抖动、必要时切换到不同的impersonate版本增加流量多样性。

FAQ

Q:curl_cffi和requests同时装有冲突吗?

没有冲突。两者是独立的包,可以同时安装并在同一项目里混用。实际项目中常见的用法是:内部服务/API调用用requests,因为更成熟、生态好;对外抓取用curl_cffi,因为指纹一致。注意from curl_cffi import requests这一行会在当前作用域内覆盖标准requests,避免在同一模块内混用两个同名对象。

Q:为什么有些站点用了chrome131还是被识别?

三种可能:一是目标做了JA4指纹而非JA3,JA4对扩展顺序更敏感,chrome131的JA4指纹与真实Chrome可能有微小偏差;二是目标同时校验HTTP/2 SETTINGS帧的字段顺序,也就是AKAMAI H2指纹,这层curl-impersonate也覆盖但需要目标机制单元覆盖到;三是目标做了客户端主动探测比如JS challenge,这已经超出TLS指纹范畴,需要额外的浏览器执行环境如Playwright。

Q:curl_cffi支持HTTP/2和HTTP/3吗?

HTTP/2完整支持,且impersonate同时会复刻HTTP/2 SETTINGS帧的字段值和优先级树,这是与requests+h2库最大的区别之一。HTTP/3基于QUIC,从0.7版本开始试验性支持,稳定性还在完善,生产环境建议先用HTTP/2。

Q:impersonate选择chrome104和chrome131在实测中差别大吗?

差别在两个维度。识别率:越新的Chrome版本,目标黑名单命中概率越低,因为新版才被大规模部署到用户;性能:新版Chrome往往用更多加密扩展,握手包更大、耗时略高。生产实践中,抓取要求高的场景选最新版;要与真实用户流量分布对齐、避免过于集中,选chrome116-chrome124之间的主流版本;轻量抓取选chrome104-chrome110。

Q:curl_cffi在Windows上能用吗?

能。curl_cffi在Windows、Linux、macOS三个平台都提供预编译wheel,pip安装无需C编译器。Windows上偶尔遇到DLL load failed报错,通常是VC++运行时缺失,安装Microsoft Visual C++ Redistributable最新版能解决。

青果网络代理IP - CTA Banner
点赞(33)
Python解析JSON响应:高效提取API数据的3种方法(附场景对照表)
Python API数据 HTTP代理 国内代理IP
2026-08-19

json.loads() 加字典遍历只适合小体量、结构固定的API。字段动态或嵌套深时用 jsonpath-ng 表达式提取更省力,处理GB级JSON或流式响应则要换 ijson 增量解析。选对方法比优化代码更重要。

IP池按需购买模式怎么评估?成本弹性与适用场景分析
代理IP成本 代理IP池 IP池 HTTP代理
2026-08-14

IP池按需购买的核心价值不是单价低,而是成本弹性——业务波动超过30%的场景下,按需模式的综合浪费率远低于固定包量,总成本可能更优。评估时看三个指标:波动系数、用量预测准确度、闲置成本。

隧道代理参数怎么设置?新手常见配置项解释
隧道代理IP 动态代理IP 国内代理 HTTP代理
2026-08-12

隧道代理的配置项看着多,其实只有三类:鉴权类、性能类、业务约束类。按业务优先级过一遍,8 个关键参数就能跑通。本文按参数分类、逐项作用、常见取值区间、上线前测试四步走,直接抄可用。

企业电商数据采集如何构建IP池?动态轮换与请求调度逻辑解析
IP池 代理IP池
2026-07-22

IP池不是"买一批IP地址往里扔"的资源堆砌,而是一套包含存活检测、权重调度、故障剔除和会话保持的调度系统。轮换策略要按业务场景设计,盲目随机切IP反而浪费资源、拉低成功率。

微信小程序

微信扫一扫体验

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部