Tutorials

Полное руководство по интеграции Python Selenium + CaptchaAI

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

Типичный повод для такой связки — QA-прогон формы логина или визового портала в стейджинге, где страница одновременно требует и reCAPTCHA, и тестовые учётные данные. Используйте только тестовые аккаунты и не прогоняйте через такие скрипты реальные персональные данные — это снимает лишние вопросы к соответствию 152-ФЗ и GDPR при запуске из CI. Пайплайны на раннерах в Европе или Казахстане отдельной настройки не требуют: таймауты запросов к CaptchaAI ниже уже рассчитаны с запасом на сетевые задержки.


Что понадобится

Установите три пакета: Selenium — для управления браузером, requests — для обращения к API CaptchaAI, и webdriver-manager, чтобы не искать драйвер под свою версию Chrome вручную.

pip install selenium requests webdriver-manager

Настройка Selenium: скрываем признаки автоматизации

Многие сайты с CAPTCHA проверяют не только сам виджет, но и флаг navigator.webdriver — по умолчанию Selenium выставляет его в true, и это само по себе может спровоцировать более жёсткую проверку. Функция ниже собирает Chrome с отключёнными признаками автоматизации и удаляет флаг через CDP-команду до того, как страница успевает его прочитать.

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager

def create_driver():
    """Create a Selenium driver with стандартная конфигурация браузера-configuredion settings."""
    options = webdriver.ChromeOptions()
    options.add_argument("--disable-blink-features=AutomationControlled")
    options.add_experimental_option("excludeSwitches", ["enable-automation"])
    options.add_experimental_option("useAutomationExtension", False)

    driver = webdriver.Chrome(
        service=Service(ChromeDriverManager().install()),
        options=options,
    )

    # Remove webdriver flag
    driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
        "source": "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
    })

    return driver

Функция-помощник для запросов к CaptchaAI

Все примеры ниже используют одну и ту же функцию: она отправляет задачу на in.php, получает ID задачи и опрашивает res.php, пока результат не будет готов или пока не истечёт время ожидания. Один и тот же вызов работает для reCAPTCHA, Turnstile и графических CAPTCHA — меняется только method и набор параметров.

Число одновременных запросов из вашего скрипта ограничено тарифом: план BASIC ($15/мес, 5 потоков) хватает для одиночных QA-прогонов, а для параллельных Selenium-сессий в CI обычно берут ADVANCE ($90/мес, 50 потоков) или выше.

import time
import requests

API_KEY = "YOUR_API_KEY"

def solve_captcha(method, **params):
    """Generic CaptchaAI solver — works for all supported CAPTCHA types."""
    submit_data = {
        "key": API_KEY,
        "method": method,
        "json": 1,
        **params,
    }

    submit = requests.post("https://ocr.captchaai.com/in.php", data=submit_data, timeout=30)
    data = submit.json()

    if data.get("status") != 1:
        raise Exception(f"Submit error: {data.get('request')}")

    task_id = data["request"]

    for _ in range(30):
        time.sleep(5)
        result = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": API_KEY,
            "action": "get",
            "id": task_id,
            "json": 1,
        }, timeout=30).json()

        if result.get("status") == 1:
            return result["request"]
        if result.get("request") == "ERROR_CAPTCHA_UNSOLVABLE":
            raise Exception("CAPTCHA unsolvable")

    raise TimeoutError("Solve timed out")

Решаем reCAPTCHA v2 в Selenium

Полный цикл: находим sitekey, решаем, вставляем токен, отправляем форму

Ищем data-sitekey в исходном коде страницы, передаём его вместе с адресом страницы в CaptchaAI, а полученный токен пишем прямо в скрытое поле g-recaptcha-response. Если у виджета есть JS-callback, вызываем его отдельно — иначе форма решит, что капча не пройдена, даже с валидным токеном в поле.

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

def solve_recaptcha_v2_selenium(driver, url):
    """Complete reCAPTCHA v2 solve in Selenium."""
    driver.get(url)

    # Wait for page to load
    WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.TAG_NAME, "body"))
    )

    # Extract sitekey
    page_source = driver.page_source
    match = re.search(r'data-sitekey=["\']([A-Za-z0-9_-]{40})["\']', page_source)
    if not match:
        raise ValueError("reCAPTCHA sitekey not found")
    sitekey = match.group(1)
    print(f"Sitekey: {sitekey}")

    # Solve via CaptchaAI
    token = solve_captcha(
        "userrecaptcha",
        googlekey=sitekey,
        pageurl=url,
    )
    print(f"Token received: {token[:50]}...")

    # Inject token into the page
    driver.execute_script(f"""
        document.getElementById('g-recaptcha-response').value = '{token}';
        document.getElementById('g-recaptcha-response').style.display = 'block';
    """)

    # If there's a callback function, call it
    driver.execute_script(f"""
        if (typeof ___grecaptcha_cfg !== 'undefined') {{
            var clients = ___grecaptcha_cfg.clients;
            for (var key in clients) {{
                var client = clients[key];
                if (client.rr && client.rr.l) {{
                    client.rr.l.callback('{token}');
                }}
            }}
        }}
    """)

    # Submit the form
    submit_button = driver.find_element(By.CSS_SELECTOR, "button[type='submit'], input[type='submit']")
    submit_button.click()

    return token

# Usage
driver = create_driver()
try:
    solve_recaptcha_v2_selenium(driver, "https://staging.example.com/qa-login")
    print(f"Current URL: {driver.current_url}")
finally:
    driver.quit()

Cloudflare Turnstile в Selenium

Turnstile прячет sitekey двумя способами — либо в атрибуте data-sitekey, либо в параметрах JS-виджета, поэтому код ниже пробует оба варианта по очереди. Токен идёт в поле cf-turnstile-response; CaptchaAI обычно возвращает его быстрее, чем reCAPTCHA v2.

def solve_turnstile_selenium(driver, url):
    """Complete Turnstile solve in Selenium."""
    driver.get(url)

    WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.TAG_NAME, "body"))
    )

    # Extract sitekey
    page_source = driver.page_source
    match = re.search(r'data-sitekey=["\']([0-9x][A-Za-z0-9_-]+)["\']', page_source)
    if not match:
        # Try JavaScript API pattern
        match = re.search(r"sitekey\s*:\s*['\"]([0-9x][A-Za-z0-9_-]+)['\"]", page_source)
    if not match:
        raise ValueError("Turnstile sitekey not found")

    sitekey = match.group(1)
    print(f"Turnstile sitekey: {sitekey}")

    # Solve via CaptchaAI
    token = solve_captcha(
        "turnstile",
        sitekey=sitekey,
        pageurl=url,
    )
    print(f"Turnstile token: {token[:50]}...")

    # Inject token
    driver.execute_script(f"""
        // Set the hidden input value
        var inputs = document.querySelectorAll('[name="cf-turnstile-response"]');
        inputs.forEach(function(input) {{ input.value = '{token}'; }});

        // Also try the callback approach
        if (typeof turnstile !== 'undefined' && turnstile.getResponse) {{
            // Widget already rendered
        }}
    """)

    # Submit form
    submit_button = driver.find_element(By.CSS_SELECTOR, "button[type='submit'], input[type='submit']")
    submit_button.click()

    return token

Графическая CAPTCHA в Selenium

Для классических image/text CAPTCHA (введите буквы или цифры с картинки) элемент проще всего отправить в CaptchaAI прямо как скриншот — без сохранения файла на диск:

import base64

def solve_image_captcha_selenium(driver, captcha_selector):
    """Solve image CAPTCHA visible in the browser."""
    # Find the CAPTCHA image element
    captcha_img = driver.find_element(By.CSS_SELECTOR, captcha_selector)

    # Get image as base64
    img_base64 = captcha_img.screenshot_as_base64

    # Solve via CaptchaAI
    answer = solve_captcha("base64", body=img_base64)
    print(f"CAPTCHA answer: {answer}")

    # Type the answer into the input field
    captcha_input = driver.find_element(
        By.CSS_SELECTOR, "input[name='captcha'], input[name='code'], input.captcha-input"
    )
    captcha_input.clear()
    captcha_input.send_keys(answer)

    return answer

reCAPTCHA v3 в Selenium

reCAPTCHA v3 не показывает виджет и не блокирует форму сама по себе — она просто возвращает score, поэтому здесь достаточно получить токен и разложить его по всем полям g-recaptcha-response, которые могли быть на странице (иногда их несколько — скрытое и видимое).

def solve_recaptcha_v3_selenium(driver, url, sitekey, action="verify"):
    """Solve reCAPTCHA v3 in Selenium."""
    driver.get(url)

    # Solve via CaptchaAI
    token = solve_captcha(
        "userrecaptcha",
        googlekey=sitekey,
        pageurl=url,
        version="v3",
        action=action,
    )

    # Inject token
    driver.execute_script(f"""
        // Set reCAPTCHA response
        var textarea = document.getElementById('g-recaptcha-response');
        if (textarea) {{
            textarea.value = '{token}';
            textarea.style.display = 'block';
        }}

        // Set any hidden input fields
        var inputs = document.querySelectorAll('input[name="g-recaptcha-response"]');
        inputs.forEach(function(input) {{ input.value = '{token}'; }});
    """)

    return token

Готовый класс для полной автоматизации

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

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
import re
import time
import requests

API_KEY = "YOUR_API_KEY"

class SeleniumCaptchaSolver:
    """Complete Selenium + CaptchaAI automation class."""

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

    def start(self):
        """Initialize the browser."""
        options = webdriver.ChromeOptions()
        options.add_argument("--disable-blink-features=AutomationControlled")
        options.add_experimental_option("excludeSwitches", ["enable-automation"])
        self.driver = webdriver.Chrome(options=options)
        self.driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
            "source": "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
        })

    def stop(self):
        """Close the browser."""
        if self.driver:
            self.driver.quit()

    def navigate(self, url):
        """Navigate to URL and wait for load."""
        self.driver.get(url)
        WebDriverWait(self.driver, 15).until(
            lambda d: d.execute_script("return document.readyState") == "complete"
        )

    def detect_captcha(self):
        """Detect which CAPTCHA type is on the page."""
        source = self.driver.page_source

        if re.search(r'data-sitekey=["\'][A-Za-z0-9_-]{40}["\']', source):
            if "recaptcha/api.js" in source or "grecaptcha" in source:
                return "recaptcha_v2"

        if "cf-turnstile" in source or "challenges.cloudflare.com/turnstile" in source:
            return "turnstile"

        if re.search(r'recaptcha.*v3|render=[A-Za-z0-9_-]{40}', source):
            return "recaptcha_v3"

        captcha_imgs = self.driver.find_elements(
            By.CSS_SELECTOR, "img.captcha, img[alt*='captcha'], img[src*='captcha']"
        )
        if captcha_imgs:
            return "image"

        return None

    def solve_and_submit(self, url, form_data=None):
        """Navigate, solve CAPTCHA, fill form, and submit."""
        self.navigate(url)
        captcha_type = self.detect_captcha()

        if not captcha_type:
            print("No CAPTCHA detected")
            return self._fill_and_submit(form_data)

        print(f"Detected: {captcha_type}")

        if captcha_type == "recaptcha_v2":
            self._solve_recaptcha_v2()
        elif captcha_type == "turnstile":
            self._solve_turnstile()
        elif captcha_type == "image":
            self._solve_image()

        return self._fill_and_submit(form_data)

    def _solve_recaptcha_v2(self):
        source = self.driver.page_source
        match = re.search(r'data-sitekey=["\']([A-Za-z0-9_-]{40})["\']', source)
        sitekey = match.group(1)

        token = self._api_solve("userrecaptcha", googlekey=sitekey, pageurl=self.driver.current_url)

        self.driver.execute_script(f"""
            document.getElementById('g-recaptcha-response').value = '{token}';
        """)

    def _solve_turnstile(self):
        source = self.driver.page_source
        match = re.search(r'data-sitekey=["\']([0-9x][A-Za-z0-9_-]+)["\']', source)
        sitekey = match.group(1)

        token = self._api_solve("turnstile", sitekey=sitekey, pageurl=self.driver.current_url)

        self.driver.execute_script(f"""
            document.querySelectorAll('[name="cf-turnstile-response"]')
                .forEach(function(el) {{ el.value = '{token}'; }});
        """)

    def _solve_image(self):
        img = self.driver.find_element(
            By.CSS_SELECTOR, "img.captcha, img[alt*='captcha'], img[src*='captcha']"
        )
        answer = self._api_solve("base64", body=img.screenshot_as_base64)

        captcha_input = self.driver.find_element(
            By.CSS_SELECTOR, "input[name='captcha'], input[name='code']"
        )
        captcha_input.clear()
        captcha_input.send_keys(answer)

    def _fill_and_submit(self, form_data):
        if form_data:
            for name, value in form_data.items():
                try:
                    field = self.driver.find_element(By.NAME, name)
                    field.clear()
                    field.send_keys(value)
                except Exception:
                    pass

        submit = self.driver.find_element(
            By.CSS_SELECTOR, "button[type='submit'], input[type='submit']"
        )
        submit.click()
        time.sleep(3)
        return self.driver.current_url

    def _api_solve(self, method, **params):
        submit = requests.post("https://ocr.captchaai.com/in.php", data={
            "key": self.api_key, "method": method, "json": 1, **params,
        }, timeout=30)
        data = submit.json()
        if data.get("status") != 1:
            raise Exception(f"Submit error: {data.get('request')}")

        task_id = data["request"]
        for _ in range(30):
            time.sleep(5)
            result = requests.get("https://ocr.captchaai.com/res.php", params={
                "key": self.api_key, "action": "get", "id": task_id, "json": 1,
            }, timeout=30).json()
            if result.get("status") == 1:
                return result["request"]
        raise TimeoutError("Solve timed out")

# Usage
solver = SeleniumCaptchaSolver(API_KEY)
solver.start()
try:
    result_url = solver.solve_and_submit(
        "https://staging.example.com/qa-login",
        form_data={"username": "[email protected]", "password": "pass123"},
    )
    print(f"Result: {result_url}")
finally:
    solver.stop()

Замените form_data на реальные тестовые поля своей формы — если username и password не нужны (например, страница только запрашивает CAPTCHA перед показом контента), просто передайте form_data=None.


Частые проблемы и их решение

Симптом Причина Решение
CAPTCHA найдена, но токен не встаёт в поле формы g-recaptcha-response не найден в DOM Проверьте, нет ли reCAPTCHA внутри iframe — в этом случае нужно сначала переключиться на него
Токен вставлен, но форма не отправляется JS-callback виджета не сработал Вызовите функцию обратного вызова reCAPTCHA явно, как в примере выше
Сайт пишет, что обнаружен «webdriver» Не применены настройки скрытия автоматизации Добавьте CDP-команду, которая убирает флаг navigator.webdriver
Скриншот графической CAPTCHA получается пустым Элемент не попал в видимую область Сначала проскрольте элемент во вьюпорт
CaptchaAI возвращает токен Turnstile, но сайт его отклоняет sitekey устарел или взят с другой страницы Извлеките sitekey заново из актуального исходника страницы прямо перед решением

Вопросы, которые обычно возникают

Нужен ли Selenium, если можно обойтись requests?

Нет, если форма отправляется обычным POST-запросом и вам нужен только токен. Selenium подключайте, когда сайт требует выполнения JavaScript, подгружает контент динамически или гоняет пользователя через несколько шагов подряд.

Сколько потоков брать для параллельных Selenium-сессий?

Один Selenium-процесс обычно занимает один поток на время решения CAPTCHA. Для последовательного QA-прогона хватает BASIC (5 потоков), а если CI запускает несколько браузеров одновременно, считайте по максимальному числу параллельных сессий и берите тариф с запасом — иначе лишние запросы просто встанут в очередь.

Как понять, что sitekey извлёкся правильно, до вызова API?

Sitekey у reCAPTCHA — это строка из 40 символов, у Turnstile — короче и может начинаться с 0x. Если регулярное выражение вернуло что-то короче или с явно неполными символами, скорее всего страница ещё не успела дорендерить виджет — добавьте WebDriverWait перед извлечением.

Как SeleniumCaptchaSolver понимает, какой тип CAPTCHA перед ним?

Метод detect_captcha() из готового класса выше проверяет исходный код страницы по очереди: 40-символьный data-sitekey вместе с recaptcha/api.js или grecaptcha в разметке — это reCAPTCHA v2; cf-turnstile или challenges.cloudflare.com/turnstile — Turnstile; паттерн render= с тем же 40-символьным ключом без явного виджета — reCAPTCHA v3; а <img> с captcha в классе, alt или src — графическая CAPTCHA. Если ни один паттерн не совпал, solve_and_submit() просто заполняет и отправляет форму без решения CAPTCHA.

Можно ли запускать Selenium в headless-режиме?

Да, но часть сайтов отдельно проверяет headless-режим по сигнатурам WebGL и Canvas. Используйте --headless=new и убедитесь, что эти сигнатуры выглядят как у обычного браузера. На решение CAPTCHA это не влияет — CaptchaAI решает на своих воркерах независимо от режима вашего браузера.


Итог

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

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

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