弹窗里的验证码有一个共同的坑:sitekey 不在页面初始 HTML 里,只有模态框打开、验证码组件渲染后才能从 DOM 读到它。不能像处理普通验证码那样直接抓 sitekey——得先触发模态框、等渲染,提取参数、识别,再赶在模态框关闭或会话过期前把 token 提交进去。
常见场景:出海电商结账页用 Turnstile 做二次校验,登录弹窗则常见 reCAPTCHA v2。国内网络访问 Google 托管脚本不稳定,直接用 CaptchaAI API 完成识别,省下不少调试时间。
弹窗验证码的五种常见场景
| 场景 | 触发方式 | 验证码表现 |
|---|---|---|
| 登录弹窗 | 点击"登录"按钮 | 验证码加载在覆盖层 div 内 |
| 反爬虫拦截层 | 检测到可疑行为后自动弹出 | 全屏模态框拦截页面 |
| 结账二次校验 | 提交支付表单 | 弹出模态框等待验证 |
| 限流提示框 | 请求频率过高被判定为异常 | 弹窗内嵌验证码作为限流门槛 |
| Cookie 同意 + 验证码 | 首次访问页面 | 验证码嵌在同意弹窗里 |
动手写代码之前:两个必须避开的坑
弹窗场景比普通页面多两类风险:弹窗生命周期和 token 时效性,先看清楚再动手实现。
时序陷阱
| 因素 | 影响 | 应对方法 |
|---|---|---|
| 模态框自动关闭超时 | 还没识别完,弹窗就先关了 | 一检测到验证码就立刻开始识别 |
| 识别过程中会话过期 | 服务端 session 在等待期间失效 | 用后台心跳保持会话存活 |
| 验证码在弹窗内渲染延迟 | 组件加载要 1-3 秒才出现在弹窗里 | 弹窗可见后先等 2 秒,再提取 sitekey |
| 填表过程中 token 过期 | 填模态框表单的过程中 token 失效 | 其他字段先填,验证码放最后再识别 |
踩坑排查
| 问题 | 原因 | 处理方式 |
|---|---|---|
| 检测到弹窗,但找不到验证码 | 验证码在弹窗打开后异步加载 | 增加等待时间;用 MutationObserver 监听组件插入 |
| token 已提交,弹窗却不关闭 | 回调函数没有被触发 | 主动查找并调用验证码的 callback |
| 识别过程中弹窗被关闭 | 触发了自动关闭超时 | 用 JS 禁用弹窗计时器:在弹窗定时器上调用 clearTimeout() |
| 每次弹出的 sitekey 都不一样 | 弹窗每次都会生成新的验证码实例 | 始终从当前弹窗 DOM 里现取 sitekey,不要缓存 |
| 点击触发按钮却打不开弹窗 | 元素不可交互,或被其他层挡住了 | 改用 page.dispatchEvent,或等元素变为可点击状态再操作 |
Python 实战:Playwright 检测与识别模态框验证码
import requests
import time
from playwright.sync_api import sync_playwright
API_KEY = "YOUR_API_KEY"
SUBMIT_URL = "https://ocr.captchaai.com/in.php"
RESULT_URL = "https://ocr.captchaai.com/res.php"
def solve_captcha(sitekey, pageurl, method="userrecaptcha"):
"""Submit and poll a CAPTCHA."""
params = {
"key": API_KEY,
"method": method,
"json": 1,
}
if method == "userrecaptcha":
params["googlekey"] = sitekey
params["pageurl"] = pageurl
elif method == "turnstile":
params["sitekey"] = sitekey
params["pageurl"] = pageurl
resp = requests.post(SUBMIT_URL, data=params, timeout=30).json()
if resp.get("status") != 1:
raise RuntimeError(f"Submit failed: {resp.get('request')}")
task_id = resp["request"]
for _ in range(60):
time.sleep(5)
poll = requests.get(RESULT_URL, params={
"key": API_KEY, "action": "get",
"id": task_id, "json": 1,
}, timeout=15).json()
if poll.get("request") == "CAPCHA_NOT_READY":
continue
if poll.get("status") == 1:
return poll["request"]
raise RuntimeError(f"Solve failed: {poll.get('request')}")
raise RuntimeError("Timeout")
def detect_modal_captcha(page):
"""
Detect CAPTCHA inside a visible modal/dialog.
Returns (sitekey, method) or (None, None).
"""
return page.evaluate("""
() => {
// Find visible modals
const modalSelectors = [
'.modal.show',
'.modal[style*="display: block"]',
'dialog[open]',
'[role="dialog"]:not([aria-hidden="true"])',
'.overlay.visible',
'.popup.active',
'[class*="modal"][class*="open"]',
];
let modal = null;
for (const sel of modalSelectors) {
const el = document.querySelector(sel);
if (el && el.offsetParent !== null) {
modal = el;
break;
}
}
// If no modal found, search entire document
const searchRoot = modal || document;
// Check for reCAPTCHA
const recaptcha = searchRoot.querySelector('.g-recaptcha[data-sitekey]');
if (recaptcha) {
return { sitekey: recaptcha.dataset.sitekey, method: 'userrecaptcha' };
}
// Check for Turnstile
const turnstile = searchRoot.querySelector('.cf-turnstile[data-sitekey]');
if (turnstile) {
return { sitekey: turnstile.dataset.sitekey, method: 'turnstile' };
}
// Check for hCaptcha
const hcaptcha = searchRoot.querySelector('.h-captcha[data-sitekey]');
if (hcaptcha) {
return { sitekey: hcaptcha.dataset.sitekey, method: 'hcaptcha' };
}
return null;
}
""")
def inject_token_in_modal(page, token, method="userrecaptcha"):
"""Inject token into the CAPTCHA inside the modal."""
if method == "userrecaptcha":
page.evaluate("""
(token) => {
// Find response textarea (may be inside modal)
const textareas = document.querySelectorAll('#g-recaptcha-response, [name="g-recaptcha-response"]');
textareas.forEach(ta => {
ta.value = token;
ta.style.display = 'block';
});
// Trigger callback
if (typeof ___grecaptcha_cfg !== 'undefined') {
Object.values(___grecaptcha_cfg.clients).forEach(client => {
Object.values(client).forEach(val => {
if (val && typeof val === 'object') {
Object.values(val).forEach(v => {
if (v && typeof v.callback === 'function') v.callback(token);
});
}
});
});
}
}
""", token)
elif method == "turnstile":
page.evaluate("""
(token) => {
const inputs = document.querySelectorAll('[name="cf-turnstile-response"]');
inputs.forEach(inp => { inp.value = token; });
if (typeof window.turnstileCallback === 'function') {
window.turnstileCallback(token);
}
}
""", token)
def handle_modal_captcha(page, trigger_selector=None, timeout=10000):
"""
Full workflow: trigger modal, detect CAPTCHA, solve, inject.
"""
# Step 1: Trigger the modal if needed
if trigger_selector:
print(f"Clicking trigger: {trigger_selector}")
page.click(trigger_selector)
# Step 2: Wait for modal to become visible
print("Waiting for modal...")
modal_selectors = [
".modal.show",
"dialog[open]",
'[role="dialog"]:not([aria-hidden="true"])',
".popup.active",
]
modal_visible = False
for selector in modal_selectors:
try:
page.wait_for_selector(selector, timeout=timeout)
modal_visible = True
print(f" Modal detected: {selector}")
break
except Exception:
continue
if not modal_visible:
print(" No modal detected")
return None
# Step 3: Wait for CAPTCHA to render inside modal
time.sleep(2) # Brief pause for dynamic CAPTCHA loading
captcha_info = detect_modal_captcha(page)
if not captcha_info:
print(" No CAPTCHA found in modal")
return None
sitekey = captcha_info["sitekey"]
method = captcha_info["method"]
print(f" Found {method} CAPTCHA: {sitekey[:20]}...")
# Step 4: Solve via CaptchaAI
token = solve_captcha(sitekey, page.url, method)
print(f" Solved: {token[:30]}...")
# Step 5: Inject token
inject_token_in_modal(page, token, method)
print(" Token injected")
return token
def main():
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
page = browser.new_page()
page.goto("https://example.com")
page.wait_for_load_state("networkidle")
# Handle CAPTCHA that appears in login modal
token = handle_modal_captcha(
page,
trigger_selector="button#login-btn",
timeout=10000,
)
if token:
# Fill form fields inside modal
page.fill('dialog input[name="email"]', "[email protected]")
page.fill('dialog input[name="password"]', "password123")
# Submit modal form
page.click('dialog button[type="submit"]')
page.wait_for_load_state("networkidle")
browser.close()
main()
JavaScript 实战:Puppeteer 处理弹窗验证码
const puppeteer = require("puppeteer");
const API_KEY = "YOUR_API_KEY";
const SUBMIT_URL = "https://ocr.captchaai.com/in.php";
const RESULT_URL = "https://ocr.captchaai.com/res.php";
async function solveCaptcha(sitekey, pageurl, method = "userrecaptcha") {
const body = new URLSearchParams({ key: API_KEY, method, json: "1" });
if (method === "userrecaptcha") { body.set("googlekey", sitekey); body.set("pageurl", pageurl); }
else if (method === "turnstile") { body.set("sitekey", sitekey); body.set("pageurl", pageurl); }
const resp = await (await fetch(SUBMIT_URL, { method: "POST", body })).json();
if (resp.status !== 1) throw new Error(`Submit: ${resp.request}`);
const taskId = resp.request;
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 5000));
const url = `${RESULT_URL}?key=${API_KEY}&action=get&id=${taskId}&json=1`;
const poll = await (await fetch(url)).json();
if (poll.request === "CAPCHA_NOT_READY") continue;
if (poll.status === 1) return poll.request;
throw new Error(`Solve: ${poll.request}`);
}
throw new Error("Timeout");
}
async function waitForModal(page, timeout = 10000) {
const selectors = [".modal.show", "dialog[open]", '[role="dialog"]', ".popup.active"];
for (const sel of selectors) {
try {
await page.waitForSelector(sel, { visible: true, timeout });
return sel;
} catch {}
}
return null;
}
async function detectModalCaptcha(page) {
return page.evaluate(() => {
const checks = [
{ sel: ".g-recaptcha[data-sitekey]", method: "userrecaptcha" },
{ sel: ".cf-turnstile[data-sitekey]", method: "turnstile" },
];
for (const { sel, method } of checks) {
const el = document.querySelector(sel);
if (el && el.dataset.sitekey) return { sitekey: el.dataset.sitekey, method };
}
return null;
});
}
async function handleModalCaptcha(page, triggerSelector) {
// Trigger modal
if (triggerSelector) await page.click(triggerSelector);
// Wait for modal
const modalSel = await waitForModal(page);
if (!modalSel) { console.log("No modal found"); return null; }
console.log(`Modal visible: ${modalSel}`);
// Wait for CAPTCHA render
await new Promise((r) => setTimeout(r, 2000));
const info = await detectModalCaptcha(page);
if (!info) { console.log("No CAPTCHA in modal"); return null; }
console.log(`Found ${info.method}: ${info.sitekey.substring(0, 20)}...`);
const token = await solveCaptcha(info.sitekey, page.url(), info.method);
console.log(`Solved: ${token.substring(0, 30)}...`);
// Inject token
await page.evaluate((t, method) => {
if (method === "userrecaptcha") {
document.querySelectorAll("#g-recaptcha-response").forEach((el) => { el.value = t; });
} else if (method === "turnstile") {
document.querySelectorAll('[name="cf-turnstile-response"]').forEach((el) => { el.value = t; });
}
}, token, info.method);
return token;
}
(async () => {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.goto("https://example.com", { waitUntil: "networkidle2" });
const token = await handleModalCaptcha(page, "button#login-btn");
if (token) {
await page.type('dialog input[name="email"]', "[email protected]");
await page.click('dialog button[type="submit"]');
await page.waitForNavigation();
}
await browser.close();
})();
常见问题
模态框是自动弹出的、不是点击触发的,怎么检测?
用 MutationObserver 监听 DOM 新增元素,在导航前设置好。弹窗元素插入并可见后,观察者触发,即可启动检测流程。
CaptchaAI 支持识别弹窗里出现的 hCaptcha 吗?
不支持。CaptchaAI 不识别 hCaptcha 和 FunCaptcha(Arkose Labs)。若检测命中的是 hCaptcha 组件,需要换别的方案,无法直接套用本文流程。
token 已经提交了,弹窗却一直不消失,可能是什么原因?
通常是回调函数没被触发——很多组件要求显式调用 callback,仅写入隐藏字段不够。参考上面的注入逻辑,确认回调已执行。
表单字段应该在识别验证码之前填,还是之后填?
先填其他字段,验证码放最后识别,缩短拿到 token 到提交表单的间隔,降低过期风险。
相关文章
- 用 API 识别 reCAPTCHA v2 回调型验证码
- GeeTest 与 Cloudflare Turnstile 对比
- 同一站点里 reCAPTCHA v2 和 Turnstile 同时出现怎么处理
下一步
把检测、识别、提交流程封装成可复用函数,弹窗验证码就不再是脚本里的意外情况。获取 CaptchaAI API Key,接入你自己的检测逻辑。
相关指南:
- 同一页面里出现多个验证码怎么处理
- Shadow DOM 里的验证码识别
- 嵌套 iframe 里的验证码提取