什么是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-ua、sec-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)
2026量化分析代理IP选型:一套完整评估方法
代理IP 代理IP池 量化分析
2026-09-24

很多人挑选代理 IP 时只看平均延迟与 IP 总量,这类指标容易掩盖长尾抖动、IP 重复、返回内容无效等问题,无法真实反映采集任务产出。本文搭建一套可复现、可量化的代理 IP 评估体系,先明确测试前必须锁定的业务边界,从质量、时延、稳定性、并发、地区、成本六大维度定义评估指标,设计六阶段测试流程。

如何使用海外代理IP处理Common Crawl规模的数据?
海外代理IP 海外IP 大数据分析 API数据
2026-09-20

先利用Common Crawl索引筛选已有页面,在数据所在区域批量处理归档;只有目标页面缺失或需要更新时,才通过海外代理IP补采。能否做到十亿级处理,主要取决于任务切分、去重、限速和断点续跑,而不是代理IP数量。

AI大模型公司做数据采集的8家HTTP代理服务商评测:带宽、可用率、覆盖……
数据采集 HTTP代理 企业级代理服务商
2026-09-16

不少 AI 大模型企业采购 HTTP 代理,习惯直接对比带宽、可用率、节点覆盖三大参数,但厂商统计口径差异大,仅凭这些指标很容易选错方案。本文横向测评青果网络、极安代理、四叶天代理、巨量代理、小象代理、熊猫代理、亿牛云、大麦 HTTP 共 8 家服务商,结合大模型语料采集、舆情监测、法律大数据等业务场景,拆解代理选型核心逻辑,讲解业务分池、并发稳定性、同口径压力测试方法,同时梳理供应商资质与协议边界,帮助 AI 数据工程团队跳出纸面参数,基于真实任务需求挑选适配的代理基础设施。

2026年适合AI训练数据采集的代理IP服务商:8家企业级厂商对比
代理IP服务商 HTTP代理 代理IP池
2026-09-10

AI训练数据采集的代理IP选型,核心不是比IP池大小,而是看业务隔离、合规资质、采集稳定性和计费灵活性。本文按场景拆解青果网络、极安代理及6家国内厂商的机制差异,帮技术决策者按需匹配。

微信小程序

微信扫一扫体验

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部