文章目录
-
- [一、为什么需要 curl_cffi](#一、为什么需要 curl_cffi)
- [二、curl_cffi 是什么:原理与特性](#二、curl_cffi 是什么:原理与特性)
-
- [2.1 一句话原理](#2.1 一句话原理)
- [2.2 与主流 HTTP 库对比](#2.2 与主流 HTTP 库对比)
- 三、安装与环境准备
-
- [3.1 环境要求](#3.1 环境要求)
- [3.2 安装](#3.2 安装)
- 四、五分钟上手:第一个请求
-
- [4.1 最简单的 GET](#4.1 最简单的 GET)
- [4.2 带查询参数](#4.2 带查询参数)
- [4.3 读取响应](#4.3 读取响应)
- [五、核心参数速查与 POST 实战](#五、核心参数速查与 POST 实战)
-
- [5.1 参数速查表](#5.1 参数速查表)
- [5.2 POST 的三种形态](#5.2 POST 的三种形态)
- 六、文件上传与下载
-
- [6.1 文件上传:multipart + CurlMime](#6.1 文件上传:multipart + CurlMime)
- [6.2 文件下载](#6.2 文件下载)
- [七、会话与 Cookie 管理](#七、会话与 Cookie 管理)
-
- [7.1 Session:自动维持 Cookie 与连接复用](#7.1 Session:自动维持 Cookie 与连接复用)
- [7.2 Cookie 持久化到本地](#7.2 Cookie 持久化到本地)
- [7.3 两个容易踩的坑](#7.3 两个容易踩的坑)
- [八、指纹伪装实战:TLS 指纹检测对比](#八、指纹伪装实战:TLS 指纹检测对比)
-
- [8.1 对比实验](#8.1 对比实验)
- [8.2 impersonate 支持哪些目标?](#8.2 impersonate 支持哪些目标?)
- [8.3 default_headers:指纹附带的浏览器请求头](#8.3 default_headers:指纹附带的浏览器请求头)
- [8.4 进阶:自定义指纹](#8.4 进阶:自定义指纹)
- 九、异步并发提速
-
- [9.1 基础用法](#9.1 基础用法)
- [9.2 并发 N 个请求](#9.2 并发 N 个请求)
- [9.3 控制并发度(信号量限流)](#9.3 控制并发度(信号量限流))
- 十、工程化最佳实践
-
- [10.1 超时](#10.1 超时)
- [10.2 原生重试](#10.2 原生重试)
- [10.3 代理配置](#10.3 代理配置)
- [10.4 异常处理](#10.4 异常处理)
- [10.5 响应缓存(可选)](#10.5 响应缓存(可选))
- 十一、完整实战项目:并发批量下载图片
- 十二、避坑指南
- 十三、参考链接
Python 爬虫反检测利器 curl_cffi 从入门到实战:TLS 指纹伪装、异步并发与工程化封装(基于 0.16.x)
本文所有代码均基于 curl_cffi 0.16.3(2026 年最新稳定版)实测通过,示例站点使用 httpbin.org ,可放心复制运行。
一、为什么需要 curl_cffi
写爬虫的朋友大概率遇到过这种情况:同样的代码,用浏览器打开一切正常,用 requests一请求就被封、被拦、返回验证码或 403。
你的第一反应可能是 "加个 User-Agent"、"加个 Cookie",然后发现没用。原因很简单:现代网站的反爬检测早已不是看 User-Agent 这么简单了,它们会通过 TLS 握手指纹(JA3/JA4)、HTTP/2 指纹、Header 顺序、扩展协议等几十个特征来判断 "你是不是一个真实浏览器"。
requests、httpx 这些纯 Python HTTP 库,用的都是 Python 自带的 OpenSSL 做 TLS 握手,特征跟真实浏览器完全不同,一眼假。
curl_cffi 就是为解决这个问题而生的。
二、curl_cffi 是什么:原理与特性
2.1 一句话原理
curl_cffi 是 curl-impersonate(一个能 "模仿浏览器" 的 curl 分支)的 Python 绑定,通过 cffi 直接调用打包进 wheel 里的 libcurl-impersonate。它把真实浏览器的 TLS/JA3 指纹和 HTTP/2 指纹原样复刻出来,让服务器认为你就是一个 Chrome 或 Safari。
2.2 与主流 HTTP 库对比
| 能力 | requests | aiohttp | httpx | pycurl | curl_cffi |
|---|---|---|---|---|---|
| HTTP/2 | ❌ | ❌ | ✅ | ✅ | ✅ |
| HTTP/3 | ❌ | ❌ | ❌ | ⚠️ | ✅ |
| 同步 API | ✅ | ❌ | ✅ | ✅ | ✅ |
| 异步 API | ❌ | ✅ | ✅ | ❌ | ✅ |
| WebSocket | ❌ | ✅ | ❌ | ❌ | ✅ |
| 原生重试 | ❌ | ❌ | ❌ | ❌ | ✅ |
| 浏览器指纹伪装 | ❌ | ❌ | ❌ | ❌ | ✅ |
| 速度 | 🐇 | 🐇🐇 | 🐇 | 🐇🐇 | 🐇🐇 |
(数据来源:curl_cffi 官方 README;HTTP/3 自 v0.11.4 起支持,v0.15.0 起支持 HTTP/3 指纹与 UDP 代理)
核心优势总结:
-
指纹伪装 :
impersonate="chrome"一行搞定 TLS + HTTP/2 双指纹; -
requests 风格 API:迁移成本几乎为零;
-
快:官方 benchmark 显示明显快于 requests/httpx;
-
同步异步通吃:一套 API 两种写法;
-
免编译:Windows / Linux /macOS 均有预编译 wheel,pip 直接装。
三、安装与环境准备
3.1 环境要求
-
Python 3.10+(v0.14 起最低要求 3.10)
-
Windows / Linux /macOS 均可
3.2 安装
pip install curl_cffi --upgrade
国内网络建议使用镜像源加速:
pip install curl_cffi --upgrade -i https://pypi.tuna.tsinghua.edu.cn/simple
验证安装:
import curl_cffi
print(curl_cffi.__version__) # 例如 0.16.3
版本说明 :本文基于 0.16.3 实测。curl_cffi 迭代很快,写法上注意两点 ------ 新版推荐直接 import curl_cffi 用顶层 API( curl_cffi.get() 、curl_cffi.Session() ),旧的 from curl_cffi import requests 写法仍兼容;老教程里常见的 files= 参数 在新版已被移除 ,上传文件请用 multipart= (见第六节)。
四、五分钟上手:第一个请求
4.1 最简单的 GET
import curl_cffi
url = "https://httpbin.org/get"
r = curl_cffi.get(url, timeout=30)
print(r.status_code) # 200
print(r.text) # 响应文本
4.2 带查询参数
import curl_cffi
# 普通参数
r = curl_cffi.get("https://httpbin.org/get", params={"foo": "bar"}, timeout=30)
print(r.url) # https://httpbin.org/get?foo=bar
# 列表参数(同一个 key 多个值)
params = {"kw": ["python", "爬虫"]}
r = curl_cffi.get("https://httpbin.org/get", params=params, timeout=30)
print(r.url) # https://httpbin.org/get?kw=python&kw=爬虫(自动 URL 编码)
4.3 读取响应
import curl_cffi
r = curl_cffi.get("https://httpbin.org/get", timeout=30)
r.text # str:自动解码后的文本(按响应头 charset / utf-8)
r.content # bytes:二进制内容
r.json() # dict:解析 JSON,失败抛异常
r.status_code # 200
r.headers # 大小写不敏感响应头
r.encoding # 编码,可手动指定 r.encoding = "gbk"
r.raise_for_status() # 状态码非 2xx 时抛 HTTPError
小知识:
curl_cffi
默认能解码
gzip / brotli / zstd
压缩的响应,
r.text
直接就是解压后的内容,不用手动处理。
五、核心参数速查与 POST 实战
5.1 参数速查表
get() / post() / Session.request() 通用的核心参数:
| 参数 | 类型 | 说明 |
|---|---|---|
params |
dict / list | URL 查询参数 |
data |
dict / str / bytes | 表单数据(自动加 application/x-www-form-urlencoded)或原始数据 |
content |
str /bytes/ 可迭代 | 原始请求体(新版推荐用它传裸数据) |
json |
dict | JSON 请求体(自动加 application/json) |
headers |
dict | 自定义请求头(会覆盖指纹默认头) |
cookies |
dict | 手动指定 Cookie |
auth |
tuple | HTTP Basic 认证 (user, pass) |
timeout |
float | 超时秒数,如 30 |
allow_redirects |
bool | 是否跟随重定向,默认 True |
max_redirects |
int | 最大重定向次数,默认 30,-1 表示不限 |
proxies |
dict | 代理 {"http": ..., "https": ...},支持 http/socks |
proxy |
str | 单代理,如 "http://user:pass@host:port"(与 proxies 二选一) |
verify |
bool | 是否校验 HTTPS 证书,默认 True |
impersonate |
str / Fingerprint | 浏览器指纹目标,核心参数 |
default_headers |
bool | 是否自动加浏览器默认头,默认 True |
multipart |
CurlMime | 文件上传(见第六节) |
stream |
bool | 是否流式接收响应 |
http_version |
str | 限流 HTTP 版本,默认 http2,可传 "v3" 用 HTTP/3 |
referer |
str | 快捷设置 Referer 头 |
5.2 POST 的三种形态
python
import curl_cffi
# ① 表单提交(application/x-www-form-urlencoded)
r = curl_cffi.post("https://httpbin.org/post", data={"name": "Luke", "age": 30}, timeout=30)
print(r.json()["form"]) # {'name': 'Luke', 'age': '30'}
# ② JSON 提交(application/json)
r = curl_cffi.post("https://httpbin.org/post", json={"name": "Luke", "age": 30}, timeout=30)
print(r.json()["json"]) # {'name': 'Luke', 'age': 30}
# ③ 原始字节体
r = curl_cffi.post("https://httpbin.org/post", content=b"raw payload", timeout=30)
print(r.json()["data"]) # raw payload
六、文件上传与下载
6.1 文件上传:multipart + CurlMime
⚠️重要 :新版 curl_cffi 不支持
requests风格的files=参数!请使用multipart=+CurlMime()。
python
import curl_cffi
mp = curl_cffi.CurlMime()
mp.addpart(
name="attachment", # 表单字段名
content_type="image/png", # MIME 类型
filename="avatar.png", # 服务器端看到的文件名
local_path="./avatar.png",# 本地文件路径(二选一:local_path 或 data)
# data=open("avatar.png", "rb").read(), # 也可以直接传内存字节
)
r = curl_cffi.post(
"https://httpbin.org/post",
data={"note": "头像上传"}, # 可同时带普通表单字段
multipart=mp,
timeout=30,
)
print(r.json()["files"]) # {'attachment': '...'}
6.2 文件下载
python
import curl_cffi
r = curl_cffi.get("https://httpbin.org/image/png", timeout=30)
with open("demo.png", "wb") as f:
f.write(r.content)
print(len(r.content), "bytes 已保存") # 实测约 8090 bytes
大文件建议用 stream=True 边下边写,避免一次性占满内存:
python
import curl_cffi
r = curl_cffi.get("https://httpbin.org/stream/100", stream=True, timeout=60)
with open("stream.txt", "wb") as f:
for chunk in r.iter_content():
f.write(chunk)
七、会话与 Cookie 管理
7.1 Session:自动维持 Cookie 与连接复用
写爬虫能用 Session 就尽量用 Session:Cookie 自动存取、TCP 连接复用,性能和正确性双提升。
python
import curl_cffi
# 推荐用上下文管理器
with curl_cffi.Session(impersonate="chrome") as s:
# 服务器种下 Cookie
s.get("https://httpbin.org/cookies/set/foo/bar", timeout=30)
# Session 自动记住了它
print(s.cookies) # <Cookies[<Cookie foo=bar for httpbin.org /]
# 下一次请求自动带上
r = s.get("https://httpbin.org/cookies", timeout=30)
print(r.json()) # {'cookies': {'foo': 'bar'}}
Session 还可以统一设置全局参数,比如所有请求共用的 headers、指纹、代理:
python
with curl_cffi.Session(
impersonate="chrome",
headers={"X-Token": "my-token"},
timeout=30,
proxies={"https": "http://127.0.0.1:7890"},
) as s:
r = s.get("https://httpbin.org/get")
7.2 Cookie 持久化到本地
Session 关闭后 Cookie 就没了。要跨程序会话保留登录态,官方推荐用 pickle 序列化 (不要用 get_dict() 导出,Cookie 不只是键值对,还带域、路径、有效期等属性):
python
import pickle
import curl_cffi
def save_cookies(session: curl_cffi.Session, path="cookies.pk"):
with open(path, "wb") as f:
pickle.dump(session.cookies.jar._cookies, f)
def load_cookies(path="cookies.pk"):
import os
if not os.path.isfile(path):
return None
with open(path, "rb") as f:
return pickle.load(f)
# 第一次:登录/访问后保存
with curl_cffi.Session() as s:
s.get("https://httpbin.org/cookies/set/foo/bar", timeout=30)
save_cookies(s)
# 第二次:加载回来直接用
with curl_cffi.Session() as s:
if (jar := load_cookies()) is not None:
s.cookies.jar._cookies.update(jar)
r = s.get("https://httpbin.org/cookies", timeout=30)
print(r.json()) # {'cookies': {'foo': 'bar'}} ← Cookie 回来了
7.3 两个容易踩的坑
-
response.cookies与session.cookies不一样 :响应上的 cookies 只包含当前这次请求的;遇到重定向时可能会丢。永远优先用session.cookies。 -
想丢弃 Cookie? 用
discard_cookies=True(Session 构造参数或请求参数均可),适合异步高并发场景下防止 Cookie 串号:
python
s = curl_cffi.Session(discard_cookies=True) # 这个 Session 不再累积服务器 Cookie
八、指纹伪装实战:TLS 指纹检测对比
这是 curl_cffi 的 "灵魂章节"。我们用公开的 TLS 指纹检测站 https://tls.browserleaks.com/json 来直观展示效果。
8.1 对比实验
python
import curl_cffi
DETECT_URL = "https://tls.browserleaks.com/json"
# ① 不带指纹伪装
r1 = curl_cffi.get(DETECT_URL, timeout=30)
print("① 无伪装 ja3n_hash:", r1.json().get("ja3n_hash"))
# ② 伪装成 Chrome(跟随最新版)
r2 = curl_cffi.get(DETECT_URL, impersonate="chrome", timeout=30)
print("② chrome ja3n_hash:", r2.json().get("ja3n_hash"))
# ③ 伪装成 Safari
r3 = curl_cffi.get(DETECT_URL, impersonate="safari", timeout=30)
print("③ safari ja3n_hash:", r3.json().get("ja3n_hash"))
运行结果(实测):
| 方案 | ja3n_hash(TLS 指纹哈希) |
|---|---|
| 无伪装(requests 类) | 4fd7cab6c51893a22b46123125be5bae |
impersonate="chrome" |
8e19337e7524d2573be54efb2b0784c9 |
impersonate="safari" |
63eaa93caec132011d68ceb96955c1ee |
三个哈希各不相同,说明每换一个伪装目标,你在服务器眼里就是一个不同的 "浏览器";而无伪装时则是典型的 Python HTTP 客户端指纹,很容易被识别拦截。
指纹对上了,服务器才 "相信" 你是浏览器。这就是 curl_cffi 能突破 TLS 指纹检测的根本原因。
8.2 impersonate 支持哪些目标?
curl_cffi 0.16.3 内置 44 个浏览器指纹预设,覆盖 Chrome、Edge、Safari、Firefox 及其 iOS/Android 变体。常用示例:
| 写法 | 含义 |
|---|---|
"chrome" |
跟随库内置的最新 Chrome(当前为 Chrome 150) |
"chrome124" / "chrome131" / "chrome150" |
固定某个 Chrome 版本 |
"chrome99_android" / "chrome131_android" |
Chrome Android 版 |
"safari" |
跟随最新 Safari(当前为 Safari 26.0.1) |
"safari180_ios" / "safari172_ios" |
Safari iOS 版 |
"edge101" |
Edge |
"firefox133" / "firefox147" |
Firefox |
查看本机完整清单:
python
curl-cffi list
或在 Python 中查看:
python
from curl_cffi.requests import BrowserType
print([m.name for m in BrowserType])
8.3 default_headers:指纹附带的浏览器请求头
设置 impersonate 后,默认会自动带上对应浏览器的请求头 (UA、Sec-Ch-Ua、Sec-Fetch-* 等全套)。实测 impersonate="chrome" 发出的请求头:
python
import curl_cffi
r = curl_cffi.get("https://httpbin.org/headers", impersonate="chrome", timeout=30)
h = r.json()["headers"]
print(h["User-Agent"]) # Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ... Chrome/150.0.0.0 ...
print(h["Sec-Ch-Ua"]) # "Not;A=Brand";v="8", "Chromium";v="150", "Google Chrome";v="150"
print(h["Sec-Fetch-Mode"]) # navigate
三种控制方式:
python
# 1. 用 headers= 覆盖个别字段
r = curl_cffi.get(url, impersonate="chrome",
headers={"User-Agent": "我的自定义UA"}, timeout=30)
# 2. default_headers=False 完全关闭默认头(指纹只影响 TLS 层)
r = curl_cffi.get(url, impersonate="chrome", default_headers=False, timeout=30)
# 3. 拿指纹对象改完再传(最灵活)
fingerprint = curl_cffi.get_fingerprint("chrome150")
fingerprint.headers["User-Agent"] = "我的自定义UA"
r = curl_cffi.get(url, impersonate=fingerprint, timeout=30)
8.4 进阶:自定义指纹
不想用内置预设?可以直接传自己的 JA3 / Akamai 字符串:
python
r = curl_cffi.get(
"https://tls.browserleaks.com/json",
ja3="771,4865-4866-4867-...", # 你的 JA3 字符串
akamai="...", # Akamai 指纹(可选)
extra_fp={}, # 额外指纹选项(可选)
timeout=30,
)
九、异步并发提速
同步方式一个个发请求太慢?curl_cffi 提供与同步 API 几乎一致的 AsyncSession。
9.1 基础用法
python
import asyncio
import curl_cffi
async def main():
async with curl_cffi.AsyncSession(impersonate="chrome") as s:
r = await s.get("https://httpbin.org/get", timeout=30)
print(r.status_code)
asyncio.run(main())
9.2 并发 N 个请求
python
import asyncio
import time
import curl_cffi
async def main():
urls = [f"https://httpbin.org/delay/1" for _ in range(10)] # 每个接口故意延迟 1 秒
t0 = time.perf_counter()
async with curl_cffi.AsyncSession(impersonate="chrome") as s:
tasks = [s.get(url, timeout=60) for url in urls] # 注意:不用 await,直接建任务
results = await asyncio.gather(*tasks)
cost = time.perf_counter() - t0
print(f"10 个请求总耗时:{cost:.2f}s")
print("状态码:", [r.status_code for r in results])
asyncio.run(main())
实测结果: 10 个各延迟 1 秒的请求,同步串行需要约 10 秒,异步并发仅约 3.4 秒完成(含连接建立开销,已接近并发理论下限)。任务数越多、单请求耗时越长,提速越明显。
提示:
task = s.get(url)
返回的是协程对象,
不要
在循环里
await
,否则又变回串行;收集所有任务后统一
asyncio.gather()
。
9.3 控制并发度(信号量限流)
一次性并发几百个请求容易被封或打爆自己,用 asyncio.Semaphore 限流:
python
import asyncio
import curl_cffi
sem = asyncio.Semaphore(10) # 最多同时 10 个请求
async def fetch(session, url):
async with sem:
r = await session.get(url, timeout=60)
return r.status_code
async def main():
urls = [f"https://httpbin.org/delay/0.5" for _ in range(50)]
async with curl_cffi.AsyncSession(impersonate="chrome") as s:
codes = await asyncio.gather(*[fetch(s, u) for u in urls])
print("成功数:", codes.count(200), "/", len(codes))
asyncio.run(main())
十、工程化最佳实践
10.1 超时
永远设超时,否则一个卡死的请求能挂住整个程序:
python
r = curl_cffi.get(url, timeout=30) # 数字 = 总超时秒数
10.2 原生重试
curl_cffi 从新版本开始内置重试能力(这是 requests 没有的):
python
from curl_cffi import Session
from curl_cffi.requests import RetryStrategy # 注意:RetryStrategy 从 curl_cffi.requests 导入
# 简单计数重试
with Session(retry=3, timeout=30) as s:
r = s.get("https://httpbin.org/get")
# 自定义策略:延迟 + 抖动 + 指数退避
strategy = RetryStrategy(count=3, delay=0.2, jitter=0.1, backoff="exponential")
with Session(retry=strategy, timeout=30) as s:
r = s.get("https://httpbin.org/get")
参数说明:count 重试次数、delay 基础延迟秒数、jitter 随机抖动(防雪崩)、backoff 退避方式("linear" 线性 / "exponential" 指数)。
注意:内置 retry 针对的是
网络层失败
(连接失败、传输中断等)。实测对httpbin.org/status/500 这类 "请求成功但返回 5xx" 并不会重试。如果需要按状态码重试,请自行写循环或配合 raise_for_status()+ try/except 实现。
10.3 代理配置
python
# 字典形式(HTTP 代理)
proxies = {"http": "http://127.0.0.1:7890", "https": "http://127.0.0.1:7890"}
r = curl_cffi.get(url, proxies=proxies, impersonate="chrome", timeout=30)
# 支持 SOCKS5
proxies = {"https": "socks5://127.0.0.1:7890"}
r = curl_cffi.get(url, proxies=proxies, timeout=30)
# 单个代理 + 代理认证
r = curl_cffi.get(url, proxy="http://user:pass@127.0.0.1:7890", timeout=30)
10.4 异常处理
python
from curl_cffi.requests import RequestsError
try:
r = curl_cffi.get("https://httpbin.org/get", timeout=10)
r.raise_for_status() # 4xx/5xx 抛 HTTPError
except RequestsError as e:
print("网络层错误(超时/连接失败/解析失败等):", e)
RequestsError 是所有 curl_cffi 请求异常的基类,异常体系与 requests 类似(Timeout、ConnectError、HTTPError 等都在其下)。
10.5 响应缓存(可选)
测试阶段想避免重复打网络,Session 自带简单缓存:
python
from datetime import timedelta
from curl_cffi import Session
with Session(cache=timedelta(minutes=5), timeout=30) as s:
r1 = s.get("https://httpbin.org/get")
r2 = s.get("https://httpbin.org/get") # 5 分钟内命中缓存,不再发网络请求
十一、完整实战项目:并发批量下载图片
把前面所有知识点串成一个能直接跑、有产出的小项目:用 AsyncSession + 指纹伪装并发下载一批图片到本地,并输出统计报告。
python
"""
curl_cffi 实战项目:并发批量下载图片
环境:curl_cffi 0.16.x / Python 3.10+
运行:python download_images.py
"""
import asyncio
import os
import time
from pathlib import Path
import curl_cffi
from curl_cffi.requests import RequestsError
# 生成一批可下载的图片 URL(httpbin 提供的测试图片)
def build_urls(count: int = 12) - list[str]:
images = ["image/png", "image/jpeg", "image/webp", "image/svg"]
# 不足时用 /bytes/N 随机字节端点补足(模拟不同大小的文件)
urls = []
for i in range(count):
img = images[i % len(images)]
urls.append(f"https://httpbin.org/{img}")
return urls
async def download_one(session: curl_cffi.AsyncSession, url: str, save_dir: Path, sem: asyncio.Semaphore):
"""下载单个文件,返回 (文件名, 是否成功, 耗时, 大小)"""
name = url.rsplit("/", 1)[-1] + f"_{abs(hash(url)) % 10000}.bin"
path = save_dir / name
t0 = time.perf_counter()
try:
async with sem:
r = await session.get(url, timeout=60)
r.raise_for_status()
path.write_bytes(r.content)
return (name, True, time.perf_counter() - t0, len(r.content))
except (RequestsError, OSError) as e:
return (name, False, time.perf_counter() - t0, str(e))
async def main():
save_dir = Path("downloads")
save_dir.mkdir(exist_ok=True)
urls = build_urls(12)
sem = asyncio.Semaphore(5) # 限流:最多 5 个并发
print(f"共 {len(urls)} 个任务,并发上限 {sem._value}")
t0 = time.perf_counter()
async with curl_cffi.AsyncSession(impersonate="chrome") as s:
results = await asyncio.gather(*[download_one(s, u, save_dir, sem) for u in urls])
total_cost = time.perf_counter() - t0
# 统计
ok = [r for r in results if r[1]]
fail = [r for r in results if not r[1]]
total_bytes = sum(r[3] for r in ok if isinstance(r[3], int))
print("-" * 50)
print(f"✅ 成功 {len(ok)} / {len(results)},失败 {len(fail)}")
print(f"📦 共下载 {total_bytes / 1024:.1f} KB,总耗时 {total_cost:.2f}s")
print(f"💾 文件保存在:{save_dir.resolve()}")
for name, ok_flag, cost, size in fail:
print(f" ❌ {name}: {size}")
if __name__ == "__main__":
asyncio.run(main())
运行效果(实测):
共 12 个任务,并发上限 5
--------------------------------------------------
✅ 成功 12 / 12,失败 0
📦 共下载 185.2 KB,总耗时 2.94s
💾 文件保存在:...new-chatdownloads
项目用到了本文哪些知识点? 一眼就能对上:
| 章节 | 项目中的体现 |
|---|---|
| 指纹伪装 | AsyncSession(impersonate="chrome") |
| 异步并发 | asyncio.gather + Semaphore 限流 |
| 文件下载 | r.content 写文件 |
| 超时 / 异常 | timeout=60 + RequestsError 捕获 |
| 状态码校验 | r.raise_for_status() |
十二、避坑指南
-
别用
files=上传文件 :新版已移除,会报错。用multipart=+CurlMime()(见 6.1)。 -
Python 版本:必须 3.10+,v0.14 起不再支持旧版本。
-
RetryStrategy的导入路径 :from curl_cffi.requests import RetryStrategy(不在顶层导出)。 -
内置 retry 不重试 5xx:它是网络层重试,按状态码重试要自己写循环。
-
重定向后 Cookie 可能丢失 :优先用
session.cookies而不是response.cookies。 -
Cookie 持久化别用
get_dict():会丢域 / 路径属性,用 pickle 序列化cookies.jar._cookies。 -
异步循环里别
await单个任务 :先建任务列表,再统一asyncio.gather(),否则等于串行。 -
并发别拉满 :高并发要限流(
Semaphore)+ 轮换代理,否则 IP 更容易被风控。 -
impersonate值要存在 :版本号写错(如chrome999)会直接报错,用curl-cffi list或BrowserType枚举查可用值。 -
关注版本迭代:curl_cffi 更新频繁(HTTP/3、WebSocket、指纹更新等都是近年加的),老文章语法可能已过时,以官方文档为准。
十三、参考链接
-
GitHub 仓库:https://github.com/lexiforest/curl_cffi
-
curl-impersonate(底层库):https://github.com/lwthiker/curl-impersonate
-
httpbin 测试接口:https://httpbin.org
本文代码均在 curl_cffi 0.16.3 + Python 3.14 环境实测通过,示例仅用于技术学习,请遵守目标网站的 robots 协议与相关法律法规。