Use Cases

Автоматическая отправка форм с обработкой CAPTCHA

Форма не отправится, пока не пройдена CAPTCHA — на этом шаге автоматизация обычно и встаёт. Ниже — связка на Selenium и API CaptchaAI: скрипт сам определяет тип защиты (reCAPTCHA v2, Turnstile или картинка), решает её через API и отправляет форму без ручного вмешательства.


Почему CAPTCHA останавливает автоматизацию форм

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

Практическая логика простая: сначала определить, какая защита стоит на конкретной странице (она может отличаться от формы к форме и меняться при обновлении фронтенда), затем получить решение через API и подставить его туда, куда ждёт форма — обычно в скрытый input, реже через явный вызов callback-функции. Дальше форма отправляется как обычно.

Практический сценарий: ночной прогон регрессии в CI

Например, ночной регрессионный прогон в CI против staging с формами за Turnstile: 10 параллельных сценариев укладываются в STANDARD ($30/мес, 15 потоков) — каждый воркер Selenium держит один поток, пока страница загружается, форма заполняется и решается CAPTCHA. Если параллельных сценариев больше 15, часть заданий просто встанет в очередь и увеличит общее время прогона, поэтому число потоков стоит планировать исходя из реального числа одновременных браузерных сессий, а не из числа тестовых сценариев в файле.

Если тестовые данные включают реальные email или другие персональные данные, сверьтесь с требованиями 152-ФЗ «О персональных данных» и используйте только те данные, которые вы вправе обрабатывать в тестовом контуре; для команд с международной аудиторией уместна та же дисциплина в духе GDPR — тестовые окружения не должны становиться местом хранения реальных персональных данных без явного основания.


Архитектура пайплайна

┌────────────┐     ┌──────────────┐     ┌────────────┐     ┌──────────────┐
│ Load Form  │────▶│ Fill Fields  │────▶│ Detect &   │────▶│ Submit Form  │
│ (Selenium) │     │              │     │ Solve      │     │              │
│            │     │              │     │ CAPTCHA    │     │              │
└────────────┘     └──────────────┘     └────────────┘     └──────────────┘

Из чего состоит решение

Пайплайн собирается из трёх независимых классов, каждый отвечает за свою часть: один решает CAPTCHA через API, второй определяет, какая защита стоит на странице, третий связывает всё воедино и управляет самой формой. Такое разделение позволяет переиспользовать модуль решения CAPTCHA в других сценариях (не только для форм) и менять логику детектора, не трогая остальной код.

Модуль решения CAPTCHA

Отправляет задачу в in.php и опрашивает res.php, пока результат не будет готов. initial_wait задаёт паузу перед первым опросом — большинство типов CAPTCHA не решаются быстрее нескольких секунд, поэтому опрашивать res.php сразу после отправки задачи бессмысленно и только тратит лимит запросов.

import time
import requests


class FormCaptchaSolver:
    BASE = "https://ocr.captchaai.com"

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

    def solve(self, params, initial_wait=10):
        params["key"] = self.api_key
        params["json"] = 1
        resp = requests.post(f"{self.BASE}/in.php", data=params).json()
        if resp["status"] != 1:
            raise Exception(f"Submit error: {resp['request']}")

        task_id = resp["request"]
        time.sleep(initial_wait)

        for _ in range(60):
            result = requests.get(
                f"{self.BASE}/res.php",
                params={"key": self.api_key, "action": "get", "id": task_id, "json": 1},
            ).json()
            if result["request"] == "CAPCHA_NOT_READY":
                time.sleep(5)
                continue
            if result["status"] == 1:
                return result["request"]
            raise Exception(f"Solve error: {result['request']}")
        raise TimeoutError("CAPTCHA solve timed out")

Определение типа CAPTCHA на странице

Сначала нужно понять, с чем работает форма — с Turnstile, reCAPTCHA v2 или картинкой. Порядок проверок здесь важен: Turnstile идёт первым, потому что его виджет тоже использует data-sitekey и без явной проверки класса cf-turnstile детектор ошибочно примет его за reCAPTCHA. Если на странице вообще нет виджета с data-sitekey и нет img.captcha, метод возвращает "none" — это нормальный случай для форм без защиты, и solve_captcha() просто пропускает шаг решения.

import re
from selenium.webdriver.common.by import By


class CaptchaDetector:
    def __init__(self, driver):
        self.driver = driver

    def detect(self):
        """Detect CAPTCHA type on current page."""
        html = self.driver.page_source

        # Turnstile
        turnstile = self.driver.find_elements(By.CSS_SELECTOR, ".cf-turnstile, [data-sitekey]")
        for el in turnstile:
            if "cf-turnstile" in (el.get_attribute("class") or ""):
                return "turnstile", el.get_attribute("data-sitekey")

        # reCAPTCHA
        recaptcha = self.driver.find_elements(By.CSS_SELECTOR, "[data-sitekey]")
        if recaptcha:
            sitekey = recaptcha[0].get_attribute("data-sitekey")
            if "recaptcha" in html.lower():
                return "recaptcha_v2", sitekey

        # Image CAPTCHA
        img = self.driver.find_elements(By.CSS_SELECTOR, "img[src*='captcha'], img.captcha")
        if img:
            return "image", img[0].get_attribute("src")

        return "none", None

Управление формой end-to-end

Связывает детектор и решатель: заполняет поля, подставляет токен в нужный input и отправляет форму. Для reCAPTCHA v2 и Turnstile токен присваивается через execute_script напрямую полю с соответствующим name (g-recaptcha-response или cf-turnstile-response) — так надёжнее, чем эмулировать клик по виджету, который сам по себе ничего не решает, а только показывает интерфейс. Для картиночной CAPTCHA solve_captcha() скачивает изображение, кодирует его в base64 и передаёт решателю с методом base64 — распознанный текст затем вводится в обычное текстовое поле как есть.

import base64
import requests as req
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


class FormAutomator:
    def __init__(self, api_key):
        self.solver = FormCaptchaSolver(api_key)
        self.driver = webdriver.Chrome()
        self.detector = CaptchaDetector(self.driver)

    def fill_field(self, selector, value):
        field = WebDriverWait(self.driver, 10).until(
            EC.presence_of_element_located((By.CSS_SELECTOR, selector))
        )
        field.clear()
        field.send_keys(value)

    def select_option(self, selector, value):
        from selenium.webdriver.support.ui import Select
        select = Select(self.driver.find_element(By.CSS_SELECTOR, selector))
        select.select_by_value(value)

    def solve_captcha(self):
        captcha_type, data = self.detector.detect()
        page_url = self.driver.current_url

        if captcha_type == "recaptcha_v2":
            token = self.solver.solve({
                "method": "userrecaptcha",
                "googlekey": data,
                "pageurl": page_url,
            })
            self.driver.execute_script(
                f'document.querySelector("[name=g-recaptcha-response]").value = "{token}";'
            )
            return True

        if captcha_type == "turnstile":
            token = self.solver.solve({
                "method": "turnstile",
                "sitekey": data,
                "pageurl": page_url,
            })
            self.driver.execute_script(
                f'document.querySelector("[name=cf-turnstile-response]").value = "{token}";'
            )
            return True

        if captcha_type == "image":
            img_data = req.get(data).content
            img_b64 = base64.b64encode(img_data).decode()
            text = self.solver.solve({"method": "base64", "body": img_b64})
            captcha_input = self.driver.find_element(
                By.CSS_SELECTOR, "input[name*='captcha']"
            )
            captcha_input.clear()
            captcha_input.send_keys(text)
            return True

        return False  # No CAPTCHA detected

    def submit_form(self, url, fields, submit_selector="button[type='submit']"):
        """
        fields: list of (selector, value) tuples
        """
        self.driver.get(url)

        for selector, value in fields:
            self.fill_field(selector, value)

        self.solve_captcha()

        submit = self.driver.find_element(By.CSS_SELECTOR, submit_selector)
        submit.click()

        return self.driver.current_url

    def close(self):
        self.driver.quit()

Рабочий пример: форма обратной связи

Полный сценарий — от загрузки до отправки. FormAutomator сам открывает страницу, последовательно заполняет каждое поле из списка fields, вызывает solve_captcha() и только потом кликает по кнопке отправки — порядок важен, потому что токен CAPTCHA живёт ограниченное время и должен подставляться последним шагом перед кликом:

automator = FormAutomator("YOUR_API_KEY")

try:
    result_url = automator.submit_form(
        url="https://example.com/contact",
        fields=[
            ("#name", "John Doe"),
            ("#email", "[email protected]"),
            ("#subject", "Sales inquiry"),
            ("#message", "I'd like to learn more about your services."),
        ],
        submit_selector="#submit-btn",
    )
    print(f"Form submitted. Redirected to: {result_url}")
finally:
    automator.close()

Адаптация под разные типы форм

Один и тот же submit_form() работает для входа, регистрации и поиска — меняются только селекторы, URL и набор полей, а логика определения и решения CAPTCHA остаётся общей. Это удобно для регрессионных наборов: один и тот же FormAutomator покрывает десятки сценариев без дублирования кода решения CAPTCHA.

Форма авторизации

result = automator.submit_form(
    url="https://staging.example.com/qa-login",
    fields=[
        ("#username", "testuser"),
        ("#password", "testpass123"),
    ],
    submit_selector="#login-btn",
)

Такой сценарий покрывает и обычный вход, и восстановление после неудачной попытки — если логин отклонён, форма может подключить CAPTCHA только на повторной попытке (см. FAQ ниже про повторные вызовы solve_captcha()).

Форма регистрации

result = automator.submit_form(
    url="https://example.com/register",
    fields=[
        ("#first-name", "Jane"),
        ("#last-name", "Smith"),
        ("#email", "[email protected]"),
        ("#password", "SecurePass!123"),
        ("#confirm-password", "SecurePass!123"),
    ],
    submit_selector="#register-btn",
)

Форма поиска

result = automator.submit_form(
    url="https://example.com/search",
    fields=[
        ("#query", "python developer"),
        ("#location", "San Francisco"),
    ],
    submit_selector="#search-btn",
)

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


Типичные проблемы и их решения

Что чаще всего идёт не так и как это чинить — таблица собрана по частым причинам падений в реальных прогонах, а не по гипотетическим случаям:

Проблема Причина Решение
Токен CAPTCHA отклонён Токен успел «протухнуть» до отправки формы Решайте CAPTCHA последним шагом и отправляйте форму сразу
Поле не найдено на странице Форма подгружается асинхронно Добавьте явное ожидание (WebDriverWait) перед поиском поля
Определён не тот тип CAPTCHA На странице несколько элементов с data-sitekey Проверьте порядок проверок в detect()
Форма перезагружается после отправки Не прошла серверная валидация Проверьте, что заполнены все обязательные поля
Callback reCAPTCHA не срабатывает Нужно явно вызвать функцию обратного вызова Вызовите grecaptcha.execute() после подстановки токена

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

Можно ли отправить форму с CAPTCHA вообще без браузера?

Для reCAPTCHA и Turnstile — да: решаете CAPTCHA через API, получаете токен и отправляете форму обычным POST-запросом, минуя Selenium целиком. Это заметно быстрее для прогонов на сотни форм. Но если форма проверяет JavaScript-состояние страницы или зависит от куки, выставленных скриптом, браузер всё же нужен.

Что делать, если CAPTCHA появляется только после неудачной попытки?

Некоторые формы подключают проверку только после первой ошибки отправки — например, после неверного пароля при логине. В этом случае одного вызова solve_captcha() в начале сценария недостаточно: запускайте его заново при каждой неудачной попытке, проверяя перед этим, появился ли на странице виджет CAPTCHA.

Сколько потоков CaptchaAI нужно для параллельного прогона тестов?

Ориентируйтесь на число одновременных сессий Selenium, а не на число тестовых сценариев в файле: 10–15 воркеров укладываются в STANDARD ($30/мес, 15 потоков), для крупных ночных прогонов на сотню и более сценариев — ADVANCE ($90/мес, 50 потоков). Если потоков не хватает, часть заданий будет просто ждать в очереди, и общее время прогона вырастет, а не упадёт с ошибкой.

Как логировать причину, если CAPTCHA не решилась вовремя?

Оборачивайте вызов solve() в try/except и пишите в лог task_id, тип CAPTCHA и фактическое время ожидания — так на разборе упавшего прогона сразу видно, был ли это сбой самого решения CAPTCHA, таймаут сети или упавший селектор Selenium, который вообще не добрался до формы.


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


Настройте эту связку под свои формы — решайте CAPTCHA через API CaptchaAI.

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