title: 一天搞定 Cloudflare Turnstile:用 AI 协作改写一个签到脚本 slug: cloudflare-turnstile-rewrite publishedAt: 2026-04-28 excerpt: 某个国外站点把验证码升级到 Turnstile,旧自动化脚本失效。三轮 vibe-coding 完成对接的过程与教训。 tags: [automation, case-study, turnstile] readingMin: 9 wordCount: 1430 draft: false
朋友委托:旧签到脚本挂了
某个国外站点把验证码从老式 reCAPTCHA v2 升级到了 Cloudflare Turnstile。朋友手里有一个跑了一年多的 Python 签到脚本,升级当天直接 401。来找我帮忙复活。
事情不复杂,但有几个看点:
- 从未碰过 Turnstile:训练数据里 Turnstile 文档不算多,模型回答经常飘
- 不能让朋友停机太久:他订阅了 VIP,断签到就掉等级
- 不能签到失败之后被站点拉黑:Turnstile 失败次数过多会进黑名单,要小心试
整个事情用 3 轮 vibe-coding 跑通,这里把过程写一遍。所有具体的站点信息全部模糊化 —— 这不是教程,是案例研究。
第一轮:理解 Turnstile 到底是什么
让 AI 干活之前先让自己懂。读了 Cloudflare 官方文档,几条关键事实:
- Turnstile 是无感知验证码:不像 reCAPTCHA 让用户选图,Turnstile 在后台跑一组 fingerprint 检测,绝大多数情况用户什么都不用做
- 页面前端会有一个
<div class="cf-turnstile" data-sitekey="0xAAAA...">,Turnstile 的 JS 在这个 div 里渲染一个隐藏 iframe - 验证通过后,iframe 回写一个
cf-turnstile-response隐藏<input>,里面是一段 token - 后端要做的事:把 token 提交给
/api/.../action,站点服务端用自己的 secret key 去 Cloudflare 校验 token
所以自动化脚本要做的不是"破解 Turnstile",而是让 Turnstile 像对真实浏览器一样,正常完成 fingerprint 检测,然后拿到 token 提交。
区分两种"绕过 Turnstile"的需求
这一点必须说清楚:
- 正当:你是这个站点的合法用户,你有账号,你想自动化日常操作 —— 这是脚本应该解决的
- 不正当:你想批量注册 / 刷接口 / 攻击站点 —— 这是 Turnstile 设计要挡的
朋友的需求毫无疑问是前者:已有 VIP 账号,只是想自动签到。但工程实现上两种需求看起来一样,所以脚本写得越保守越好 —— 绝不批量调用,签到完立即退出,日内只跑一次。
第二轮:让 AI 写第一稿
把背景丢给 Claude:
用户有一个 Python 签到脚本,目标站点验证码从 reCAPTCHA 升级到了 Cloudflare Turnstile。脚本不需要破解 Turnstile,需要的是真实浏览器自动化让 Turnstile 跑过,拿到 token 提交。请给一个 Playwright 方案。
Claude 给的第一稿大致这样:
from playwright.sync_api import sync_playwright
def sign_in(username: str, password: str):
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
ctx = browser.new_context()
page = ctx.new_page()
page.goto("https://example.com/login")
page.fill('input[name="username"]', username)
page.fill('input[name="password"]', password)
# 等 Turnstile 完成
page.wait_for_selector('input[name="cf-turnstile-response"]', state="attached")
token = page.input_value('input[name="cf-turnstile-response"]')
page.click('button[type="submit"]')
page.wait_for_url("**/dashboard")
browser.close()看起来合理,但跑起来 80% 失败。
第一稿失败的两个原因
观察了几次,失败模式是:
- headless=True 时 Turnstile 直接拒绝:Cloudflare 能识别 headless Chromium 的特征(navigator.webdriver / 缺少 plugins / 等等),直接判定为 bot
input字段填充太快:page.fill是瞬间填的,真实用户敲键盘有时序。Turnstile 的 fingerprint 检测会看 input event 的时间分布
修法分别是:
browser = p.chromium.launch(
headless=False, # 关键:用有头模式
args=[
"--disable-blink-features=AutomationControlled", # 隐藏 webdriver 标志
],
)
# 用 type 而非 fill,模拟真实敲键
page.type('input[name="username"]', username, delay=80)
page.type('input[name="password"]', password, delay=80)delay=80 是关键:每个按键间隔 ~80ms,接近真实人类敲键速度(每分钟 ~150 字符)。这条改完后通过率从 20% 拉到 ~85%。
第三轮:让脚本"诚实"
跑了几天 85% 通过率,剩 15% 失败有几种:
- 服务器在欧洲机房,触发了一些地理风控
- Cloudflare 偶尔给"挑战"模式:不是无感验证,而是要 click 一个复选框
- 签到接口本身有 rate limit:同一 IP 一天调 3 次以上会 429
最终的脚本架构是这样的:
def sign_in_with_retry(max_retries: int = 2) -> bool:
"""每天只签到一次,失败最多重试 2 次,中间间隔 5 分钟。"""
for attempt in range(max_retries + 1):
try:
result = sign_in()
if result == "success":
return True
if result == "challenged":
# Turnstile 给挑战 → 人工接入
notify_owner("turnstile_challenged")
return False
if result == "rate_limited":
# 站点 429 → 直接放弃当天
return False
except Exception as e:
log_error(e)
if attempt < max_retries:
time.sleep(300) # 5 分钟后重试
return False几条工程纪律值得记下来:
- 失败有分类:不是所有失败都该重试。
challenged/rate_limited这种本质上不可重试的,要识别后直接退出 - 跑前再核对最后签到时间:防止脚本重启被两次执行
- 失败通知机制:用最简陋的方式(Telegram bot / 邮件),让脚本"知道"有人会管它
- 绝不并发:一个账号一次只跑一个 instance
长期跑下来的稳定性
部署到 cron(每天 UTC 02:00 跑一次)之后,大约 95% 一次成功,4% 重试一次成功,1% 完全失败需要人工。考虑到 Turnstile 的设计是就是为了让自动化变难,这个数字已经足够好。
AI 协作在这种案例里能干什么
事后复盘,这一天的工作里 AI 真正帮上忙的是:
- 第一轮的"读文档 + 输出结构化总结":让我快速理解 Turnstile 是什么、要做什么
- 第二轮的"写脚本第一稿":省了我手敲 Playwright API 的时间
- debug 失败模式时的"假设 → 验证"循环:我看 log,AI 提假设,我去试,reciprocate 几轮就找到问题
AI 没能帮上的是:
- 不能告诉我 headless 会被 Cloudflare 识别:这条要真跑了一遍才能观察到。AI 训练数据里没有这条具体的失败模式
- 不能告诉我
delay=80ms这个数字:这是基于"接近人类敲键速度"经验值,AI 一开始给的是delay=50,跑下来发现还是太快
所以"协作"的本质,是 AI 提供"广度",我提供"深度":AI 让我快速贴近问题,真正 debug 时仍需要人在 loop。
给在做类似事情的人的建议
最后落几条非泛泛而谈的:
- headless=False 是 Turnstile 自动化的起点,不是终点,但起点必须正确
type(..., delay=80)而不是fill(...),这一条变化能从 20% 拉到 85%- 写一个"挑战模式被触发"的人工接入路径:不是所有失败都该自动重试
- 签到前后都打 log:出问题时你需要知道是 Turnstile 没过 / 登录失败 / 还是 rate limit
这篇没有放完整代码,因为完整代码会暴露目标站点信息,违反了"对朋友项目保密"的承诺。这是一个案例研究,不是教程。