Tutorials

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

Короткий ответ: CaptchaAI подключается к Playwright тремя вызовами HTTP-клиента — отправить задачу на in.php, опросить res.php, подставить токен в форму. Playwright изначально построен на async/await, запускает Chromium, Firefox и WebKit из одной кодовой базы и требует меньше ручной донастройки под автоматизацию, чем более старый Selenium. Для команд, которые уже мигрировали свои QA-скрипты на Playwright, интеграция с CaptchaAI сводится к нескольким переиспользуемым функциям, а не к отдельному сервису.

В этом руководстве вы получите:

  • рабочий асинхронный клиент CaptchaAI на aiohttp, который переиспользуется во всех сценариях ниже;
  • готовые функции для reCAPTCHA v2, Cloudflare Turnstile и графической CAPTCHA в Playwright;
  • класс PlaywrightCaptchaSolver, который сам определяет тип CAPTCHA, решает её и отправляет форму;
  • таблицу типичных ошибок интеграции и разбор частых вопросов по потокам и таймаутам.

Что понадобится перед стартом

Установите Playwright и асинхронный HTTP-клиент, затем скачайте бинарник Chromium:

pip install playwright aiohttp
playwright install chromium

Асинхронный клиент CaptchaAI на aiohttp

import aiohttp
import asyncio

API_KEY = "YOUR_API_KEY"


async def solve_captcha(method, **params):
    """Async CaptchaAI solver for Playwright workflows."""
    async with aiohttp.ClientSession() as session:
        # Submit task
        submit_data = {
            "key": API_KEY,
            "method": method,
            "json": 1,
            **params,
        }
        async with session.post("https://ocr.captchaai.com/in.php", data=submit_data) as resp:
            data = await resp.json(content_type=None)
            if data.get("status") != 1:
                raise Exception(f"Submit error: {data.get('request')}")
            task_id = data["request"]

        # Poll for result
        for _ in range(30):
            await asyncio.sleep(5)
            async with session.get("https://ocr.captchaai.com/res.php", params={
                "key": API_KEY,
                "action": "get",
                "id": task_id,
                "json": 1,
            }) as resp:
                result = await resp.json(content_type=None)
                if result.get("status") == 1:
                    return result["request"]
                if result.get("request") == "ERROR_CAPTCHA_UNSOLVABLE":
                    raise Exception("CAPTCHA unsolvable")

        raise TimeoutError("Solve timed out")

Функция solve_captcha — переиспользуемый фрагмент: принимает method (userrecaptcha, turnstile, base64 и т.д.), отправляет задачу на in.php и опрашивает res.php каждые 5 секунд до 30 попыток. Её вызывают все сценарии ниже.


Настройка браузера Playwright для стабильной автоматизации

Явные настройки контекста и user-agent делают поведение скрипта предсказуемым между прогонами:

from playwright.async_api import async_playwright


async def create_browser():
    """Launch Playwright browser with стандартная конфигурация браузера-configuredion settings."""
    pw = await async_playwright().start()
    browser = await pw.chromium.launch(
        headless=False,
        args=[
            "--disable-blink-features=AutomationControlled",
        ],
    )
    context = await browser.new_context(
        user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
                   "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
        viewport={"width": 1920, "height": 1080},
        locale="en-US",
    )

    # Remove Playwright detection signals
    await context.add_init_script("""
        Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
        delete navigator.__proto__.webdriver;
    """)

    page = await context.new_page()
    return pw, browser, context, page

--disable-blink-features=AutomationControlled убирает частый сигнал автоматизации, а add_init_script переопределяет navigator.webdriver до выполнения кода страницы.


Решаем reCAPTCHA v2 в Playwright

Сценарий находит sitekey в DOM, получает токен от CaptchaAI и вызывает JS-callback вручную, если он есть на странице:

import re


async def solve_recaptcha_v2_playwright(page, url):
    """Complete reCAPTCHA v2 solve in Playwright."""
    await page.goto(url, wait_until="networkidle")

    # Extract sitekey from the page
    content = await page.content()
    match = re.search(r'data-sitekey=["\']([A-Za-z0-9_-]{40})["\']', content)
    if not match:
        raise ValueError("reCAPTCHA sitekey not found")

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

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

    # Inject token
    await page.evaluate(f"""() => {{
        document.getElementById('g-recaptcha-response').value = '{token}';
        document.getElementById('g-recaptcha-response').style.display = 'block';
    }}""")

    # Trigger callback if available
    await page.evaluate(f"""() => {{
        if (typeof ___grecaptcha_cfg !== 'undefined') {{
            var clients = ___grecaptcha_cfg.clients;
            for (var key in clients) {{
                var client = clients[key];
                try {{
                    Object.keys(client).forEach(function(k) {{
                        if (client[k] && client[k].callback) {{
                            client[k].callback('{token}');
                        }}
                    }});
                }} catch(e) {{}}
            }}
        }}
    }}""")

    # Submit form
    await page.click("button[type='submit'], input[type='submit']")
    await page.wait_for_load_state("networkidle")

    return token

Решаем Cloudflare Turnstile в Playwright

Turnstile прячет sitekey то в data-sitekey, то в инлайн-скрипте — код проверяет оба варианта:

async def solve_turnstile_playwright(page, url):
    """Complete Turnstile solve in Playwright."""
    await page.goto(url, wait_until="networkidle")

    content = await page.content()

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

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

    # Solve via CaptchaAI
    token = await solve_captcha(
        "turnstile",
        sitekey=sitekey,
        pageurl=url,
    )

    # Inject token into hidden inputs
    await page.evaluate(f"""() => {{
        document.querySelectorAll('[name="cf-turnstile-response"]')
            .forEach(el => el.value = '{token}');
    }}""")

    # Submit
    await page.click("button[type='submit'], input[type='submit']")
    await page.wait_for_load_state("networkidle")

    return token

Решаем графическую CAPTCHA в Playwright

Если CAPTCHA — картинка с искажённым текстом, проще всего сделать скриншот и отправить в CaptchaAI как base64:

async def solve_image_captcha_playwright(page, captcha_selector):
    """Solve image CAPTCHA visible on the page."""
    captcha_element = page.locator(captcha_selector)

    # Screenshot the CAPTCHA image
    img_bytes = await captcha_element.screenshot()
    import base64
    img_base64 = base64.b64encode(img_bytes).decode()

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

    # Type the answer
    captcha_input = page.locator("input[name='captcha'], input[name='code'], input.captcha-input")
    await captcha_input.fill(answer)

    return answer

Перехват сетевых запросов для отладки CAPTCHA

Полезно, когда sitekey не лежит в HTML открыто, а приходит внутренним вызовом страницы — проще поймать его в трафике, чем парсить DOM:

async def intercept_captcha_params(page, url):
    """Intercept network requests to find CAPTCHA parameters."""
    captcha_params = {}

    async def handle_request(route, request):
        if "recaptcha" in request.url or "turnstile" in request.url:
            from urllib.parse import urlparse, parse_qs
            parsed = urlparse(request.url)
            params = parse_qs(parsed.query)
            captcha_params.update(params)
            print(f"Intercepted: {request.url}")
        await route.continue_()

    await page.route("**/*", handle_request)
    await page.goto(url, wait_until="networkidle")
    await page.unroute("**/*")

    return captcha_params

Класс PlaywrightCaptchaSolver: собираем всё в один пайплайн

В реальном проекте эти три функции удобнее собрать в один класс, который сам определяет тип CAPTCHA, решает её и отправляет форму:

import re
import asyncio
import aiohttp
import base64
from playwright.async_api import async_playwright


API_KEY = "YOUR_API_KEY"


class PlaywrightCaptchaSolver:
    """Complete Playwright + CaptchaAI automation class."""

    def __init__(self, api_key, headless=False):
        self.api_key = api_key
        self.headless = headless
        self.pw = None
        self.browser = None
        self.context = None
        self.page = None

    async def start(self):
        """Initialize the browser."""
        self.pw = await async_playwright().start()
        self.browser = await self.pw.chromium.launch(
            headless=self.headless,
            args=["--disable-blink-features=AutomationControlled"],
        )
        self.context = await self.browser.new_context(
            user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
                       "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
            viewport={"width": 1920, "height": 1080},
        )
        await self.context.add_init_script(
            "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
        )
        self.page = await self.context.new_page()

    async def stop(self):
        """Close the browser."""
        if self.browser:
            await self.browser.close()
        if self.pw:
            await self.pw.stop()

    async def navigate(self, url):
        """Navigate and wait for page to load."""
        await self.page.goto(url, wait_until="networkidle")

    async def detect_captcha(self):
        """Detect which CAPTCHA type is present."""
        content = await self.page.content()

        if re.search(r'data-sitekey=["\'][A-Za-z0-9_-]{40}["\']', content):
            if "recaptcha" in content.lower():
                return "recaptcha_v2"

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

        if re.search(r"render=[A-Za-z0-9_-]{40}", content):
            return "recaptcha_v3"

        img_count = await self.page.locator(
            "img.captcha, img[alt*='captcha'], img[src*='captcha']"
        ).count()
        if img_count > 0:
            return "image"

        return None

    async def solve_and_submit(self, url, form_data=None):
        """Full workflow: navigate, detect, solve, fill, submit."""
        await self.navigate(url)
        captcha_type = await self.detect_captcha()

        if captcha_type:
            print(f"Detected: {captcha_type}")
            await self._solve(captcha_type)

        if form_data:
            for name, value in form_data.items():
                try:
                    await self.page.fill(f"[name='{name}']", value)
                except Exception:
                    pass

        await self.page.click("button[type='submit'], input[type='submit']")
        await self.page.wait_for_load_state("networkidle")
        return self.page.url

    async def _solve(self, captcha_type):
        content = await self.page.content()
        url = self.page.url

        if captcha_type == "recaptcha_v2":
            match = re.search(r'data-sitekey=["\']([A-Za-z0-9_-]{40})["\']', content)
            token = await self._api_solve("userrecaptcha", googlekey=match.group(1), pageurl=url)
            await self.page.evaluate(f"""() => {{
                document.getElementById('g-recaptcha-response').value = '{token}';
            }}""")

        elif captcha_type == "turnstile":
            match = re.search(r'data-sitekey=["\']([0-9x][A-Za-z0-9_-]+)["\']', content)
            token = await self._api_solve("turnstile", sitekey=match.group(1), pageurl=url)
            await self.page.evaluate(f"""() => {{
                document.querySelectorAll('[name="cf-turnstile-response"]')
                    .forEach(el => el.value = '{token}');
            }}""")

        elif captcha_type == "image":
            img = self.page.locator("img.captcha, img[alt*='captcha'], img[src*='captcha']").first
            img_bytes = await img.screenshot()
            answer = await self._api_solve("base64", body=base64.b64encode(img_bytes).decode())
            await self.page.fill("input[name='captcha'], input[name='code']", answer)

    async def _api_solve(self, method, **params):
        async with aiohttp.ClientSession() as session:
            async with session.post("https://ocr.captchaai.com/in.php", data={
                "key": self.api_key, "method": method, "json": 1, **params,
            }) as resp:
                data = await resp.json(content_type=None)
                if data.get("status") != 1:
                    raise Exception(f"Submit error: {data.get('request')}")
                task_id = data["request"]

            for _ in range(30):
                await asyncio.sleep(5)
                async with session.get("https://ocr.captchaai.com/res.php", params={
                    "key": self.api_key, "action": "get", "id": task_id, "json": 1,
                }) as resp:
                    result = await resp.json(content_type=None)
                    if result.get("status") == 1:
                        return result["request"]
            raise TimeoutError("Solve timed out")


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


asyncio.run(main())

Playwright или Selenium: что выбрать для решения CAPTCHA

Критерий Playwright Selenium
Нативная асинхронность Да Нет (нужны потоки)
Готовность к автоматизации из коробки Меньше донастройки Требует больше ручной конфигурации
Скорость Быстрее Медленнее грузит страницы
Перехват запросов Встроенный Нужен прокси или расширение
Поддержка браузеров Chromium, Firefox, WebKit Chrome, Firefox, Edge, Safari
Стиль API На промисах, современный Императивный, классический

Для CaptchaAI разницы нет: вызов in.php/res.php одинаков в обоих случаях.


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

  • page.evaluate не срабатывает. Контент страницы ещё не загружен — дождитесь wait_until="networkidle" перед вызовом.
  • Токен не подставляется в форму. Обычно неверный селектор элемента — проверьте реальную разметку через page.content(), а не через инструменты разработчика в обычном браузере.
  • Сайт распознаёт автоматизированный запуск. Не подключён add_init_script — добавьте переопределение navigator.webdriver при создании контекста, как в разделе выше.
  • Таймаут на wait_until="networkidle". Страница держит открытые соединения из-за фоновых polling-скриптов — используйте wait_until="domcontentloaded" вместо networkidle.
  • Скриншот CAPTCHA получается пустым. Элемент находится вне видимой области — прокрутите к нему: await element.scroll_into_view_if_needed().

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

Playwright или Selenium — что выбрать для решения CAPTCHA?

Для новых проектов — Playwright: выше производительность, нативный async/await и меньше донастройки браузера под автоматизацию. Selenium имеет смысл, если у вас уже есть рабочая кодовая база на нём и переписывать её нецелесообразно.

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

Зависит от параллелизма ваших браузерных контекстов: один поток CaptchaAI обслуживает одну CAPTCHA одновременно, а после решения сразу освобождается под следующую. Для нескольких параллельных сессий Playwright (частый сценарий для QA-команд из СНГ, тестирующих staging в европейских регионах) обычно достаточно тарифа ADVANCE ($90/мес, 50 потоков); для крупного парсинга — CORPORATE ($240/мес, 150 потоков) с неограниченным числом решений на поток.

Playwright не находит sitekey Turnstile или reCAPTCHA — что делать?

Проверьте, не грузится ли виджет лениво после networkidle — тогда page.content() ещё не содержит data-sitekey. Добавьте page.wait_for_selector на контейнер виджета либо перехватите запрос к google.com/recaptcha, как показано выше.

Влияет ли headless-режим Playwright на решение CAPTCHA через CaptchaAI?

Нет: CaptchaAI решает CAPTCHA на собственной инфраструктуре независимо от того, в каком режиме запущен ваш браузер. headless=True ускоряет прогон в CI, но некоторые сайты по-разному ведут себя в headless и headed режимах — стоит протестировать оба варианта, если форма после решения CAPTCHA всё равно не отправляется.

Можно ли использовать прокси вместе с Playwright при парсинге через CaptchaAI?

Да, прокси задаётся при создании контекста браузера (browser.new_context(proxy=...)) и не влияет на работу CaptchaAI — сама CAPTCHA решается на стороне API независимо от того, откуда пришёл запрос браузера. Для распределённых QA-стендов из разных регионов это обычная практика: прокси нужен для стабильности сетевого соединения, а не для решения CAPTCHA.


Итоги

Python Playwright + CaptchaAI — рабочий стек для асинхронной автоматизации CAPTCHA: один клиент на aiohttp закрывает reCAPTCHA v2, Cloudflare Turnstile и графические CAPTCHA, а класс PlaywrightCaptchaSolver объединяет определение типа, решение и отправку формы в один вызов. Перехват сетевых запросов и явная настройка контекста браузера снимают большинство проблем с поиском sitekey и ложными срабатываниями антибот-проверок.

Похожие статьи

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