Tutorials

单页应用程序中的 reCAPTCHA:动态加载模式

拉下 SPA 的页面源码,搜 sitekey 一个字符都搜不到——不是站点做了防护,是 reCAPTCHA 还没渲染。React 要等组件挂载,Vue 要等你点“提交订单”。四步固定:

  1. 触发渲染:点按钮、切路由或填前置字段;
  2. iframe[src*='recaptcha'] 出现,再读 sitekey;
  3. 把 sitekey 和路由 URL 交给 CaptchaAI,轮询取 token;
  4. 把 token 交回去:写字段,或调用回调。

SPA 和传统页面差在哪

传统页面 SPA
脚本写在初始 HTML 里 路由切换后才注入
页面加载即渲染 组件挂载才渲染
sitekey 在页面源码里 sitekey 在 JS 产物里

结论:别假设元素已经存在。

第一步:等组件真正出现

别用固定 sleep。渲染时机取决于接口返回,本地够用的 3 秒到 CI 里就不够:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    page = browser.new_page()
    page.goto("https://staging.example.com/qa-login")

    # SPA may need a click or navigation to trigger reCAPTCHA
    page.click("#show-login-form")

    # Wait for reCAPTCHA iframe to appear
    recaptcha_frame = page.wait_for_selector(
        "iframe[src*='recaptcha']",
        timeout=15000
    )
    print("reCAPTCHA detected:", recaptcha_frame.get_attribute("src"))

这一步就超时,多半是触发动作没做对,见排查表。

第二步:三个位置提取 sitekey

sitekey 有三处落点,按可靠性排序:

  • iframe 的 src?k= 后面那串,最稳;
  • data-sitekey 属性:标准容器都带;
  • JS 产物里的 grecaptcha.render:兜底。
# Method 1: From the iframe src
iframe_src = recaptcha_frame.get_attribute("src")
# src contains: ...?k=6LcR_RsTAAAAADge...
import re
match = re.search(r'[?&]k=([^&]+)', iframe_src)
site_key = match.group(1) if match else None

# Method 2: From data-sitekey attribute
site_key = page.eval_on_selector(
    "[data-sitekey]",
    "el => el.getAttribute('data-sitekey')"
)

# Method 3: From JavaScript bundle (last resort)
site_key = page.evaluate("""
    () => {
        // Check for grecaptcha render calls
        const scripts = document.querySelectorAll('script');
        for (const s of scripts) {
            const match = s.textContent.match(/sitekey['":\s]+(['"])(6L[^'"]+)\\1/);
            if (match) return match[2];
        }
        return null;
    }
""")

三条按顺序试,命中即返回;写死一条,改版就失败。

第三步:调用 CaptchaAI API 拿 token

methoduserrecaptchagooglekey 填 sitekey,pageurl 填当前路由地址,不是首页。

import requests
import time

def solve_recaptcha(site_key, page_url):
    # Submit task
    resp = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": "YOUR_API_KEY",
        "method": "userrecaptcha",
        "googlekey": site_key,
        "pageurl": page_url,
        "json": 1
    })
    task_id = resp.json()["request"]

    # Poll for result
    for _ in range(60):
        time.sleep(3)
        result = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": "YOUR_API_KEY",
            "action": "get",
            "id": task_id,
            "json": 1
        })
        data = result.json()
        if data["status"] == 1:
            return data["request"]
    raise TimeoutError("Solve timed out")

时间账: reCAPTCHA v2 识别耗时上限 <60 秒,token 有效期约 120 秒。别提前备 token,其他字段填完最后一步再识别。

第四步:把 token 交回 SPA

交到哪里取决于站点实现:

方法一:写入隐藏 textarea

最常见:写进 g-recaptcha-response 再点提交。

token = solve_recaptcha(site_key, "https://staging.example.com/qa-login")

# Inject into the g-recaptcha-response textarea
page.evaluate(f"""
    document.getElementById('g-recaptcha-response').value = '{token}';
""")

# Submit the form
page.click("#login-button")

方法二:调用回调函数

注册了 data-callback 的实现只认回调,写字段不解锁。

# Find the callback function name
callback = page.evaluate("""
    () => {
        const widget = document.querySelector('.g-recaptcha');
        return widget?.getAttribute('data-callback') || null;
    }
""")

# Call it with the token
if callback:
    page.evaluate(f"window['{callback}']('{token}')")
else:
    # Try the default grecaptcha callback
    page.evaluate(f"""
        if (window.grecaptcha) {{
            // Set the response and trigger verification
            document.getElementById('g-recaptcha-response').value = '{token}';
            // Find and call any registered callbacks
            const form = document.querySelector('form');
            if (form) form.dispatchEvent(new Event('submit'));
        }}
    """)

方法三:在 Puppeteer 里拦截 XHR

表单走 REST 接口时 Node.js 更顺手:先看清参数结构,再在页面上下文里写入并回调。

const puppeteer = require('puppeteer');

async function solveRecaptchaInSPA() {
  const browser = await puppeteer.launch({ headless: false });
  const page = await browser.newPage();

  // Intercept the API call and inject the token
  await page.setRequestInterception(true);
  page.on('request', request => {
    if (request.url().includes('/api/login')) {
      const postData = JSON.parse(request.postData() || '{}');
      // Token already set via page.evaluate — let it pass
      request.continue();
    } else {
      request.continue();
    }
  });

  await page.goto('https://staging.example.com/qa-login');
  await page.waitForSelector('[data-sitekey]');

  const siteKey = await page.$eval(
    '[data-sitekey]',
    el => el.getAttribute('data-sitekey')
  );

  // Solve with CaptchaAI (implementation omitted — same as Python)
  const token = await solveCaptcha(siteKey, page.url());

  // Inject token and trigger callback
  await page.evaluate((t) => {
    document.getElementById('g-recaptcha-response').value = t;
    const callback = document.querySelector('.g-recaptcha')
      ?.getAttribute('data-callback');
    if (callback && window[callback]) {
      window[callback](t);
    }
  }, token);
}

React、Vue、Angular 的等待姿势

差异不在 API,在什么时候能读到 sitekey。

React(react-google-recaptcha)

组件异步挂载,等 state="attached" 最稳:

# Wait for React to mount the component
page.wait_for_selector(".g-recaptcha", state="attached")

# The sitekey is in the rendered div's data attribute
site_key = page.eval_on_selector(
    ".g-recaptcha", "el => el.dataset.sitekey"
)

Vue(vue-recaptcha)

v-if 没满足时 DOM 里连节点都没有:

# Vue may use v-if to conditionally render
# Navigate or interact to trigger the condition
page.click("#proceed-to-checkout")
page.wait_for_selector("iframe[src*='recaptcha']")

Angular

多为懒加载模块,选择器写宽些:

# Angular apps may lazy-load reCAPTCHA modules
# Wait for the specific Angular component
page.wait_for_selector("re-captcha, app-recaptcha, [data-sitekey]")

路由切换后组件会重新渲染

SPA 复用同一个页面实例:URL 变了、DOM 换了,page 还是原来那个。上个路由的元素句柄会失效,token 也不能带过去。

# Listen for reCAPTCHA appearing after SPA navigation
page.goto("https://example.com")

# Navigate within the SPA
page.click("a[href='/login']")

# The URL changed but no page reload happened
# Wait for the CAPTCHA to render in the new "page"
page.wait_for_selector("iframe[src*='recaptcha']", timeout=10000)

出海项目里的两个场景

国内站点主流是 GeeTest(极验)、网易易盾,SPA 里的 reCAPTCHA 多见于出海项目:

  • 跨境电商后台:登录页挂 reCAPTCHA v2,把四步封进 pytest 夹具,回归用例自己跑;
  • 多环境联调:sitekey 按环境注入,一律从 DOM 里读。

另外 reCAPTCHA 依赖 Google 域名下的资源,办公网不一定稳,超时先分清是哪一层的问题。

排查表

症状 原因 处理方式
找不到 g-recaptcha-response 组件没渲染 显式等待,超时 15 秒
token 写了但表单没反应 站点用回调实现 data-callback 并调用
字段写完立刻被清空 响应式渲染覆盖了值 改走 data-callback

常见问题

wait_for_selector 超时,是没渲染还是没加载?

看 network 面板有没有 recaptcha/api.js:有请求有响应,是触发条件没满足;没请求就是组件分支没走到。

token 写进去,React 一重渲染就被清空怎么办?

重渲染会用组件自己的 state 覆盖 DOM。优先走 data-callback,没有回调就把写入挪到点提交前一刻。

不用浏览器,只发 HTTP 请求行不行?

知道 sitekey、也复现出提交接口就行,但这两样第一次仍得靠浏览器摸清。

同一个 token 能复用吗?

不能。token 一次性,有效期约 120 秒;切路由、重开弹窗都算新流程。

相关文章

下一步

把这套流程接进现有的 React、Vue 或 Angular 项目——获取 CaptchaAI API Key

该文章已禁用评论。