Comparisons

Объяснение reCAPTCHA v2 и невидимой reCAPTCHA

Невидимая reCAPTCHA — не отдельная версия, а режим отображения reCAPTCHA v2. Проверка, формат sitekey и поле g-recaptcha-response совпадают. Различается только то, что видит пользователь: флажок «Я не робот» либо ничего.

Отсюда три следствия:

  • со стороны API разница ровно одна — параметр invisible=1;
  • вариант определяется только по разметке, не по sitekey;
  • по виджету никто не кликал, поэтому обратный вызов запускают вручную — именно здесь ломается большинство интеграций.

Разметка: чем отличается флажок от невидимого режима

Флажок v2

<!-- Standard checkbox widget -->
<div class="g-recaptcha"
     data-sitekey="6Le-wvkSAAAAAPBMRTvw..."
     data-callback="onSubmit">
</div>

<!-- Widget renders as: -->
<!-- [✓] I'm not a robot     reCAPTCHA logo -->

Виджет виден, пользователь кликает сам. Если поведение показалось Google подозрительным, под флажком разворачивается сетка изображений.

Невидимая v2

<!-- Pattern 1: Invisible widget on a button -->
<button class="g-recaptcha"
        data-sitekey="6Le-wvkSAAAAAPBMRTvw..."
        data-callback="onSubmit"
        data-size="invisible">
  Submit
</button>

<!-- Pattern 2: Invisible div (programmatic trigger) -->
<div class="g-recaptcha"
     data-sitekey="6Le-wvkSAAAAAPBMRTvw..."
     data-size="invisible"
     data-callback="onSubmit">
</div>

<!-- Pattern 3: Programmatic render -->
<script>
  grecaptcha.render('submit-btn', {
    sitekey: '6Le-wvkSAAAAAPBMRTvw...',
    callback: onSubmit,
    size: 'invisible'
  });
</script>

Кликать не по чему, проверка срабатывает иначе:

  • шаблон 1: виджет на самой кнопке, вызов по нажатию;
  • шаблон 2: скрытый div, вызов инициирует код через grecaptcha.execute();
  • шаблон 3: программный рендер — типичен для SPA и хуже всего ловится по разметке.

Как определить вариант на странице

Определять вариант в рантайме надёжнее захардкоженного списка: конфигурацию меняют без предупреждения.

Проверка на Python

import requests
from bs4 import BeautifulSoup
import re

def detect_recaptcha_variant(url):
    resp = requests.get(url)
    soup = BeautifulSoup(resp.text, "html.parser")

    # Check for invisible indicators
    invisible_widget = soup.find(attrs={"data-size": "invisible", "class": "g-recaptcha"})
    if invisible_widget:
        return {
            "variant": "invisible",
            "sitekey": invisible_widget.get("data-sitekey"),
            "callback": invisible_widget.get("data-callback")
        }

    # Check for programmatic invisible in scripts
    for script in soup.find_all("script"):
        if script.string and "invisible" in str(script.string):
            key_match = re.search(r"sitekey['\"]?\s*[:=]\s*['\"]([^'\"]+)", script.string)
            if key_match:
                return {
                    "variant": "invisible-programmatic",
                    "sitekey": key_match.group(1),
                    "callback": "check grecaptcha.render() call"
                }

    # Check for standard checkbox
    checkbox_widget = soup.find(class_="g-recaptcha")
    if checkbox_widget:
        return {
            "variant": "checkbox",
            "sitekey": checkbox_widget.get("data-sitekey"),
            "callback": checkbox_widget.get("data-callback")
        }

    return None

result = detect_recaptcha_variant("https://staging.example.com/qa-login")
print(result)

Проверка на Node.js

const axios = require("axios");
const cheerio = require("cheerio");

async function detectRecaptchaVariant(url) {
  const { data } = await axios.get(url);
  const $ = cheerio.load(data);

  // Check for invisible
  const invisible = $(".g-recaptcha[data-size='invisible']");
  if (invisible.length) {
    return {
      variant: "invisible",
      sitekey: invisible.attr("data-sitekey"),
      callback: invisible.attr("data-callback"),
    };
  }

  // Check scripts for programmatic invisible
  const scripts = $("script")
    .map((_, el) => $(el).html())
    .get()
    .join("\n");
  if (scripts.includes("invisible")) {
    const keyMatch = scripts.match(/sitekey['"]?\s*[:=]\s*['"]([^'"]+)/);
    if (keyMatch) {
      return {
        variant: "invisible-programmatic",
        sitekey: keyMatch[1],
        callback: "check render call",
      };
    }
  }

  // Check for standard checkbox
  const checkbox = $(".g-recaptcha");
  if (checkbox.length) {
    return {
      variant: "checkbox",
      sitekey: checkbox.attr("data-sitekey"),
      callback: checkbox.attr("data-callback"),
    };
  }

  return null;
}

Быстрая проверка в консоли браузера

const el = document.querySelector('[data-size="invisible"]');
console.log(el ? "Invisible reCAPTCHA" : "Checkbox reCAPTCHA");
Признак Флажок v2 Невидимая v2
Атрибут data-size="invisible" Отсутствует Присутствует
Вызов grecaptcha.execute() Нет, кликает пользователь Да, вызывается кодом
Задача с картинками Под флажком В правом нижнем углу окна

Определяйте вариант на каждом прогоне, а не однократно при написании сценария.


Решение через API CaptchaAI

Схема одинакова: отправьте задачу на in.php, сохраните ID задачи, опрашивайте res.php до готовности токена. Названия параметров и поле результата не меняются.

Флажок v2

import requests
import time

resp = requests.get("https://ocr.captchaai.com/in.php", params={
    "key": "YOUR_API_KEY",
    "method": "userrecaptcha",
    "googlekey": "6Le-wvkSAAAA...",
    "pageurl": "https://example.com/form"
})
task_id = resp.text.split("|")[1]

for _ in range(60):
    time.sleep(5)
    result = requests.get("https://ocr.captchaai.com/res.php", params={
        "key": "YOUR_API_KEY", "action": "get", "id": task_id
    })
    if result.text.startswith("OK|"):
        token = result.text.split("|")[1]
        break

Невидимая v2

import requests
import time

resp = requests.get("https://ocr.captchaai.com/in.php", params={
    "key": "YOUR_API_KEY",
    "method": "userrecaptcha",
    "googlekey": "6Le-wvkSAAAA...",
    "pageurl": "https://example.com/form",
    "invisible": 1  # Only parameter difference
})
task_id = resp.text.split("|")[1]

for _ in range(60):
    time.sleep(5)
    result = requests.get("https://ocr.captchaai.com/res.php", params={
        "key": "YOUR_API_KEY", "action": "get", "id": task_id
    })
    if result.text.startswith("OK|"):
        token = result.text.split("|")[1]
        break

Разница в одну строку: разбор ответа, обработка ошибок и тайм-ауты переносятся без правок.


Передача токена на страницу: главный источник сбоев

Токен получен, форма не отправляется — причина почти всегда одна.

Флажок v2: обычно хватает скрытого поля

# Selenium — inject into hidden field
driver.execute_script(
    f'document.getElementById("g-recaptcha-response").value = "{token}";'
)

# If the page uses a callback, also call it
callback = driver.find_element("css selector", ".g-recaptcha").get_attribute("data-callback")
if callback:
    driver.execute_script(f'{callback}("{token}");')

Невидимая v2: обратный вызов нужно запускать явно

# Selenium — inject AND call the callback
driver.execute_script(
    f'document.getElementById("g-recaptcha-response").value = "{token}";'
)

# CRITICAL: Invisible reCAPTCHA almost always requires calling the callback
callback_name = driver.find_element(
    "css selector", ".g-recaptcha[data-size='invisible']"
).get_attribute("data-callback")

driver.execute_script(f'{callback_name}("{token}");')
// Puppeteer — invisible callback injection
await page.evaluate((tok) => {
  // Set the hidden field
  document.getElementById("g-recaptcha-response").value = tok;

  // Find and call the callback function
  const widget = document.querySelector("[data-size='invisible']");
  const cbName = widget?.getAttribute("data-callback");
  if (cbName && typeof window[cbName] === "function") {
    window[cbName](tok);
  }
}, token);

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

Отсюда порядок действий:

  • запишите токен в g-recaptcha-response;
  • вызовите функцию из data-callback;
  • отправьте форму — но только если обратный вызов не сделал этого сам, иначе form.submit() даст дубль.

Сводка различий

Характеристика Флажок v2 Невидимая v2
Видимый виджет Да — флажок «Я не робот» Нет
Что запускает проверку Клик по флажку Кнопка, отправка формы или загрузка страницы
Трение для пользователя Заметное Почти нулевое
Формат sitekey Тот же Тот же
Поле токена g-recaptcha-response g-recaptcha-response
Обратный вызов Необязателен Нужен почти всегда
Метод CaptchaAI method=userrecaptcha + invisible=1
Срок жизни токена 120 секунд 120 секунд
Время решения 10–30 с 10–25 с

Один решатель на оба варианта

Вместо двух веток кода удобнее один класс: читает разметку, подбирает параметры и возвращает вместе с токеном имя обратного вызова.

import requests
import time
from bs4 import BeautifulSoup

class RecaptchaV2UniversalSolver:
    def __init__(self, api_key):
        self.api_key = api_key

    def detect_and_solve(self, page_url, page_html=None):
        if not page_html:
            page_html = requests.get(page_url).text

        soup = BeautifulSoup(page_html, "html.parser")

        # Detect variant
        invisible = soup.find(attrs={"data-size": "invisible", "class": "g-recaptcha"})
        widget = invisible or soup.find(class_="g-recaptcha")

        if not widget:
            raise Exception("No reCAPTCHA widget found")

        sitekey = widget.get("data-sitekey")
        is_invisible = invisible is not None
        callback = widget.get("data-callback")

        params = {
            "key": self.api_key,
            "method": "userrecaptcha",
            "googlekey": sitekey,
            "pageurl": page_url
        }
        if is_invisible:
            params["invisible"] = 1

        resp = requests.get("https://ocr.captchaai.com/in.php", params=params)
        if not resp.text.startswith("OK|"):
            raise Exception(f"Submit failed: {resp.text}")

        task_id = resp.text.split("|")[1]
        for _ in range(60):
            time.sleep(5)
            result = requests.get("https://ocr.captchaai.com/res.php", params={
                "key": self.api_key, "action": "get", "id": task_id
            })
            if result.text.startswith("OK|"):
                return {
                    "token": result.text.split("|")[1],
                    "variant": "invisible" if is_invisible else "checkbox",
                    "callback": callback,
                    "sitekey": sitekey
                }
            if result.text != "CAPCHA_NOT_READY":
                raise Exception(f"Solve error: {result.text}")

        raise Exception("Timed out")

# Usage
solver = RecaptchaV2UniversalSolver("YOUR_API_KEY")
result = solver.detect_and_solve("https://staging.example.com/qa-login")
print(f"Variant: {result['variant']}, Callback: {result['callback']}")

Возврат callback экономит проход по DOM: сценарий уже знает, что вызывать.


Что делать, если не сработало

Почти все жалобы вида «токен есть, а форма не проходит» сводятся к четырём случаям.

Симптом Что проверить
Токен не принимается В невидимом варианте — вызван ли data-callback, а не только подстановка
Виджет не найден Класс .g-recaptcha, атрибут data-size, вызовы grecaptcha.render() в скриптах
Страница перезагружается после подстановки Обратный вызов должен идти до отправки формы
Токен стабильно устаревает Тайминг: живёт он 120 секунд, а в медленном сценарии Selenium набегает минута ожиданий

Пример: ночной прогон тестов на десяти клиентских формах

У студии из Алматы десять клиентских форм заявок: где-то флажок, где-то невидимый режим. Ночные end-to-end тесты идут в staging, где формы закрыты reCAPTCHA как в продакшене. Универсальный решатель здесь и окупается: сценарий читает разметку на месте, а не хранит карту «сайт → вариант», и переключение формы клиентом не роняет ночной прогон.

Расходы предсказуемы: CaptchaAI тарифицирует одновременные потоки, а не решения. Десять параллельных прогонов укладываются в STANDARD ($30/мес, 15 потоков); при полусотне браузеров следующий шаг — ADVANCE ($90/мес, 50 потоков).

И про данные: в фикстурах держите только синтетические контакты, а не реальные лиды клиента. Для проектов в РФ это требование 152-ФЗ «О персональных данных».


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

Как понять, какой вариант reCAPTCHA стоит на странице?

Читайте разметку, а не ключ. Атрибут data-size="invisible" у элемента g-recaptcha — признак невидимого варианта; без него это флажок. Проверьте и скрипты на grecaptcha.render(): в SPA виджет часто создаётся программно.

Почему токен получен, а форма не отправляется?

Скорее всего, не вызван обратный вызов. Подстановки в g-recaptcha-response мало: страница ждёт функцию из data-callback, которую обычно запускает клик пользователя.

Меняется ли стоимость при переходе на невидимый вариант?

Нет. CaptchaAI тарифицирует потоки, а не отдельные решения; invisible=1 не выделен в отдельный тип. На счёт влияет число одновременных задач.

Может ли сайт отдавать разные варианты разным пользователям?

Да, и нередко: один sitekey даёт флажок на десктопе и невидимую проверку на мобильной вёрстке. Настройка живёт на стороне сайта — определяйте вариант динамически.

Что придётся переписать при переходе с флажка на невидимый режим?

В запросе к API — одну строку invisible=1. Переписать придётся работу со страницей: вместо подстановки в скрытое поле понадобится найти имя обратного вызова и вызвать его.


Связанные материалы

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