curl_cffi 与 requests:解决了什么问题,无法解决什么问题

将 requests 替换为 curl_cffi 可以将许多 403 转换为 200。但对于被封的 IP 则无济于事。以下是两者之间的界限,以及一个十五分钟的测试,告诉您问题出在哪一边。

curl_cffi 与 requests 的问题通常在事件中期出现:一个运行了几个月的爬虫在第一次调用时开始返回 403,Reddit 上有人建议更换客户端,结果奏效。这是一个有真实解释的真实效果——但这种说法(“只需使用 curl_cffi”)掩盖了正在发生的事情以及它停止帮助的地方。requests 并不慢,也不是写得不好。在抓取上下文中,它只有一个缺点,这是一个大问题,与您输入的 API 无关。这是两个客户端真正不同的地方,切换时会发生什么变化,以及无论您选择哪个库,冒充永远无法解决的一个问题。

唯一重要的区别

两个库发送相同的头。区别在于下一层,在单个 HTTP 字节移动之前打开连接的 TLS 握手。requests 基于 urllib3 和 OpenSSL,广告的密码列表、扩展集和排序属于 Python,而不是其他任何东西。curl_cffi 是一个绑定到 curl 的修补分支的库,它逐字节地重现浏览器的 ClientHello 字节及其 HTTP/2 SETTINGS 帧——因此其 JA3、JA3N 和 Akamai 哈希匹配真实的 Chrome,而不是脚本库。反机器人供应商保留这些签名的数据库;Chrome User-Agent 头与 Python 握手之间的不匹配是一个您无法解释的矛盾。我们在 JA3 和 JA4 指纹识别 中解开了这些机制,这同样的效果解释了为什么 curl 在相同的 URL 上获得 403 而您的浏览器获得 200

比较中的其他一切都来自实现。因为 curl_cffi 包装了 libcurl,它继承了 HTTP/2、HTTP/3、websockets 和 asyncio,而这些 requests 从未支持过。因为 requests 是纯 Python,它可以安装在任何地方,并且拥有十年的生态系统支持。两者的陈述同时成立,哪个占主导地位完全取决于您的目标。

您可以在五分钟内运行的可重复测试

不要盲信任何人的通过率表,包括我们的。指纹差异是可以直接观察到的:将两个客户端指向一个 TLS 回显端点并比较它们报告回来的哈希值。如果两行匹配,您的构建没有冒充任何东西。

# pip install requests curl_cffi
import requests
import curl_cffi

URL = "https://tls.browserleaks.com/json"

a = requests.get(URL, timeout=30).json()
b = curl_cffi.get(URL, impersonate="chrome", timeout=30).json()

print("requests   ja3n:", a["ja3n_hash"], "| akamai:", a.get("akamai_hash", "-"))
print("curl_cffi  ja3n:", b["ja3n_hash"], "| akamai:", b.get("akamai_hash", "-"))

# Two different ja3n hashes = impersonation is working. The curl_cffi README
# documents aa56c057ad164ec4fdcb7a5a283be9fc as a Chrome-matched ja3n value.
# The akamai (HTTP/2) fingerprint is usually empty for requests: it is
# HTTP/1.1 only, so there is no SETTINGS frame to fingerprint in the first place.

自 v0.15 起,有一个相同检查的一行版本:curl-cffi get tls.browserleaks.com/json --impersonate chrome。在调试其他任何事情之前运行它——它将“我的冒充配置错误”与“我的冒充正常,但其他东西阻止了我”分开,这完全是两个不同的下午。

requests 和 curl_cffi 在协议支持、并发、指纹识别和可移植性方面的并排比较
requests 在可移植性和生态系统方面获胜;curl_cffi 在协议和指纹方面获胜。两者都没有在 IP 质量上获胜——这不是客户端功能。

curl_cffi 无法修复的:IP 声誉

这是更换库建议遗漏的部分。TLS 指纹回答了“这是什么软件?”的问题。它没有回答“这来自哪里?”——第二个问题通过对您的出口 IP 的单独查询来回答:哪个 ASN 拥有它,它是托管提供商还是消费者 ISP,它是否出现在滥用源中,在过去一小时内有多少其他会话从同一地址访问过该站点。从数据中心范围内的云 VM 到达的完美 Chrome 握手是一个显然安装在服务器机架中的 Chrome 浏览器。这并不比 python-requests 更有说服力。在某些情况下,它的说服力更低,因为矛盾更为明显。

项目自己的 FAQ 将 IP 质量放在其因素列表的首位,优先于请求速率和 JavaScript 指纹,以解释为什么仅靠冒充可能不够。这种排序不是偶然的:声誉是防御者评估的最便宜信号,也是攻击者最难伪造的信号,因为与头或密码列表不同,您无法在本地生成它。如果您的基于 requests 的爬虫已经通过数据中心池运行并被阻止,使用相同池切换到 curl_cffi 会改变两个失败检查中的一个。您将在软目标上看到部分改进,而在硬目标上则完全没有改进——这正是人们报告的困惑结果。

该轴的修复是地址质量,而不是代码:来自真实消费者 ISP 分配的 住宅 IP,这就是 90M+ 地址跨 200+ 国家/地区为您带来的。如果您想在更改任何内容之前检查当前出口的外观,我们的免费 IP 质量检查器 会报告目标将看到的 ASN 和分类。

告诉您哪个轴出问题的 2x2

与其猜测,不如针对您的真实目标独立测试两个变量。四个请求,四行输出,结果命名您的问题:

import requests
import curl_cffi

TARGET = "https://your-target.example/api/items"
DC  = "http://USER:PASS@your-datacenter-gateway:PORT"
RES = "http://USER:PASS@gate.quantumproxies.io:PORT"

def probe(label, fn):
    try:
        print(f"{label:26} -> {fn().status_code}")
    except Exception as e:
        print(f"{label:26} -> {type(e).__name__}")

probe("requests  + datacenter",
      lambda: requests.get(TARGET, proxies={"https": DC}, timeout=30))
probe("curl_cffi + datacenter",
      lambda: curl_cffi.get(TARGET, proxy=DC, impersonate="chrome", timeout=30))
probe("requests  + residential",
      lambda: requests.get(TARGET, proxies={"https": RES}, timeout=30))
probe("curl_cffi + residential",
      lambda: curl_cffi.get(TARGET, proxy=RES, impersonate="chrome", timeout=30))

像真值表一样读取四个结果:

而不是一次运行几十次。两个阻止层都是概率性的,单个 200 几乎没有告诉您任何信息。

使用真实家庭 IP 测试住宅行

图示显示来自数据中心 IP 的浏览器匹配 TLS 握手被阻止,而来自住宅 IP 的相同握手通过
冒充和 IP 声誉是独立的门槛。修复一个而留下另一个是“我切换到 curl_cffi 但没有变化”如此常见的报告的原因。

当额外的依赖不值得时

坦诚比重写更便宜。当以下情况时,请继续使用 requests:

而且大多数人忽略了一条中间路径:您不必放弃 requests 来获得握手。维护者指向 curl-adapter,它将 curl_cffi 安装为 requests 传输适配器,而 PyPI 上的 httpx-curl-cffi 则为 httpx 做同样的事情。您保留现有的代码和生态系统,只有线上字节发生变化。

迁移需要先了解的陷阱

API 足够接近,大多数脚本在更改导入后即可运行,但兼容性页面列出了实际差异,在进行大规模移植之前阅读它们是值得的。重定向响应体不会保留在 Response.history 中。具有空域的 Cookie 在重定向时可能会丢失。流响应对象无法被序列化,尽管普通响应可以。文件 API 略有不同。而且根本没有传输或适配器,因为库故意与 libcurl-impersonate 焊接在一起。代理配置也有一个小的不同之处,这让人们感到困惑:

# requests: dict, and retries mounted on an adapter
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

s = requests.Session()
s.mount("https://", HTTPAdapter(max_retries=Retry(total=4, status_forcelist=[429, 503])))
r = s.get(url, proxies={"https": "http://USER:PASS@gate.quantumproxies.io:PORT"}, timeout=30)

# curl_cffi: single proxy string preferred, retries are a session parameter
from curl_cffi import Session
from curl_cffi.requests import RetryStrategy

s = Session(
    impersonate="chrome",
    proxy="http://USER:PASS@gate.quantumproxies.io:PORT",
    retry=RetryStrategy(count=4, delay=1.0, backoff="exponential"),
    timeout=30,
)
r = s.get(url)  # note: retry fires on transport errors, not on 429/503

完整的代理界面——字典键、proxy_auth、每请求轮换、异步和生成无用的 WRONG_VERSION_NUMBER 错误的 https:// 前缀——在我们的 curl_cffi 代理指南 中逐步介绍。如果您选择不更换,另一方的等效参考是我们的 Python Requests 代理指南

常见问题

curl_cffi 比 requests 快吗?

是的,该项目的基准测试将其与 aiohttp 和 pycurl 相提并论,而不是 requests。增益来自 libcurl 在 C 中完成的工作加上 HTTP/2 多路复用,而不是聪明的 Python。对于少量顺序调用,差异是不可见的;在高并发情况下,尤其是异步情况下,差异是显著的。

curl_cffi 使用安全吗?

它是 MIT 许可的,广泛部署,并提供预编译的轮子,因此没有构建步骤需要审核。有一个值得注意的警告:v0.15.0 的建议涵盖基于重定向的 SSRF。如果您获取其他人提供的 URL,请设置 allow_redirects="safe" 或禁用重定向。冒充浏览器是一种技术措施,而不是忽视站点条款的许可。

curl_cffi 能绕过 Cloudflare 吗?

它去除了 TLS 和 HTTP/2 指纹特征,从而清除了基本保护级别。它无法执行 JavaScript 挑战,解决 Turnstile 或修复被标记的出口 IP。维护者在他们的 FAQ 中也这么说,并建议为更高层级使用更好的代理池加上浏览器自动化。

curl_cffi 与 httpx 或 tls_client——我应该使用哪个?

httpx 为您提供 HTTP/2 和异步,但没有指纹冒充,因此在隐蔽性方面介于 requests 和 curl_cffi 之间。tls_client 也伪造 TLS 配置文件,基准测试类似;curl_cffi 拥有更大的社区,并增加了 HTTP/3 和 websockets。如果 httpx 已经在您的堆栈中,httpx-curl-cffi 传输可以让您在不重写的情况下实现冒充。

简而言之:当您的目标读取握手时切换到 curl_cffi,当不读取时保持在 requests 上,并且永远不要期望任何一种选择能够清洗数据中心 IP。客户端在一个轴上不同,代理在另一个轴上不同,阻止的爬虫几乎总是关于两者的故事。运行四个探测,读取真值表,并修复数据指向的轴,而不是互联网大喊的那个。

修复冒充无法触及的轴