不可见的 reCAPTCHA 删除了该复选框,但添加了新的自动化故障模式。最大的问题是根本没有检测到不可见的小部件,缺少回调函数,提交过期的令牌,以及需要不可见参数时使用v2标准参数。
本指南涵盖了每种常见错误模式以及准确的修复方法。如果您需要有关隐形 reCAPTCHA 工作原理的背景知识,请阅读reCAPTCHA Invisible 的工作原理以及解决方法第一的。
快速错误参考
| 错误 | 原因 | 处理方式 |
|---|---|---|
ERROR_WRONG_GOOGLEKEY |
站点密钥错误或来自不同域的站点密钥 | 从不可见的小部件 div 或 grecaptcha.render() 调用中提取 sitekey |
ERROR_PAGEURL |
URL 不匹配 — 发送父页面 URL 而不是 iframe URL | 使用加载不可见小部件的确切 URL |
ERROR_CAPTCHA_UNSOLVABLE |
谷歌将该任务标记为不可能完成 | 使用新的代理和 cookies 重试;检查站点是否切换到 v3 |
ERROR_BAD_TOKEN_OR_PAGEURL |
令牌被目标站点拒绝 | 验证pageurl完全匹配;通过回调注入,而不是隐藏字段 |
CAPCHA_NOT_READY |
任务仍在处理中 | 保持每 5 秒轮询一次;隐形解决需要 10-30 秒 |
ERROR_KEY_DOES_NOT_EXIST |
CaptchaAI API 密钥无效 | 检查钥匙位于captchaai.com/account |
| 令牌已接受,但表单失败 | token注入后回调未执行 | 查找并使用令牌调用 data-callback 函数 |
错误1:未检测到页面上不可见的reCAPTCHA
不可见的 reCAPTCHA 没有可见的复选框。如果您的抓取工具没有检测到它,受验证码保护的请求会因表单提交错误或重定向而默默失败。
如何检测隐形 reCAPTCHA
在 HTML 页面中查找这些模式:
<!-- Pattern 1: div with data-size="invisible" -->
<div class="g-recaptcha" data-sitekey="6LdKlZEU..."
data-size="invisible"
data-callback="onSubmit"></div>
<!-- Pattern 2: button with data-sitekey and invisible size -->
<button class="g-recaptcha"
data-sitekey="6LdKlZEU..."
data-callback="onSubmit"
data-action="submit">Submit</button>
<!-- Pattern 3: programmatic render with size: invisible -->
<script>
grecaptcha.render('submit-btn', {
sitekey: '6LdKlZEU...',
callback: onSubmit,
size: 'invisible'
});
</script>
检测脚本(Python):
import requests
from bs4 import BeautifulSoup
import re
def detect_invisible_recaptcha(url):
resp = requests.get(url)
soup = BeautifulSoup(resp.text, "html.parser")
# Check for data-size="invisible"
widget = soup.find("div", {"data-size": "invisible", "class": "g-recaptcha"})
if widget:
return {
"type": "invisible",
"sitekey": widget.get("data-sitekey"),
"callback": widget.get("data-callback")
}
# Check for programmatic render with invisible
scripts = soup.find_all("script")
for script in scripts:
if script.string and "size" in str(script.string) and "invisible" in str(script.string):
key_match = re.search(r"sitekey['\"]?\s*[:=]\s*['\"]([^'\"]+)", script.string)
if key_match:
return {
"type": "invisible-programmatic",
"sitekey": key_match.group(1),
"callback": "check grecaptcha.render() call"
}
return None
检测脚本(Node.js):
const axios = require("axios");
const cheerio = require("cheerio");
async function detectInvisibleRecaptcha(url) {
const { data } = await axios.get(url);
const $ = cheerio.load(data);
// Check for data-size="invisible"
const widget = $(".g-recaptcha[data-size='invisible']");
if (widget.length) {
return {
type: "invisible",
sitekey: widget.attr("data-sitekey"),
callback: widget.attr("data-callback"),
};
}
// Check script tags for programmatic invisible render
const scriptContent = $("script")
.map((_, el) => $(el).html())
.get()
.join("\n");
if (scriptContent.includes("invisible")) {
const keyMatch = scriptContent.match(/sitekey['"]?\s*[:=]\s*['"]([^'"]+)/);
if (keyMatch) {
return {
type: "invisible-programmatic",
sitekey: keyMatch[1],
callback: "check grecaptcha.render() call",
};
}
}
return null;
}
错误 2:站点密钥错误 — ERROR_WRONG_GOOGLEKEY
当您发送的站点密钥与目标页面上不可见的 reCAPTCHA 小部件不匹配时,就会发生这种情况。常见原因:
- 从不同页面上的v2复选框复制了站点密钥
- 使用不同 reCAPTCHA 版本的 锚点 URL 中的站点密钥
- 页面有多个 reCAPTCHA 小部件,而您抓错了一个
修复:提取正确的不可见站点密钥
import requests
from bs4 import BeautifulSoup
def get_invisible_sitekey(url):
resp = requests.get(url)
soup = BeautifulSoup(resp.text, "html.parser")
# Priority 1: invisible widget
widget = soup.find(attrs={"data-size": "invisible", "class": "g-recaptcha"})
if widget:
return widget["data-sitekey"]
# Priority 2: any g-recaptcha div (may be invisible without data-size)
widget = soup.find(class_="g-recaptcha")
if widget and widget.get("data-sitekey"):
return widget["data-sitekey"]
return None
sitekey = get_invisible_sitekey("https://staging.example.com/qa-login")
print(f"Sitekey: {sitekey}")
错误 3:回调未执行 — 表单已提交但没有任何反应
这是开发人员错过的第一个不可见的 reCAPTCHA 失败。与 v2 复选框将token 提交 g-recaptcha-response 就足够了不同,不可见的 reCAPTCHA 几乎总是使用 JavaScript 回调函数。如果您注入令牌但不调用回调,则表单永远不会处理。
回调流程如何工作
grecaptcha.execute()发起隐形挑战- 解决后Google调用
data-callback中指定的函数 - 该回调函数提交表单或进行 API 调用
修复:查找并执行回调
第 1 步 — 识别回调名称:
# From HTML: data-callback="onSubmit"
# From JS: callback: onSubmit
# From grecaptcha.render: second argument with callback property
步骤 2 — 注入代币并调用回调 (Selenium):
from selenium import webdriver
import requests
import time
driver = webdriver.Chrome()
driver.get("https://example.com/form")
# Get sitekey
sitekey = driver.find_element("css selector", ".g-recaptcha").get_attribute("data-sitekey")
callback_name = driver.find_element("css selector", ".g-recaptcha").get_attribute("data-callback")
# Solve with CaptchaAI
task_id = requests.get("https://ocr.captchaai.com/in.php", params={
"key": "YOUR_API_KEY",
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": driver.current_url,
"invisible": 1
}).text.split("|")[1]
# Poll for result
token = None
for _ in range(60):
time.sleep(5)
resp = requests.get("https://ocr.captchaai.com/res.php", params={
"key": "YOUR_API_KEY",
"action": "get",
"id": task_id
}).text
if resp.startswith("OK|"):
token = resp.split("|")[1]
break
# Inject token into the response field
driver.execute_script(
f'document.getElementById("g-recaptcha-response").value = "{token}";'
)
# CRITICAL: Call the callback function
driver.execute_script(f'{callback_name}("{token}");')
第 2 步 - 注入令牌并调用回调(Puppeteer):
const puppeteer = require("puppeteer");
const axios = require("axios");
(async () => {
const browser = await puppeteer.launch({ headless: "new" });
const page = await browser.newPage();
await page.goto("https://example.com/form");
// Get sitekey and callback
const { sitekey, callback } = await page.evaluate(() => {
const el = document.querySelector(".g-recaptcha[data-size='invisible']");
return {
sitekey: el?.getAttribute("data-sitekey"),
callback: el?.getAttribute("data-callback"),
};
});
// Submit to CaptchaAI
const submitResp = await axios.get("https://ocr.captchaai.com/in.php", {
params: {
key: "YOUR_API_KEY",
method: "userrecaptcha",
googlekey: sitekey,
pageurl: page.url(),
invisible: 1,
},
});
const taskId = submitResp.data.split("|")[1];
// Poll for result
let token;
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 5000));
const result = await axios.get("https://ocr.captchaai.com/res.php", {
params: { key: "YOUR_API_KEY", action: "get", id: taskId },
});
if (result.data.startsWith("OK|")) {
token = result.data.split("|")[1];
break;
}
}
// Inject token and fire callback
await page.evaluate(
(tok, cb) => {
document.getElementById("g-recaptcha-response").value = tok;
if (cb && typeof window[cb] === "function") {
window[cb](tok);
}
},
token,
callback,
);
await browser.close();
})();
错误4:缺少invisible=1参数
通过 CaptchaAI 解决不可见的 reCAPTCHA 时,您必须在请求中包含 invisible=1。如果没有它,求解器会将任务视为标准 v2 复选框挑战,这可能会导致 ERROR_CAPTCHA_UNSOLVABLE 或目标站点拒绝的令牌。
错误与正确的请求
# WRONG — missing invisible=1
params = {
"key": "YOUR_API_KEY",
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url
}
# CORRECT — includes invisible=1
params = {
"key": "YOUR_API_KEY",
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url,
"invisible": 1 # Required for invisible reCAPTCHA
}
response = requests.get("https://ocr.captchaai.com/in.php", params=params)
错误5:提交前令牌已过期
不可见的 reCAPTCHA 令牌将在 120 秒后过期 - 与 v2 标准相同。但隐形工作流程通常在解决和提交之间有额外的处理步骤,从而更有可能过期。
症状
- token 提交后表单返回一般错误
- 服务器端
siteverify返回timeout-or-duplicate - 令牌有效,但到达提交步骤所需的时间太长
修复:及时解决
仅当您准备好立即提交时才请求解决:
import requests
import time
def solve_invisible_recaptcha(api_key, sitekey, page_url):
# Submit task
resp = requests.get("https://ocr.captchaai.com/in.php", params={
"key": api_key,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url,
"invisible": 1
})
if not resp.text.startswith("OK|"):
raise Exception(f"Submit failed: {resp.text}")
task_id = resp.text.split("|")[1]
# Poll for result
for _ in range(60):
time.sleep(5)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": 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("Solve timed out after 5 minutes")
# Usage: solve JUST before you need to submit
# 1. Navigate to page and prepare form data first
# 2. THEN solve the captcha
# 3. Inject token and submit immediately
token = solve_invisible_recaptcha("YOUR_API_KEY", sitekey, page_url)
# Submit within 120 seconds of receiving the token
错误 6:令牌被拒绝 — ERROR_BAD_TOKEN_OR_PAGEURL
目标网站向 Google 验证了令牌,但失败。常见原因:
| 原因 | 如何识别 | 处理方式 |
|---|---|---|
错误 pageurl |
URL 与 sitekey 注册中的域不匹配 | 使用小部件加载的确切 URL |
| 不同域使用的令牌 | 跨域令牌复用 | 使用正确域的 pageurl 解决 |
| 令牌已使用 | 提交同一个令牌两次 | 为每次提交请求一个新的解决方案 |
| IP 不匹配 | 您的 IP 与求解器的 IP 不同 | 添加您的 proxy 参数以匹配会话 IP |
| 隐形旗帜不见了 | 作为v2标准解决,用于不可见页面 | 将 invisible=1 添加到解决请求中 |
调试清单
def debug_invisible_solve(api_key, sitekey, page_url, proxy=None):
"""Run a diagnostic solve with detailed logging."""
print(f"Sitekey: {sitekey}")
print(f"Page URL: {page_url}")
print(f"Proxy: {proxy or 'none'}")
params = {
"key": api_key,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url,
"invisible": 1
}
if proxy:
params["proxy"] = proxy
params["proxytype"] = "HTTP"
# Submit
resp = requests.get("https://ocr.captchaai.com/in.php", params=params)
print(f"Submit response: {resp.text}")
if not resp.text.startswith("OK|"):
return None
task_id = resp.text.split("|")[1]
print(f"Task ID: {task_id}")
# Poll with timing
start = time.time()
for _ in range(60):
time.sleep(5)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": api_key, "action": "get", "id": task_id
})
elapsed = time.time() - start
print(f" [{elapsed:.0f}s] {result.text[:50]}")
if result.text.startswith("OK|"):
token = result.text.split("|")[1]
print(f"Token received after {elapsed:.0f}s")
print(f"Token length: {len(token)} characters")
print(f"Token starts with: {token[:30]}...")
return token
if result.text != "CAPCHA_NOT_READY":
print(f"FAILED: {result.text}")
return None
print("TIMEOUT after 5 minutes")
return None
错误 7:一页上有多个 reCAPTCHA 小部件
某些页面同时具有可见的 v2 复选框和不可见的 reCAPTCHA。如果您解决了错误的问题,则令牌有效,但与保护您所需操作的小部件不匹配。
修复:定位正确的小部件
from bs4 import BeautifulSoup
def find_all_recaptcha_widgets(html):
soup = BeautifulSoup(html, "html.parser")
widgets = []
for el in soup.find_all(class_="g-recaptcha"):
widgets.append({
"sitekey": el.get("data-sitekey"),
"size": el.get("data-size", "normal"),
"callback": el.get("data-callback"),
"tag": el.name,
"id": el.get("id")
})
return widgets
# Example output:
# [
# {"sitekey": "6LdA...", "size": "normal", "callback": None, "tag": "div", "id": "recaptcha-login"},
# {"sitekey": "6LdB...", "size": "invisible", "callback": "onRegister", "tag": "div", "id": "recaptcha-register"}
# ]
# Use the widget with size="invisible" for the invisible solve
具有错误处理功能的完整隐形 reCAPTCHA 求解器
这个生产就绪的包装器可以处理上述所有错误:
import requests
import time
import logging
logger = logging.getLogger(__name__)
class InvisibleRecaptchaSolver:
def __init__(self, api_key, max_retries=3):
self.api_key = api_key
self.max_retries = max_retries
self.base_url = "https://ocr.captchaai.com"
def solve(self, sitekey, page_url, proxy=None):
"""Solve invisible reCAPTCHA with automatic retry on transient errors."""
for attempt in range(1, self.max_retries + 1):
try:
token = self._attempt_solve(sitekey, page_url, proxy)
if token:
return token
except Exception as e:
logger.warning(f"Attempt {attempt} failed: {e}")
if attempt < self.max_retries:
time.sleep(2 ** attempt)
raise Exception(f"Failed to solve after {self.max_retries} attempts")
def _attempt_solve(self, sitekey, page_url, proxy):
params = {
"key": self.api_key,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url,
"invisible": 1
}
if proxy:
params["proxy"] = proxy
params["proxytype"] = "HTTP"
# Submit task
resp = requests.get(f"{self.base_url}/in.php", params=params)
if "ERROR" in resp.text:
error = resp.text.strip()
if error in ("ERROR_WRONG_GOOGLEKEY", "ERROR_KEY_DOES_NOT_EXIST"):
raise Exception(f"Configuration error (do not retry): {error}")
if error == "ERROR_ZERO_BALANCE":
raise Exception("Account balance is zero — add funds")
raise Exception(f"Submit error: {error}")
if not resp.text.startswith("OK|"):
raise Exception(f"Unexpected submit response: {resp.text}")
task_id = resp.text.split("|")[1]
# Poll for result
for _ in range(60):
time.sleep(5)
result = requests.get(f"{self.base_url}/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":
continue
if result.text == "ERROR_CAPTCHA_UNSOLVABLE":
logger.warning("Captcha unsolvable — will retry with new task")
return None
raise Exception(f"Poll error: {result.text}")
raise Exception("Solve timed out after 5 minutes")
# Usage
solver = InvisibleRecaptchaSolver("YOUR_API_KEY")
token = solver.solve(
sitekey="6LdKlZEU...",
page_url="https://staging.example.com/qa-login"
)
print(f"Token: {token[:50]}...")
const axios = require("axios");
class InvisibleRecaptchaSolver {
constructor(apiKey, maxRetries = 3) {
this.apiKey = apiKey;
this.maxRetries = maxRetries;
this.baseUrl = "https://ocr.captchaai.com";
}
async solve(sitekey, pageUrl, proxy) {
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
try {
const token = await this._attemptSolve(sitekey, pageUrl, proxy);
if (token) return token;
} catch (err) {
console.warn(`Attempt ${attempt} failed: ${err.message}`);
if (attempt < this.maxRetries) {
await new Promise((r) => setTimeout(r, 2 ** attempt * 1000));
}
}
}
throw new Error(`Failed to solve after ${this.maxRetries} attempts`);
}
async _attemptSolve(sitekey, pageUrl, proxy) {
const params = {
key: this.apiKey,
method: "userrecaptcha",
googlekey: sitekey,
pageurl: pageUrl,
invisible: 1,
};
if (proxy) {
params.proxy = proxy;
params.proxytype = "HTTP";
}
// Submit task
const submitResp = await axios.get(`${this.baseUrl}/in.php`, { params });
if (submitResp.data.includes("ERROR")) {
const error = submitResp.data.trim();
if (["ERROR_WRONG_GOOGLEKEY", "ERROR_KEY_DOES_NOT_EXIST"].includes(error)) {
throw new Error(`Configuration error (do not retry): ${error}`);
}
throw new Error(`Submit error: ${error}`);
}
const taskId = submitResp.data.split("|")[1];
// Poll for result
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 5000));
const result = await axios.get(`${this.baseUrl}/res.php`, {
params: { key: this.apiKey, action: "get", id: taskId },
});
if (result.data.startsWith("OK|")) {
return result.data.split("|")[1];
}
if (result.data === "CAPCHA_NOT_READY") continue;
if (result.data === "ERROR_CAPTCHA_UNSOLVABLE") return null;
throw new Error(`Poll error: ${result.data}`);
}
throw new Error("Solve timed out after 5 minutes");
}
}
// Usage
const solver = new InvisibleRecaptchaSolver("YOUR_API_KEY");
solver.solve("6LdKlZEU...", "https://staging.example.com/qa-login").then((token) => {
console.log(`Token: ${token.substring(0, 50)}...`);
});
故障排除清单
当隐形 reCAPTCHA 解决失败时,请运行此检查表:
| 步 | 查看 | 命令/Action |
|---|---|---|
| 1 | 确认它是不可见的,不是v2标准 | 在渲染调用中查找 data-size="invisible" 或 size: 'invisible' |
| 2 | 验证 sitekey 是否正确 | 具体与隐形小部件上的data-sitekey进行比较 |
| 3 | 确认 API 请求中的 invisible=1 |
检查您的 in.php 参数 |
| 4 | 检查 pageurl 是否完全匹配 |
使用浏览器 DevTools URL,而不是重定向 URL |
| 5 | 找到回调函数名 | 在 grecaptcha.render() 中查找 data-callback 属性或 callback |
| 6 | 验证token注入+回调调用 | 这两个步骤都是必需的——仅凭令牌是不够的 |
| 7 | 检查令牌新鲜度 | 令牌必须在 120 秒内使用 |
| 8 | 使用代理测试 IP 是否重要 | 添加proxy和proxytype参数 |
常问问题
隐形 reCAPTCHA 与解决问题的 v2 标准有何不同?
API 方法相同 (method=userrecaptcha),但您必须将 invisible=1 添加到您的请求中。关键区别在于注入方面:不可见的 reCAPTCHA 几乎总是需要在注入令牌后调用 JavaScript 回调函数,而 v2 标准通常仅适用于隐藏字段。
为什么我的代币在测试中有效但在生产中失败?
很可能是 IP 不匹配。在测试中,解算器和您的浏览器可能共享相似的 IP。在生产中,求解器的 IP 和服务器的 IP 不同。添加与您的会话 IP 匹配的代理参数来解决此问题。
隐形 reCAPTCHA 需要多长时间才能解决?
通过 CaptchaAI,典型的求解时间为 10-30 秒。隐形挑战通常比 v2 复选框挑战更快,因为它们不需要图像识别 - 它们依赖于风险分析。
我可以在没有浏览器的情况下解决隐形 reCAPTCHA 问题吗?
是的。由于解决是通过 API 在服务器端进行的,因此您只需要 sitekey 和 pageurl。仅当您必须在实际页面上执行回调函数时才需要浏览器。对于纯 API 工作流程,提取站点密钥,通过 CaptchaAI 进行解析,然后通过 HTTP 请求提交令牌。
后续步骤
- reCAPTCHA Invisible 的工作原理以及解决方法— 隐形机制的背景
- 如何使用API解决reCAPTCHA v2— 用于比较的标准 v2 求解
- 常见 reCAPTCHA v2 解决错误和修复— v2 标准错误模式
- CaptchaAI 错误代码:完整参考— 完整的错误代码表
相关文章
- 解决 Recaptcha 看不见的 Python
- 常见的网格图像验证码错误和修复
- Recaptcha V2 与 Invisible