Пока модальное окно не открыто, sitekey CAPTCHA в DOM просто нет — виджет подгружается скриптом в момент показа попапа. Порядок действий: открыть модалку, дождаться рендера CAPTCHA внутри неё, извлечь sitekey, решить и внедрить токен — пока окно не закрылось само и не истёк сеанс.
В отличие от статичной CAPTCHA на обычной странице, здесь поиск виджета до триггера бессмысленен — синхронный запрос селектора вернёт пустой результат, потому что узла ещё нет в DOM. Это меняет архитектуру скрипта: сначала событие или клик, затем ожидание появления модалки, и только потом извлечение параметров CAPTCHA внутри неё.
Где CAPTCHA чаще всего встраивают в модалку
| Сценарий | Триггер | Особенность |
|---|---|---|
| Модальный вход в аккаунт | Клик по кнопке «Войти» | CAPTCHA внутри overlay-блока |
| Антибот-проверка на весь экран | Срабатывает автоматически | Модалка перекрывает всю страницу |
| Подтверждение при оформлении заказа | Отправка формы оплаты | Окно всплывает для верификации |
| Ограничение частоты запросов | Слишком много запросов подряд | CAPTCHA-гейт перед повтором |
| Согласие на cookie | Первый визит на сайт | CAPTCHA встроена в диалог согласия |
Разметка модального окна отличается от сайта к сайту: где-то это .modal.show от Bootstrap, где-то нативный <dialog>, где-то кастомный overlay без единого стандартного класса. Поэтому обработчик ниже перебирает несколько селекторов подряд вместо того, чтобы полагаться на один жёстко заданный вариант.
Тайминг модалки: где чаще всего теряют токен
| Фактор | Риск | Митигация |
|---|---|---|
| Автозакрытие по таймеру | Окно закрывается раньше токена | Решайте сразу после обнаружения sitekey |
| Истечение сессии на бэкенде | Сервер сбрасывает сессию во время ожидания | Держите сессию фоновым heartbeat-запросом |
| Задержка рендера CAPTCHA | Виджет догружается 1–3 секунды | Ждите 2 секунды перед чтением sitekey |
| Истечение токена при заполнении формы | Токен стареет, пока вводите поля | Решайте CAPTCHA последней |
Отсюда правило для кода ниже: модалка → пауза на рендер → sitekey → решение → токен и сабмит.
Python и Playwright: обработчик модальных CAPTCHA
Скрипт кликает по триггеру, ждёт модалку по нескольким селекторам и читает data-sitekey. handle_modal_captcha() собирает весь цикл в одну функцию.
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: обработчик модального окна
Та же логика на Puppeteer: waitForModal() перебирает селекторы, а handleModalCaptcha() связывает клик, решение и внедрение токена в один вызов.
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();
})();
Обе версии решают одну задачу с разной механикой ожидания: page.wait_for_selector в Playwright и page.waitForSelector в Puppeteer принимают таймаут в миллисекундах и оба бросают исключение при неудаче, поэтому в обоих обработчиках перебор селекторов обёрнут в try/except (try/catch) — падение на одном варианте селектора не должно останавливать проверку остальных.
Типичные проблемы и их решение
Ниже — пять сбоев, которые чаще всего всплывают в логах при автоматизации модальных CAPTCHA, и как их разобрать по трейсу без повторного ручного прогона.
| Проблема | Причина | Решение |
|---|---|---|
| Модалка есть, CAPTCHA не найдена | Виджет грузится асинхронно | Увеличьте ожидание, ловите вставку через MutationObserver |
| Токен внедрён, модалка не закрывается | Не сработал callback | Вызовите callback вручную |
| Модалка закрывается во время решения | Сработал таймер автозакрытия | clearTimeout() на его идентификаторе |
| Sitekey каждый раз новый | Новый экземпляр CAPTCHA при открытии | Читайте sitekey заново, не кешируйте |
| Клик не открывает модалку | Элемент неинтерактивен или перекрыт | page.dispatchEvent или ждите кликабельности |
Пример из практики: QA модального логина
Частый кейс QA-команд — форма входа, где CAPTCHA всплывает в модалке сразу после клика «Войти», а сессия на бэкенде уже отсчитывает таймаут. При тестах с нестабильного мобильного канала timeout в 10 секунд стоит увеличить — иначе прогон упадёт из-за сети, а не из-за CAPTCHA. Для параллельных прогонов хватает STANDARD ($30/мес, 15 потоков); при нагрузке выше — ADVANCE ($90/мес, 50 потоков). И гигиена логов: сохраняйте из модальной формы только то, что нужно для дебага — с оглядкой на 152-ФЗ и GDPR-практику кросс-граничных команд. Отдельно логируйте, на каком именно селекторе сработало обнаружение модалки — это сильно ускоряет разбор упавших прогонов на CI, где воспроизвести конкретный DOM руками не получится.
FAQ
Как поймать модалку, которая открывается сама, без клика?
Настройте MutationObserver до навигации — он следит за новыми узлами DOM и сработает, как только появится видимая модалка.
CAPTCHA внутри модалки лежит в отдельном iframe — что делать?
Дождитесь модалки, переключитесь в контекст iframe и уже там ищите sitekey — снаружи его не видно.
Сколько реально есть времени до автозакрытия модалки?
Зависит от сайта — обычно от секунд до пары минут. Решайте сразу после обнаружения sitekey, не откладывайте.
Какого тарифа CaptchaAI хватит для параллельных модальных тестов?
BASIC ($15/мес, 5 потоков) — для одиночных прогонов; STANDARD ($30/мес, 15 потоков) или ADVANCE ($90/мес, 50 потоков) — для нескольких стендов сразу.
Токен отправлен, но модалка не закрывается — почему?
Не сработал JS-callback виджета. Вызовите его вручную (___grecaptcha_cfg для reCAPTCHA, window.turnstileCallback для Turnstile).