Tutorials

Динамическая загрузка CAPTCHA: обнаружение ленивых CAPTCHA

Скрипт открывает страницу, забирает HTML сразу после page.goto() — и не находит ни .g-recaptcha, ни .cf-turnstile. Это не значит, что на странице нет CAPTCHA. Это значит, что она ещё не отрисована: многие сайты добавляют CAPTCHA в DOM только после клика по кнопке, фокуса на поле формы, прокрутки до нужного блока или задержки в несколько секунд. Если автоматизация не умеет ждать такую CAPTCHA, она либо падает с ошибкой «элемент не найден», либо (хуже) отправляет форму без токена и получает отказ от сервера. Ниже — три рабочих способа поймать момент появления CAPTCHA и готовый код для Puppeteer и Selenium.


Из-за чего CAPTCHA появляется не сразу

Отложенный рендеринг CAPTCHA — обычная практика: так сайты не тратят ресурс на виджет, пока пользователь не дошёл до нужного шага формы. Причины почти всегда одна из пяти:

  • Клик по кнопке — reCAPTCHA добавляется в форму после нажатия «Отправить»; воспроизводится кликом по кнопке.
  • Фокус на поле — CAPTCHA появляется, когда пользователь ставит курсор в input; воспроизводится фокусом на поле email или пароля.
  • Позиция скролла — CAPTCHA рендерится, когда нужный блок попадает в область видимости; воспроизводится прокруткой до формы.
  • Таймер — CAPTCHA подгружается через несколько секунд после отрисовки страницы (в примерах ниже — 3 секунды); воспроизводится обычным ожиданием.
  • Условие в JS — CAPTCHA появляется после ответа конкретного AJAX-запроса; воспроизводится вызовом этого запроса первым.

Прежде чем писать код обнаружения, откройте страницу в обычном браузере с DevTools и посмотрите, какое именно из пяти действий добавляет виджет в DOM — это сэкономит время на подборе триггера вслепую.


Типичные симптомы и на что они указывают

Прежде чем выбирать способ обнаружения, полезно понимать, к какой причине ведёт каждый симптом:

  1. CAPTCHA так и не появляется. Обычно сработал не тот триггер — откройте страницу в DevTools и найдите реальное действие, которое добавляет CAPTCHA в DOM.
  2. sitekey равен null. Элемент уже есть в разметке, но скрипт CAPTCHA ещё не отработал — дождитесь появления iframe CAPTCHA, а не только пустого div-контейнера.
  3. MutationObserver ничего не поймал. Скорее всего, CAPTCHA была в DOM ещё до установки наблюдателя — сначала проверяйте наличие уже существующих элементов, и только потом подписывайтесь на изменения.
  4. Таймаут ожидания. Сайт показывает CAPTCHA только в обычном (не headless) режиме — переключитесь на headed-режим браузера или headless: 'new' в Puppeteer.

Ниже — три способа обнаружения, от самого простого до самого раннего по времени срабатывания.


Способ 1: MutationObserver — ловим появление элемента в DOM

MutationObserver подписывается на изменения дерева документа и срабатывает в момент, когда CAPTCHA реально попадает в DOM — вне зависимости от того, что именно её вызвало. Пример на Puppeteer:

const puppeteer = require('puppeteer');

const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.goto('https://staging.example.com/qa-login');

// Set up MutationObserver before triggering the CAPTCHA
const captchaInfo = await page.evaluate(() => {
  return new Promise((resolve) => {
    // Check if already present
    const existing = document.querySelector('.g-recaptcha, .cf-turnstile, .h-captcha');
    if (existing) {
      resolve({
        type: existing.className,
        sitekey: existing.getAttribute('data-sitekey'),
      });
      return;
    }

    // Watch for new elements
    const observer = new MutationObserver((mutations) => {
      for (const mutation of mutations) {
        for (const node of mutation.addedNodes) {
          if (node.nodeType !== 1) continue;

          const captcha = node.matches?.('.g-recaptcha, .cf-turnstile, .h-captcha')
            ? node
            : node.querySelector?.('.g-recaptcha, .cf-turnstile, .h-captcha');

          if (captcha) {
            observer.disconnect();
            resolve({
              type: captcha.className,
              sitekey: captcha.getAttribute('data-sitekey'),
            });
            return;
          }
        }
      }
    });

    observer.observe(document.body, { childList: true, subtree: true });

    // Timeout after 30 seconds
    setTimeout(() => {
      observer.disconnect();
      resolve(null);
    }, 30000);
  });
});

console.log('Detected CAPTCHA:', captchaInfo);

Обратите внимание на порядок действий: наблюдатель настраивается до триггера, а не после. Если сначала кликнуть по кнопке и только потом подписаться на MutationObserver, есть шанс пропустить момент вставки элемента — особенно если сайт рендерит CAPTCHA быстрее, чем выполняется следующая строка кода. Сам триггер запускается одной из тех же строк, что и в списке причин выше:

// Click the submit button to trigger CAPTCHA
await page.click('#submit-btn');

// Or focus the input
await page.focus('#email');

// Or scroll to the form
await page.evaluate(() => {
  document.querySelector('#signup-form').scrollIntoView();
});

Способ 2: дождаться загрузки JS-библиотеки CAPTCHA

Виджету недостаточно просто появиться в DOM — ему нужна собственная библиотека JavaScript (grecaptcha, turnstile и т. д.), а она может подгрузиться позже самого div. Дождитесь появления глобального объекта, а не только разметки:

// Wait for reCAPTCHA script to load
await page.waitForFunction(() => {
  return typeof window.grecaptcha !== 'undefined' 
    && typeof window.grecaptcha.render === 'function';
}, { timeout: 30000 });

// Now extract parameters
const sitekey = await page.evaluate(() => {
  const el = document.querySelector('.g-recaptcha');
  return el?.getAttribute('data-sitekey');
});

Тот же приём применим и к Cloudflare Turnstile — просто ждите глобальный объект turnstile вместо grecaptcha:

await page.waitForFunction(() => {
  return typeof window.turnstile !== 'undefined';
}, { timeout: 30000 });

const sitekey = await page.evaluate(() => {
  const el = document.querySelector('.cf-turnstile');
  return el?.getAttribute('data-sitekey');
});

Способ 3: перехват вызова render до отрисовки

Самый ранний момент обнаружения — не ждать появления элемента, а подключиться к самой библиотеке CAPTCHA и перехватить её функцию рендеринга ещё до того, как страница успела что-либо загрузить:

// Inject before page scripts run
await page.evaluateOnNewDocument(() => {
  window.__captchaDetected = null;

  // Hook grecaptcha.render
  let _grecaptcha;
  Object.defineProperty(window, 'grecaptcha', {
    set(val) {
      _grecaptcha = val;
      const origRender = val.render;
      val.render = function(container, params) {
        window.__captchaDetected = {
          type: 'recaptcha',
          sitekey: params.sitekey,
          callback: params.callback?.name,
          container: typeof container === 'string' ? container : container.id,
        };
        return origRender.apply(this, arguments);
      };
    },
    get() { return _grecaptcha; },
  });
});

await page.goto('https://example.com/signup');

// Trigger the CAPTCHA (click, scroll, etc.)
await page.click('#show-form');

// Wait for detection
await page.waitForFunction(() => window.__captchaDetected !== null, {
  timeout: 30000,
});

const detected = await page.evaluate(() => window.__captchaDetected);
console.log('Detected:', detected);
// { type: 'recaptcha', sitekey: '6Le-wvkS...', callback: 'onCaptcha', container: 'recaptcha-box' }

Этот способ надёжнее MutationObserver в одном конкретном случае: если CAPTCHA рендерится в скрытый контейнер (display: none), который потом просто становится видимым, а не создаётся заново — MutationObserver такое изменение не поймает, а перехват render() поймает всегда, потому что срабатывает в момент вызова функции библиотеки, а не в момент изменения видимости.


Python и Selenium: ожидание ленивых CAPTCHA

Та же логика на Selenium — только вместо наблюдателя за DOM используется явное ожидание (WebDriverWait) после триггера:

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

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

# Trigger the CAPTCHA loading
submit = driver.find_element(By.ID, "submit-btn")
submit.click()

# Wait for CAPTCHA to appear
try:
    captcha_el = WebDriverWait(driver, 30).until(
        EC.presence_of_element_located((
            By.CSS_SELECTOR,
            ".g-recaptcha, .cf-turnstile, .h-captcha"
        ))
    )
    sitekey = captcha_el.get_attribute("data-sitekey")
    captcha_class = captcha_el.get_attribute("class")

    if "g-recaptcha" in captcha_class:
        captcha_type = "recaptcha"
    elif "cf-turnstile" in captcha_class:
        captcha_type = "turnstile"
    else:
        captcha_type = "hcaptcha"

    print(f"Type: {captcha_type}, Sitekey: {sitekey}")
except Exception:
    print("No CAPTCHA appeared within 30 seconds")

Ждите iframe, а не только div (для reCAPTCHA)

Частая ошибка: код видит div.g-recaptcha в DOM и решает, что CAPTCHA готова, хотя sitekey ещё null, потому что библиотека не успела отрисовать iframe внутрь. Дожидайтесь именно iframe:

# reCAPTCHA loads an iframe even when the div exists but the script is still loading
WebDriverWait(driver, 30).until(
    EC.presence_of_element_located((
        By.CSS_SELECTOR,
        "iframe[src*='recaptcha'], iframe[src*='challenges.cloudflare.com']"
    ))
)
print("CAPTCHA iframe loaded")

Полный цикл: обнаружение → решение → инъекция токена

Дальше — то, ради чего всё это делается: как только sitekey определён, задача уходит в CaptchaAI, а готовый токен возвращается прямо в найденное поле формы:

import requests
import time

def detect_and_solve(driver, api_key, trigger_action=None):
    """Detect a lazy-loaded CAPTCHA, solve it, and inject the token."""

    # 1. Trigger the CAPTCHA
    if trigger_action:
        trigger_action(driver)

    # 2. Wait for it to appear
    captcha_el = WebDriverWait(driver, 30).until(
        EC.presence_of_element_located((
            By.CSS_SELECTOR,
            ".g-recaptcha, .cf-turnstile, .h-captcha"
        ))
    )

    sitekey = captcha_el.get_attribute("data-sitekey")
    page_url = driver.current_url
    captcha_class = captcha_el.get_attribute("class")

    # 3. Determine type and method
    if "g-recaptcha" in captcha_class:
        method, key_param, token_field = "userrecaptcha", "googlekey", "g-recaptcha-response"
    elif "cf-turnstile" in captcha_class:
        method, key_param, token_field = "turnstile", "sitekey", "cf-turnstile-response"
    else:
        method, key_param, token_field = "hcaptcha", "sitekey", "h-captcha-response"

    # 4. Solve with CaptchaAI
    resp = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": api_key, "method": method,
        key_param: sitekey, "pageurl": page_url, "json": "1",
    }).json()

    task_id = resp["request"]
    for _ in range(24):
        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["status"] == 1:
            token = result["request"]
            break

    # 5. Inject
    driver.execute_script(f"""
        const el = document.querySelector('textarea[name="{token_field}"], input[name="{token_field}"]');
        if (el) el.value = arguments[0];
    """, token)

    return token

Важно: пример выше рассчитан на reCAPTCHA (userrecaptcha) и Cloudflare Turnstile (turnstile) — оба типа CaptchaAI решает полноценно. Класс .h-captcha оставлен в селекторе только для распознавания разметки: CaptchaAI не решает hCaptcha, и при находке этого класса задачу в in.php отправлять не нужно — обработайте случай отдельно.


Пример: QA-стенд с формой регистрации на медленном соединении

Частый сценарий для команд, тестирующих продукт на staging-окружении в Европе или Центральной Азии: форма регистрации грузит CAPTCHA по таймеру в 2–3 секунды, а на нестабильном мобильном соединении реальный рендеринг может занять и 8–10 секунд. Жёсткий sleep(3) в этом случае регулярно ловит пустой DOM. Правильная последовательность — выставить WebDriverWait (или waitForFunction в Puppeteer) с запасом на 20–30 секунд и ждать конкретное условие (появление iframe, а не просто div), а не фиксированную паузу. Если тест параллельно проверяет ещё и обработку персональных данных в форме — согласно 152-ФЗ «О персональных данных» стоит собирать в тестовых прогонах только те поля, которые реально нужны для проверки CAPTCHA-флоу, а не полный набор данных пользователя.


Часто задаваемые вопросы

Как понять, что CAPTCHA на странице подгружается отложенно?

Откройте исходный код страницы (Ctrl+U) сразу после загрузки. Если div или скрипт CAPTCHA там отсутствует, но появляется после клика, скролла или паузы — это отложенная (ленивая) загрузка, и её нужно ждать, а не парсить с первого кадра.

Сколько обычно нужно ждать появления CAPTCHA?

В большинстве случаев CAPTCHA рендерится в течение 1–5 секунд после триггера. Ставьте таймаут ожидания с запасом (в примерах выше — 30 секунд), но ждите конкретное условие через WebDriverWait/waitForFunction, а не фиксированную задержку — так скрипт не тормозит зря на быстрых сайтах и не падает на медленных.

Почему MutationObserver пропускает появление CAPTCHA?

Две типичные причины: либо CAPTCHA уже была в DOM до того, как вы подписались на наблюдателя (проверяйте существующие элементы первым делом), либо виджет рендерится внутри iframe с чужого домена — в этом случае наблюдайте за появлением самого iframe[src*='recaptcha'], а не за содержимым внутри него.

Работает ли обнаружение ленивых CAPTCHA в headless-режиме?

В основном да, но с оговорками: часть сайтов намеренно показывает CAPTCHA только в обычном режиме браузера. Используйте headless: 'new' в Puppeteer или запускайте тесты в headed-режиме на staging — это стандартная практика для QA-автоматизации, а не попытка что-то скрыть от сайта.

Что делать сразу после того, как sitekey найден?

Отправьте его вместе с pageurl в in.php с методом, соответствующим типу CAPTCHA (userrecaptcha для reCAPTCHA, turnstile для Cloudflare Turnstile), опросите res.php до получения токена и подставьте его в найденное поле формы — весь цикл собран в функции detect_and_solve() выше.


Решайте CAPTCHA сразу после обнаружения

Получите API-ключ на captchaai.com и подключите его к коду обнаружения выше — от найденного sitekey до готового токена в форме останется один вызов in.php.


Похожие материалы

Комментарии для этой статьи отключены.