Troubleshooting

在同一页面上处理 reCAPTCHA v2 和 v3

登录表单先跳出一个"我不是机器人"的复选框,后台又悄悄打了个分——这大概率是同一页面同时挂了 reCAPTCHA v2 和 v3。v3 无声运行,评分低于阈值时 v2 才弹出来。自动化脚本要应付两套验证码逻辑:不同站点密钥,不同解法。本文按检测、求解、边缘情况的顺序讲清楚具体怎么处理。


reCAPTCHA v2 + v3 组合的设计逻辑

User visits page
    ↓
reCAPTCHA v3 runs invisibly in background
    ↓
Score returned to server (e.g., 0.4)
    ↓
Score below threshold (e.g., < 0.7)?
    ├─ YES → Show reCAPTCHA v2 checkbox/image challenge
    └─ NO  → Allow action without visible CAPTCHA

这种模式想两头兼顾:

  • 多数正常用户(v3 得分高)全程看不到验证码 → 摩擦最小
  • 可疑流量(v3 得分低)才会看到 v2 挑战 → 作为安全兜底
  • 站点方自己决定"隐形通过"和"弹出挑战"之间的分数线

国内团队测试这类页面容易卡在网络层:reCAPTCHA 脚本和校验接口都在 Google 域名下,大陆直连不稳定是常态,别把网络问题误判成验证码没解出来。

特征 reCAPTCHA v3 reCAPTCHA v2
用户是否可见 否,后台打分 是,触发时才出现
站点密钥位置 ?render=KEYexecute('KEY', ...) 挂件上的 data-sitekey="KEY"
返回内容 0.0–1.0 的分数 通过/失败的挑战结果
提交字段名 g-recaptcha-response g-recaptcha-response

常见的双验证码实现方式

方式一:v3 预评估,v2 兜底(最常见组合)

<!-- Both scripts loaded -->
<script src="https://www.google.com/recaptcha/api.js?render=V3_SITE_KEY"></script>
<script src="https://www.google.com/recaptcha/api.js?render=explicit" async defer></script>

<form id="loginForm">
    <!-- v2 widget (hidden initially) -->
    <div id="recaptcha-v2-container" style="display:none;">
        <div class="g-recaptcha" data-sitekey="V2_SITE_KEY"></div>
    </div>
    <button type="submit">Login</button>
</form>

<script>
// First attempt: v3 invisible
grecaptcha.ready(function() {
    grecaptcha.execute('V3_SITE_KEY', {action: 'login'}).then(function(v3Token) {
        fetch('/api/verify-v3', {
            method: 'POST',
            body: JSON.stringify({token: v3Token})
        })
        .then(r => r.json())
        .then(data => {
            if (data.score < 0.7) {
                // Score too low → show v2 fallback
                document.getElementById('recaptcha-v2-container').style.display = 'block';
                grecaptcha.render('recaptcha-v2-container', {sitekey: 'V2_SITE_KEY'});
            } else {
                // Score OK → submit form directly
                document.getElementById('loginForm').submit();
            }
        });
    });
});
</script>

方式二:不同页面各用各的站点密钥

Homepage → v3 only (passive score)
Login page → v3 assessment, v2 fallback
Checkout → v2 always (high security)
Contact form → v3 only

方式三:一份脚本同时跑两种模式

<script src="https://www.google.com/recaptcha/api.js?render=V3_SITE_KEY"></script>
<script>
    // v3 execute
    grecaptcha.execute('V3_SITE_KEY', {action: 'login'});

    // v2 render (uses a different site key)
    grecaptcha.render('v2-container', {sitekey: 'V2_SITE_KEY'});
</script>

怎么判断页面是否同时用了 v2 和 v3

信号 在哪找
v3 存在 api.js?render=KEY(非 explicit)或 grecaptcha.execute() 调用
v2 存在 data-sitekey="KEY" 属性,或 grecaptcha.render() 调用
判定双验证码 两个信号同时出现,且两个 key 不同

用 Python 检测

import requests
import re

def detect_dual_recaptcha(url):
    """Detect if a page uses both reCAPTCHA v2 and v3."""
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                      "AppleWebKit/537.36 (KHTML, like Gecko) "
                      "Chrome/120.0.0.0 Safari/537.36",
    }
    html = requests.get(url, headers=headers, timeout=15).text

    result = {
        "has_v3": False,
        "has_v2": False,
        "v3_site_key": None,
        "v2_site_key": None,
        "dual": False,
        "pattern": None,
    }

    # Detect v3 (render parameter or enterprise.execute)
    v3_match = re.search(r"api\.js\?render=([A-Za-z0-9_-]+)", html)
    if v3_match and v3_match.group(1) != "explicit":
        result["has_v3"] = True
        result["v3_site_key"] = v3_match.group(1)

    # Detect v3 in execute calls
    v3_execute = re.search(
        r"grecaptcha\.(?:enterprise\.)?execute\s*\(\s*['\"]([^'\"]+)['\"]",
        html,
    )
    if v3_execute:
        result["has_v3"] = True
        if not result["v3_site_key"]:
            result["v3_site_key"] = v3_execute.group(1)

    # Detect v2 (g-recaptcha class or explicit render)
    v2_match = re.search(r'data-sitekey="([^"]+)"', html)
    if v2_match:
        key = v2_match.group(1)
        if key != result.get("v3_site_key"):
            result["has_v2"] = True
            result["v2_site_key"] = key

    # Check for explicit v2 render
    v2_render = re.search(
        r"grecaptcha\.render\s*\([^,]+,\s*\{[^}]*sitekey:\s*['\"]([^'\"]+)",
        html,
    )
    if v2_render:
        result["has_v2"] = True
        if not result["v2_site_key"]:
            result["v2_site_key"] = v2_render.group(1)

    result["dual"] = result["has_v3"] and result["has_v2"]

    if result["dual"]:
        # Determine pattern
        if "display:none" in html or "display: none" in html:
            result["pattern"] = "v3_pre_assessment_v2_fallback"
        else:
            result["pattern"] = "v2_v3_simultaneous"

    return result

detection = detect_dual_recaptcha("https://staging.example.com/qa-login")
print(detection)

用 Node.js 检测

const axios = require("axios");

async function detectDualRecaptcha(url) {
    const { data: html } = await axios.get(url, { timeout: 15000 });

    const result = {
        hasV3: false,
        hasV2: false,
        v3SiteKey: null,
        v2SiteKey: null,
        dual: false,
    };

    // v3 detection
    const v3Match = html.match(/api\.js\?render=([A-Za-z0-9_-]+)/);
    if (v3Match && v3Match[1] !== "explicit") {
        result.hasV3 = true;
        result.v3SiteKey = v3Match[1];
    }

    // v2 detection
    const v2Match = html.match(/data-sitekey="([^"]+)"/);
    if (v2Match && v2Match[1] !== result.v3SiteKey) {
        result.hasV2 = true;
        result.v2SiteKey = v2Match[1];
    }

    result.dual = result.hasV3 && result.hasV2;
    return result;
}

detectDualRecaptcha("https://staging.example.com/qa-login").then(console.log);

三种求解策略,怎么选

三种打法各有取舍,选哪种取决于你对目标页面触发规律的了解程度:

策略 适用场景 代价
先 v3 后 v2 不确定站点会不会兜底,通用性最好 多一次 v3 调用
直接解 v2 已确认自动化流量必触发 v2 省调用,但依赖前期观察
浏览器判断 实现复杂、脚本探测不可靠 依赖 Selenium,速度较慢

策略一:先解 v3,需要时再解 v2(最贴合站点判断流程)

import requests
import time

API_KEY = "YOUR_API_KEY"

def solve_v3(site_key, page_url, action="login"):
    """Solve reCAPTCHA v3 and return token."""
    submit = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": API_KEY,
        "method": "userrecaptcha",
        "googlekey": site_key,
        "pageurl": page_url,
        "version": "v3",
        "action": action,
        "json": 1,
    }).json()

    task_id = submit["request"]

    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, "json": 1,
        }).json()
        if result.get("status") == 1:
            return result["request"]

    raise TimeoutError("v3 solve timeout")

def solve_v2(site_key, page_url):
    """Solve reCAPTCHA v2 and return token."""
    submit = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": API_KEY,
        "method": "userrecaptcha",
        "googlekey": site_key,
        "pageurl": page_url,
        "json": 1,
    }).json()

    task_id = submit["request"]

    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, "json": 1,
        }).json()
        if result.get("status") == 1:
            return result["request"]

    raise TimeoutError("v2 solve timeout")

def solve_dual_recaptcha(v3_key, v2_key, page_url, action="login"):
    """Handle dual reCAPTCHA: try v3, fall back to v2."""
    # Step 1: Try v3
    v3_token = solve_v3(v3_key, page_url, action)

    # Step 2: Submit v3 token to target
    response = requests.post(f"{page_url}/verify", data={
        "g-recaptcha-response": v3_token,
    })

    # Step 3: Check if v2 fallback is needed
    if "recaptcha" in response.text.lower() and v2_key:
        print("v3 score too low — v2 fallback triggered")
        v2_token = solve_v2(v2_key, page_url)
        return {"version": "v2", "token": v2_token}

    return {"version": "v3", "token": v3_token}

result = solve_dual_recaptcha(
    v3_key="6LcExample_v3_key",
    v2_key="6LcExample_v2_key",
    page_url="https://staging.example.com/qa-login",
)
print(f"Solved with {result['version']}")

策略二:跳过 v3,直接解 v2

如果自动化流量已确认总是触发 v2,跳过没意义的 v3 调用,省时间和成本:

# If you consistently fail v3 assessment, just solve v2 directly
token = solve_v2(v2_site_key, page_url)
submit_form(token)

前提是用日志或抽样验证过"确实总是兜底",别凭猜测跳过——猜错了反而要多绕一圈。

策略三:实现复杂时,用浏览器直接读取当前状态

from selenium import webdriver
from selenium.webdriver.common.by import By
import time

driver = webdriver.Chrome()
driver.get("https://staging.example.com/qa-login")
time.sleep(3)

# Check if v2 widget is visible
v2_visible = driver.execute_script("""
    const container = document.querySelector('.g-recaptcha');
    if (!container) return false;
    const style = window.getComputedStyle(container.parentElement);
    return style.display !== 'none' && style.visibility !== 'hidden';
""")

if v2_visible:
    # v2 is showing — solve v2
    sitekey = driver.find_element(
        By.CSS_SELECTOR, "[data-sitekey]"
    ).get_attribute("data-sitekey")
    token = solve_v2(sitekey, driver.current_url)
    driver.execute_script(
        f'document.getElementById("g-recaptcha-response").value = "{token}";'
    )
else:
    # v3 only — solve v3
    # Extract v3 key from page source
    v3_key = driver.execute_script(
        "return document.querySelector('script[src*=\"render=\"]')"
        ".src.match(/render=([^&]+)/)[1];"
    )
    token = solve_v3(v3_key, driver.current_url)
    # Inject v3 token into the form
    driver.execute_script(f"""
        const input = document.createElement('input');
        input.type = 'hidden';
        input.name = 'g-recaptcha-response';
        input.value = '{token}';
        document.querySelector('form').appendChild(input);
    """)

driver.find_element(By.CSS_SELECTOR, "form").submit()

容易踩坑的边缘情况

页面上有两个不同的站点密钥

两个密钥千万别用混,version 和 key 对不上,拿到的 token 直接无效:

  • v3 密钥:出现在脚本 URL 的 ?render=KEY 里,也会出现在 grecaptcha.execute('KEY', ...) 调用中
  • v2 密钥:写在挂件 div 上的 data-sitekey="KEY"

带 v2 兜底的 reCAPTCHA Enterprise

  • recaptcha/enterprise.js 加载 → v3 Enterprise 参数
  • 普通 recaptcha/api.js 加载 → 标准 v3 参数
  • 兜底逻辑不变,仍走普通 v2:
# Detect and handle Enterprise + v2 combo
if "recaptcha/enterprise.js" in html:
    # Use enterprise parameter for v3
    v3_params = {"enterprise": 1, "version": "v3"}
else:
    v3_params = {"version": "v3"}

一个页面有多个表单

  • 别抓页面上第一个 data-sitekey 就用——很可能是别的表单
  • 先定位你要提交的那个 <form>,再从它内部取密钥
# Target the login form specifically
login_form = soup.find("form", id="login-form")
widget = login_form.find(attrs={"data-sitekey": True})
sitekey = widget["data-sitekey"]

常见问题

页面上的 v2 和 v3 是不是都要解决?

不一定。多数情况下 v3 评分过阈值,v2 挑战根本不会出现。只有触发了兜底逻辑,才需要再解一次 v2。

轮询 v3 结果时抛出 TimeoutError,是什么原因?

常见诱因:

  • 任务队列繁忙,或轮询间隔设得太紧(示例代码 5 秒一次、最多 60 次)
  • 线程数不够、并发大——换套餐(如 ADVANCE,$90/月、50 线程)能缓解

v3 的评分阈值一般设在多少?

站点自己设定,CaptchaAI 看不到也改不了。行业常见区间是 0.5~0.7,低于才弹出 v2;陌生站点建议先多跑几次观察规律。

怎么确认 v2 兜底有没有被触发?

看提交 v3 token 后的服务器响应:带着 v2 挂件 HTML,或触发跳转/AJAX 返回验证码内容,就是触发了。浏览器里则看 v2 容器的 CSS display 是不是从 none 变成可见。

国内网络环境测试这类页面,要注意什么?

先单独确认网络链路能否稳定拿到 reCAPTCHA 的 JS 和响应,再排查 v2/v3 的具体逻辑——两类问题混在一起排查会浪费大量时间。


总结

同页出现 v2 和 v3,本质上是 v3 做无感预评估、v2 做可见兜底的组合拳。

  • 判断双验证码:看 render 参数(v3)和 data-sitekey(v2)两个信号是否同时出现
  • 自动化打法:先用 CaptchaAI 解出 v3、提交 token,只有触发兜底时才补一次 v2
  • 关键提醒:两个版本的站点密钥不同,必须各自单独调用 API,别混用

相关文章

该文章已禁用评论。