从调用识别接口的角度看,标准版 v3 和 Enterprise v3 的区别只有一个参数:enterprise=1。提交方式、轮询节奏、拿到的 token 格式完全一样。
麻烦在前一步:判断页面用的是哪一个。两个版本都不渲染可见控件,参数写反的后果却很直接——接口正常返回 token,提交却被后端拒绝,日志上表现为“识别成功但登录失败”。
第一步:判断页面用的是标准版还是 Enterprise
判据只能从页面加载的 JavaScript 里找,可靠信号有两个:脚本文件是 api.js 还是 enterprise.js,执行函数是 grecaptcha.execute() 还是 grecaptcha.enterprise.execute()。
Python:抓页面源码判断
import requests
import re
def detect_v3_type(url):
resp = requests.get(url)
html = resp.text
# Check for enterprise.js
if "enterprise.js" in html:
version = "enterprise_v3"
execute_fn = "grecaptcha.enterprise.execute"
elif "recaptcha/api.js" in html and "render=" in html:
version = "standard_v3"
execute_fn = "grecaptcha.execute"
else:
return None
# Extract sitekey from render parameter
key_match = re.search(r'render[=:]\s*["\']?([A-Za-z0-9_-]{40})', html)
sitekey = key_match.group(1) if key_match else None
# Extract action parameter
action_match = re.search(r'action["\']?\s*[:=]\s*["\'](\w+)', html)
action = action_match.group(1) if action_match else "unknown"
return {
"version": version,
"sitekey": sitekey,
"action": action,
"execute_fn": execute_fn
}
info = detect_v3_type("https://staging.example.com/qa-login")
print(info)
Node.js:同一套判据
const axios = require("axios");
async function detectV3Type(url) {
const { data: html } = await axios.get(url);
let version, executeFn;
if (html.includes("enterprise.js")) {
version = "enterprise_v3";
executeFn = "grecaptcha.enterprise.execute";
} else if (html.includes("recaptcha/api.js") && html.includes("render=")) {
version = "standard_v3";
executeFn = "grecaptcha.execute";
} else {
return null;
}
const keyMatch = html.match(/render[=:]\s*['"]?([A-Za-z0-9_-]{40})/);
const actionMatch = html.match(/action['"]?\s*[:=]\s*['"](\w+)/);
return {
version,
sitekey: keyMatch?.[1] || null,
action: actionMatch?.[1] || "unknown",
executeFn,
};
}
排查时的控制台快检
// Paste in DevTools console
if (document.querySelector('script[src*="enterprise.js"]')) {
console.log("Enterprise v3");
console.log("Execute:", typeof grecaptcha?.enterprise?.execute);
} else if (document.querySelector('script[src*="api.js"][src*="render="]')) {
console.log("Standard v3");
console.log("Execute:", typeof grecaptcha?.execute);
}
检测要放在每次加载页面后执行,不要把版本写死在配置里——站点迁到 Enterprise 不会通知集成方。
两个版本的差异对照
| 对比项 | 标准 v3 | Enterprise v3 |
|---|---|---|
| 是否显示控件 | 无,隐形运行 | 无,隐形运行 |
| 分数(0.0–1.0) | 有 | 有 |
| action 参数 | 必填 | 必填 |
| 脚本文件 | api.js?render=KEY |
enterprise.js?render=KEY |
| 执行函数 | grecaptcha.execute() |
grecaptcha.enterprise.execute() |
| 原因码 | 无 | 有(AUTOMATION、TOO_MUCH_TRAFFIC 等) |
| 按 action 配置阈值 | 不支持 | 支持,在 Cloud Console 配置 |
| 密码泄露检测 | 无 | 有 |
| Account Defender | 无 | 有 |
| 站点侧校验接口 | siteverify,免费 |
recaptchaenterprise.googleapis.com,按量付费 |
| CaptchaAI 提交参数 | version=v3 |
version=v3 加 enterprise=1 |
| 典型识别耗时 | 10–20 秒 | 10–20 秒 |
原因码、自定义阈值属于站点运营方,集成方看不到配置,它们只影响后端对同一个 token 判定的宽严。
用 CaptchaAI 识别:两段代码只差一行
标准 v3
import requests
import time
# Submit
resp = requests.get("https://ocr.captchaai.com/in.php", params={
"key": "YOUR_API_KEY",
"method": "userrecaptcha",
"version": "v3",
"googlekey": sitekey,
"action": "login",
"pageurl": page_url
})
task_id = resp.text.split("|")[1]
# Poll
for _ in range(60):
time.sleep(5)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": "YOUR_API_KEY", "action": "get", "id": task_id
})
if result.text.startswith("OK|"):
token = result.text.split("|")[1]
break
Enterprise v3:多一个 enterprise 参数
提交时加上 enterprise: 1,轮询部分一个字不改:
import requests
import time
# Submit — add enterprise=1
resp = requests.get("https://ocr.captchaai.com/in.php", params={
"key": "YOUR_API_KEY",
"method": "userrecaptcha",
"version": "v3",
"enterprise": 1, # Required for Enterprise
"googlekey": sitekey,
"action": "login",
"pageurl": page_url
})
task_id = resp.text.split("|")[1]
# Polling is identical to standard
for _ in range(60):
time.sleep(5)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": "YOUR_API_KEY", "action": "get", "id": task_id
})
if result.text.startswith("OK|"):
token = result.text.split("|")[1]
break
自动判断版本的通用封装
生产脚本更实用的写法是检测和提交合成一个方法:页面只拉一次,版本、sitekey、action 一起取出。
import requests
import time
import re
class RecaptchaV3Solver:
def __init__(self, api_key):
self.api_key = api_key
def detect_and_solve(self, page_url, action=None):
"""Auto-detect standard vs enterprise and solve."""
html = requests.get(page_url).text
is_enterprise = "enterprise.js" in html
key_match = re.search(r'render[=:]\s*["\']?([A-Za-z0-9_-]{40})', html)
if not key_match:
raise Exception("No v3 sitekey found")
sitekey = key_match.group(1)
if not action:
action_match = re.search(r'action["\']?\s*[:=]\s*["\'](\w+)', html)
action = action_match.group(1) if action_match else "verify"
params = {
"key": self.api_key,
"method": "userrecaptcha",
"version": "v3",
"googlekey": sitekey,
"action": action,
"pageurl": page_url
}
if is_enterprise:
params["enterprise"] = 1
resp = requests.get("https://ocr.captchaai.com/in.php", params=params)
if not resp.text.startswith("OK|"):
raise Exception(f"Submit failed: {resp.text}")
task_id = resp.text.split("|")[1]
for _ in range(60):
time.sleep(5)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": self.api_key, "action": "get", "id": task_id
})
if result.text.startswith("OK|"):
return result.text.split("|")[1]
if result.text != "CAPCHA_NOT_READY":
raise Exception(f"Solve failed: {result.text}")
raise Exception("Timed out")
solver = RecaptchaV3Solver("YOUR_API_KEY")
token = solver.detect_and_solve("https://staging.example.com/qa-login", action="login")
print(f"Token: {token[:40]}...")
action 参数:分数高低的隐形开关
action 是 v3 独有的上下文标记,两个版本都必填。站点给不同动作设不同期望值,登录页发成 homepage,分数就会偏低、token 被拒。正确做法是从页面脚本里抓真实取值:
import re
def find_v3_actions(html):
"""Extract all action parameters from page JavaScript."""
# Look for grecaptcha.execute(key, {action: '...'})
pattern = r"(?:grecaptcha\.(?:enterprise\.)?execute|action)\s*[(:]\s*['\"](\w+)"
actions = re.findall(pattern, html)
return list(set(actions))
# Common actions: "login", "submit", "register", "checkout", "homepage"
抓不到时的排查顺序:
- 在页面脚本里搜
execute(,看括号内的第二个参数; - 再按
login、submit、checkout逐个试; - 每次都看后端返回,而不是只看拿没拿到 token。
把 token 交回页面
两个版本一致:写进 g-recaptcha-response 字段,或直接放进 POST 请求体。
# For browser-based workflows (Selenium)
driver.execute_script(
f'document.getElementById("g-recaptcha-response").value = "{token}";'
)
# For pure HTTP workflows
requests.post(page_url, data={
"g-recaptcha-response": token,
"username": "user",
"password": "pass"
})
// Puppeteer
await page.evaluate((tok) => {
document.getElementById("g-recaptcha-response").value = tok;
}, token);
// Pure HTTP (axios)
await axios.post(pageUrl, new URLSearchParams({
"g-recaptcha-response": token,
username: "user",
password: "pass",
}));
出海业务里的实际情况
国内站点很少用 reCAPTCHA v3,主流是 GeeTest(极验)、网易易盾、腾讯防水墙;v3 更多出现在出海电商、跨境 SaaS 和海外合作方页面上。由此带来一个常见误判:reCAPTCHA 脚本由 Google 域名分发,境内网络下不一定能稳定加载,本地调试时页面一直转圈,往往是脚本没下载下来,和识别服务无关。这类回归建议放到境外服务器跑。
自动化只应针对自有或已授权的环境。网络安全法、数据安全法、PIPL 和 robots 协议是绕不开的边界。
最常见的五个参数错误
| 现象 | 直接原因 | 处理方式 |
|---|---|---|
标准 v3 上带了 enterprise=1 |
token 可能不被站点接受 | 加参数前先确认页面里有 enterprise.js |
Enterprise v3 上漏掉 enterprise=1 |
token 被后端拒绝 | 检测到 enterprise.js 就必须带上 |
action 取值不对 |
分数偏低,token 被拒 | 从页面 JavaScript 里提取真实字符串 |
漏写 version=v3 |
任务被当成 v2 处理 | 基于分数的 reCAPTCHA 一律带 version=v3 |
| 用 v2 的 sitekey 提交 v3 | 返回 ERROR_WRONG_GOOGLEKEY |
v3 的 sitekey 来自 render=KEY 参数 |
常见问题
分数很低,是识别服务不行吗?
不一定。v3 的分数由 Google 结合站点自身数据给出,action 是否匹配、请求来源、账号状态都会影响。先确认 action 与页面脚本一致,再看对方阈值是否偏严。
识别 Enterprise v3 更耗资源吗?
不会。两者提交方式、轮询逻辑和典型耗时(10–20 秒)一样,占用同一个线程。CaptchaAI 按并发线程计费而非按次计费:BASIC $15/月 5 线程、STANDARD $30/月 15 线程、ADVANCE $90/月 50 线程,套餐内识别次数不限。
在 Selenium 脚本里怎么判断当前页面是 Enterprise?
在已加载的页面源码里搜 enterprise.js 即可:
page_source = driver.page_source
is_enterprise = "enterprise.js" in page_source
轮询多久算超时?
按 5 秒一次轮询,v3 通常 10–20 秒返回,上限设 2–3 分钟比较稳妥,超时就重新提交。返回值既不是 CAPCHA_NOT_READY 也不是 OK|,说明是错误码,应立刻中断记录。