先说结论:用 requests 处理 Cloudflare Turnstile 不需要起浏览器。三个动作 —— 抓 sitekey、提交给 CaptchaAI、轮询拿 token,最后把 token 作为表单字段 cf-turnstile-response POST 出去。剩下的都是超时和重试这类工程细节。
国内开发者最常在海外 SaaS 的注册页和 API 控制台上撞见它:页面里一个 div 带着 data-sitekey,脚本一跑就卡住。
安全范围: 本指南仅适用于你自有或经授权的 QA、staging 与预发布环境,不涉及第三方站点或未授权流程。
环境准备与前置条件
只需要一个 HTTP 客户端:
pip install requests
国内装包慢可加镜像源 -i https://pypi.tuna.tsinghua.edu.cn/simple。还需要准备:
- CaptchaAI 的 API Key,注册后在 CaptchaAI 控制台查看
- 目标页面的完整 URL
- 该页面的 Turnstile sitekey(下一步抓取)
计费按并发线程算,不按次数:BASIC $15/月、5 线程,STANDARD $30/月、15 线程,ADVANCE $90/月、50 线程,套餐内识别次数不限。
第 1 步:提取 Turnstile 站点密钥
sitekey 是公开标识,通常写在 HTML 里。几条正则即可覆盖常见写法:
import re
import requests
def extract_turnstile_sitekey(url):
"""Extract Cloudflare Turnstile sitekey from page HTML."""
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 Chrome/120.0.0.0",
"Accept": "text/html,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
}
response = requests.get(url, headers=headers, timeout=15)
patterns = [
r'data-sitekey=["\']([0-9x][A-Za-z0-9_-]+)["\']',
r"sitekey\s*:\s*['\"]([0-9x][A-Za-z0-9_-]+)['\"]",
r"siteKey\s*[=:]\s*['\"]([0-9x][A-Za-z0-9_-]+)['\"]",
]
for pattern in patterns:
match = re.search(pattern, response.text)
if match:
return match.group(1)
return None
sitekey = extract_turnstile_sitekey("https://example.com/signup")
print(f"Sitekey: {sitekey}")
请求头要完整,否则可能在拿到 HTML 前就收到 403。三条正则都没命中,多半是组件由 JavaScript 动态渲染。
第 2 步:把任务提交给 CaptchaAI
用 method=turnstile 提交到 in.php,返回 request 即任务 ID:
import requests
API_KEY = "YOUR_API_KEY"
def submit_turnstile(sitekey, page_url):
"""Submit Turnstile solving task to CaptchaAI."""
response = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "turnstile",
"sitekey": sitekey,
"pageurl": page_url,
"json": 1,
})
data = response.json()
if data.get("status") != 1:
raise Exception(f"Submit failed: {data.get('request')}")
return data["request"]
task_id = submit_turnstile("0x4AAAAAAAC3DHQhMMQ_Rxrg", "https://example.com/signup")
print(f"Task ID: {task_id}")
json=1 让接口返回 JSON。提交失败别吞错误:ERROR_WRONG_USER_KEY、ERROR_ZERO_BALANCE 直接指明是密钥还是余额问题。
第 3 步:轮询获取 token
任务异步执行,每 5 秒查一次 res.php:
import time
def poll_result(task_id, timeout=120):
"""Poll CaptchaAI for the solved Turnstile token."""
start = time.time()
while time.time() - start < timeout:
time.sleep(5)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY,
"action": "get",
"id": task_id,
"json": 1,
}).json()
if result.get("status") == 1:
return result["request"]
if result.get("request") == "ERROR_CAPTCHA_UNSOLVABLE":
raise Exception("Turnstile could not be solved")
raise TimeoutError("Solve timed out")
token = poll_result(task_id)
print(f"Token: {token[:50]}...")
轮询间隔不要压到 1 秒。遇到 ERROR_CAPTCHA_UNSOLVABLE 应立刻抛出,该错误码表示任务已终结。
完整可运行脚本
三步串起来加表单提交即最小闭环:
import re
import time
import requests
API_KEY = "YOUR_API_KEY"
TARGET_URL = "https://example.com/signup"
def solve_turnstile(sitekey, page_url):
"""Full Turnstile solve: submit + poll."""
# Submit
submit = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "turnstile",
"sitekey": sitekey,
"pageurl": page_url,
"json": 1,
})
data = submit.json()
if data.get("status") != 1:
raise Exception(f"Submit error: {data.get('request')}")
task_id = data["request"]
print(f"Task submitted: {task_id}")
# Poll
for _ in range(30):
time.sleep(5)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY,
"action": "get",
"id": task_id,
"json": 1,
}).json()
if result.get("status") == 1:
return result["request"]
raise TimeoutError("Solve timed out")
# --- Main flow ---
session = requests.Session()
session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 Chrome/120.0.0.0",
"Accept": "text/html,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
})
# 1. Get page and extract sitekey
response = session.get(TARGET_URL, timeout=15)
match = re.search(r'data-sitekey=["\']([0-9x][A-Za-z0-9_-]+)["\']', response.text)
if not match:
raise ValueError("Turnstile sitekey not found")
sitekey = match.group(1)
print(f"Sitekey: {sitekey}")
# 2. Solve Turnstile
token = solve_turnstile(sitekey, TARGET_URL)
print(f"Token: {token[:50]}...")
# 3. Submit form with token
form_response = session.post(TARGET_URL, data={
"cf-turnstile-response": token,
"email": "[email protected]",
"password": "SecurePass123",
})
print(f"Form status: {form_response.status_code}")
字段名 cf-turnstile-response 固定。全程复用同一个 requests.Session(),让 cookie 保持一致。
需要 action 参数的情况
部分站点会在服务端校验 action,值写在 data-action 里。不带上表单照样被拒:
def solve_turnstile_with_action(sitekey, page_url, action):
"""Solve Turnstile that requires an action parameter."""
submit = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "turnstile",
"sitekey": sitekey,
"pageurl": page_url,
"action": action, # Include the action from data-action attribute
"json": 1,
})
data = submit.json()
if data.get("status") != 1:
raise Exception(f"Submit error: {data.get('request')}")
task_id = data["request"]
for _ in range(30):
time.sleep(5)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY,
"action": "get",
"id": task_id,
"json": 1,
}).json()
if result.get("status") == 1:
return result["request"]
raise TimeoutError("Solve timed out")
抓页面时看一眼容器有没有 data-action,有就带上。
token 的三种提交方式
用法取决于表单形态,下面三种覆盖大部分场景。
方式 1:表单 POST,字段名 cf-turnstile-response
# Most common — Turnstile uses cf-turnstile-response field
response = session.post(form_url, data={
"cf-turnstile-response": token,
"email": "[email protected]",
})
方式 2:JSON 接口
response = session.post(api_url, json={
"turnstileToken": token,
"email": "[email protected]",
})
方式 3:自定义字段名
# Some sites rename the field — check the form HTML
response = session.post(form_url, data={
"cf-turnstile-response": token,
"captcha_token": token, # Custom duplicate field
"action": "signup",
})
第三种最易踩坑,照抄 DevTools 里的真实请求体最可靠。
生产环境封装:带重试的求解类
跑通后建议封装成类,把可重试与不可重试错误分开:
import re
import time
import requests
class TurnstileSolver:
"""Production-ready Turnstile solver with retry logic."""
API_URL = "https://ocr.captchaai.com"
def __init__(self, api_key, max_retries=3):
self.api_key = api_key
self.max_retries = max_retries
def extract_sitekey(self, session, url):
"""Extract Turnstile sitekey from page."""
response = session.get(url, timeout=15)
match = re.search(
r'data-sitekey=["\']([0-9x][A-Za-z0-9_-]+)["\']', response.text
)
return match.group(1) if match else None
def solve(self, sitekey, page_url, action=None):
"""Solve Turnstile with retry logic. Returns token string."""
for attempt in range(1, self.max_retries + 1):
try:
token = self._solve_once(sitekey, page_url, action)
return token
except TimeoutError:
print(f"Attempt {attempt} timed out")
except Exception as e:
error_str = str(e)
if "ERROR_ZERO_BALANCE" in error_str:
raise # Don't retry billing errors
if "ERROR_WRONG_USER_KEY" in error_str:
raise
print(f"Attempt {attempt} failed: {e}")
raise Exception(f"Failed after {self.max_retries} attempts")
def _solve_once(self, sitekey, page_url, action=None):
"""Single solve attempt."""
params = {
"key": self.api_key,
"method": "turnstile",
"sitekey": sitekey,
"pageurl": page_url,
"json": 1,
}
if action:
params["action"] = action
submit = requests.post(f"{self.API_URL}/in.php", data=params, timeout=30)
submit.raise_for_status()
data = submit.json()
if data.get("status") != 1:
raise Exception(f"Submit error: {data.get('request')}")
task_id = data["request"]
for _ in range(30):
time.sleep(5)
result = requests.get(f"{self.API_URL}/res.php", params={
"key": self.api_key,
"action": "get",
"id": task_id,
"json": 1,
}, timeout=30).json()
if result.get("status") == 1:
return result["request"]
if result.get("request") == "ERROR_CAPTCHA_UNSOLVABLE":
raise Exception("CAPTCHA unsolvable")
raise TimeoutError("Poll timed out")
# Usage
solver = TurnstileSolver("YOUR_API_KEY")
token = solver.solve("0x4AAAAAAAC3DHQhMMQ_Rxrg", "https://example.com/signup")
核心是错误分类:余额不足与密钥错误属配置问题,应立即抛出;只有超时和临时失败值得重试。
常见故障对照表
| 症状 | 原因 | 处理方式 |
|---|---|---|
| token 拿到但表单被拒 | sitekey 不对或漏了 action | 重新抓 sitekey,有 data-action 时一并提交 |
| 找不到 sitekey | 组件由 JavaScript 渲染 | 改用可执行脚本的方案 |
| 取页面就返回 403 | 请求头不完整 | 补齐 User-Agent 等头部 |
| 识别超过 60 秒 | 队列拥堵 | 高峰时段属正常,可延长超时 |
| token 用一次即失效 | 站点要求每次用新 token | 每次提交前重新识别 |
常见问题
用 requests 就够了,还需要上浏览器吗?
只要 sitekey 能在原始 HTML 里匹配到就够了,资源占用远低于浏览器方案。只有容器完全由 JavaScript 生成时才需要换方案。
提交任务后大概多久能拿到 token?
按 CaptchaAI 公布的指标,Turnstile 识别耗时在 10 秒以内。轮询间隔 5 秒、总超时 120 秒较稳妥,高峰排队变长属正常。
Turnstile 的托管、非交互、隐形三种模式要分别处理吗?
不需要。三种模式参数一致,都是 method=turnstile 加 sitekey 与 pageurl,差异由服务端处理。
除了 Turnstile 还支持哪些类型?
不支持 hCaptcha 与 FunCaptcha。目前覆盖 reCAPTCHA v2/v3 系列、Cloudflare Turnstile 与 Challenge、GeeTest v3、图片/九宫格与 BLS,另有 CaptchaFox(测试版)等三种。GeeTest v4 为即将支持。
国内环境跑这套脚本有什么要注意的?
识别请求走 CaptchaAI 接口,不像 reCAPTCHA 那样依赖 Google 域名。更该留意的是采集范围:只对自有或已授权的站点做自动化,遵循 robots 协议与《网络安全法》《数据安全法》的要求。
小结
路径很短:抓 sitekey → 提交到 CaptchaAI → 轮询 res.php 拿 token → 以 cf-turnstile-response 回填表单。页面带 data-action 就补上 action,超时阈值在自有环境实测后再定。