先说这事怎么开始的
去年十月接了个活,抓某招聘站点的岗位数据,客户说"就几万条,不多"。我惯性上了 requests 2.31.0 + fake-useragent + 一个二十来块钱的机房代理池,Python 3.11.6,4C8G 的阿里云机器。跑起来十分钟,前 200 条没问题,然后开始大面积 403,返回的 header 里带 cf-ray,body 里是 Cloudflare 的 "Verify you are human" 页面。
我第一反应是 IP 被封了,换代理。换了还是 403。换住宅代理,还是 403。那天晚上我翻了三个小时的代理服务商后台,差点去找客服退款。直到第二天早上我用浏览器手动打开同一个 URL,秒开,才意识到问题根本不在 IP 上——是我这个 Python 客户端在握手阶段就把自己卖了。
这段是给同样卡在这里的人提个醒:403 且有 cf-ray / 没有 cf-ray 但 body 很短返回 200 空内容,先怀疑指纹,别急着花钱买代理。 判断方法很简单,curl -v 'https://目标站' 看 TLS 握手协商出来的 cipher suite,再对比你浏览器里 https://tls.browserleaks.com/json 的结果。我那次对比:requests 走的是 OpenSSL 默认套件,JA3 指纹是一串在整个互联网上只有脚本才会用的值;Chrome 124 的 JA3 完全另一串。两边一放一起,太明显了。
四个方案,1000 次样本的实测数据
测试条件统一:同一个 URL 列表 1000 条,每条间隔 1.5~3 秒随机(这个随机很重要,固定 2 秒是最蠢的),只取 HTML 不加载资源,并发压到 8,跑三轮取平均。
requests+ UA 随机:11.3%,平均响应 386ms,403 集中在第 150 条之后httpx 0.27开 HTTP/2:14.1%,强一点点但没本质区别,h2 的指纹同样不对curl_cffi 0.7.4,impersonate="chrome124":68.4%,直连,没加任何代理curl_cffi+ 住宅代理:91.2%,剩下 8.8% 是 Cloudflare 的 Turnstile 硬挑战,纯协议层过不去playwright 1.47.0默认 headless chromium:47.0%playwright+playwright-stealth:51.6%,这个库去年就不怎么更新了,别指望patchright(playwright 的补丁分支):78.9%camoufox(定制 firefox):84.6%,但启动一个实例要 3~5 秒,重
这组数不是什么权威基准,换个站点全得重测,我自己换到另一个电商站,camoufox 掉到 60% 出头,patchright 反而更稳。但相对排序在我测过的六七个站里基本没变过:纯协议层能解决的,别上浏览器;上了浏览器,普通 playwright 基本等于裸奔。
curl_cffi 怎么用,以及三个坑
用法比想象中简单,它就是把 curl-impersonate 包了一层:
from curl_cffi import requests
s = requests.Session(impersonate="chrome124")
r = s.get(url, timeout=15, proxies={"https": "http://user:pass@host:port"})
print(r.status_code, len(r.text))
坑一:impersonate 的可用值不是随便写的,得看 from curl_cffi.requests import BrowserType 里列出来的,我一开始手写 "chrome120" 报 ImpersonateError,排查了半小时以为是版本问题。目前 0.7.x 支持到 chrome124、chrome131、safari17_0 这些,版本号对不上直接抛异常。
坑二:一定要用 Session,不要每次 requests.get。 每次新建连接都要重新 TLS 握手,我实测单个握手 60~130ms,1000 条请求光握手就浪费两分钟,而且频繁新建连接本身也是个特征。Session 复用之后我这边整体耗时从 6 分 40 秒压到 4 分 12 秒。
坑三:0.5.x 到 0.7.x 的 API 变过,网上 2023 年那批教程里的写法现在跑不通,AsyncSession 的参数名也改过。我是直接看的 GitHub README,别搜博客,搜出来一半是过期的。另外它不支持 HTTP/3,遇到强制 h3 的站点会直接回落,这种情况只能上浏览器。
Playwright 那一路,坑更碎
先说最容易踩的:Playwright 启动 Chromium 时默认会带上 --enable-automation,光在 args 里加 --disable-blink-features=AutomationControlled 是没用的,因为它不会覆盖默认参数。正确做法是:
browser = p.chromium.launch(
headless=True,
ignore_default_args=["--enable-automation"],
args=["--disable-blink-features=AutomationControlled"]
)
再说内存。这个是我那次被坑得最惨的地方。一个 browser.new_context() 大概吃 180~250MB,我那台 8G 的机器跑到 30 个 context 就开始 swap,跑到 40 个直接 OOM 把进程杀了,日志里只有一行 Killed,什么都看不出来。后来改成 context 池,一个 context 跑 50 个页面就 close() 重建,稳定在 22 个 context 左右。
还有个偏门的:某些站点会检测 CDP 的 Runtime.enable,Playwright 一启动就会调这个,等于自己举手。这个我没找到彻底解法,只能换 patchright,它在这个层面做了处理。camoufox 走的是 firefox 路线,指纹和 chromium 系完全两套,反而更难被识别——代价是包体大、冷启动慢,我在 CI 里跑它的时候专门加了 5 秒等待。
我现在的判断标准变了
以前我选方案看成功率,现在不太看了。
去年年中我给一个站做爬虫,用 playwright 方案,成功率 79%,看着一般。但那站半年改了三次版,每次我只改两三个 selector,前后不到两小时。隔壁团队同一个站用的是逆向签名 + 纯协议方案,成功率 95%,每次改版要重扒一遍 JS,找那个被混淆的 sign 函数,一次两天起。按 150 元/小时算,我两次维护 300 块,他们三次维护 1800 块。成功率差 16 个点,成本差 6 倍。
我现在管这个叫失效响应时间:从目标站改版,到你发现、修好、恢复跑数,中间隔多久。这个指标比成功率更值得写进方案文档。选型的时候我会问自己三个问题——这个站多久改一次版、改版后我能多快定位、这套代码除了我还有没有人能接。第三个问题经常被忽略,但我吃过亏:用 camoufox 那套方案是我一个人搭的,后来我休假十天,线上挂了没人会修。
顺带说个副作用:成功率越高的方案,往往越依赖浏览器,机器成本跟着涨。我那个站从 4C8G 升到 8C16G,阿里云按量付费每月多出来 260 块左右,一年三千。所以算总账的时候别只算代理钱,把机器和人力一起算进去,结论经常是反的。
最后几句不太好听的
频率控制这件事,我见过太多人图快把间隔设成固定 0.5 秒,然后抱怨人家封得快。请求间隔加随机抖动、单 IP 每分钟不超过 20 次、夜里跑量白天停——这三条能让你少掉一大半麻烦,而且不花一分钱。robots.txt 该看一眼还是看一眼,尤其是 Crawl-delay 那行。
还有,别碰个人信息。我上面说的采集都是公开发布的岗位信息、商品价格这类,涉及手机号、身份证、简历内容的,一个都别沾,《个人信息保护法》那套东西不是闹着玩的,客户给你多少钱都不值。
对了,Cloudflare 的 Turnstile 我是真没找到纯协议层的稳定解法,谁有靠谱思路可以说说,我到现在还是靠降频 + 绕路解决的,不太优雅。