Tutorials

Guia completo de integração Python Playwright + CaptchaAI

Precisa resolver reCAPTCHA, Cloudflare Turnstile ou CAPTCHA de imagem dentro de um script Playwright sem travar o event loop? A resposta curta: uma função assíncrona baseada em aiohttp conversa com a API da CaptchaAI enquanto o Playwright continua livre para navegar, e o token volta direto para o campo certo do formulário antes do envio. É esse padrão que este guia constrói do zero, com código completo para cada tipo de desafio. Diferente do Selenium, o Playwright já nasce assíncrono — a chamada ao solucionador não bloqueia nenhuma thread enquanto espera a resposta da CaptchaAI, que pode levar de poucos segundos a mais de um minuto dependendo do tipo de CAPTCHA.


Pré-requisitos: Playwright e aiohttp em Python

Instale as duas dependências antes de tudo — o Playwright cuida do navegador, o aiohttp fala com a API da CaptchaAI:

pip install playwright aiohttp
playwright install chromium

Crie a função assíncrona que fala com a CaptchaAI

Essa função é a base de tudo o que vem a seguir: ela envia a tarefa para in.php, faz o polling em res.php até o token ficar pronto e é reaproveitada por cada uma das funções de resolução deste guia.

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")

Configure o navegador Playwright para automação estável

Abra o Chromium com os argumentos que removem os sinais mais óbvios de automação e ajuste o sinal de navegador que identifica a sessão como controlada por script. Isso evita bloqueios genéricos de bot em sites comuns — mas lembre-se: a CaptchaAI resolve o desafio na própria infraestrutura, então essa configuração do navegador não muda a taxa de resolução do CAPTCHA em si, só o acesso à página até chegar nele.

from playwright.async_api import async_playwright


async def create_browser():
    """Launch Playwright browser with stealth-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

Resolva reCAPTCHA v2 no Playwright

Extraia a sitekey do HTML, envie para a CaptchaAI e injete o token de volta no campo oculto do reCAPTCHA — disparando também o callback do widget quando ele existir:

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

Resolva Cloudflare Turnstile no Playwright

O fluxo é o mesmo do reCAPTCHA — só muda o campo de destino do token, cf-turnstile-response em vez de g-recaptcha-response:

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

Resolva CAPTCHA de imagem no Playwright

Para CAPTCHA de imagem, o Playwright tira um screenshot do elemento, converte para base64 e envia direto para a CaptchaAI decodificar:

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

Intercepte requisições de rede para capturar parâmetros do CAPTCHA

O Playwright tem suporte nativo à interceptação de requisições, o que costuma ser mais confiável do que aplicar regex sobre o HTML para achar sitekey ou action: os parâmetros chegam prontos, extraídos direto da chamada de API que o próprio site já faz. Se você registrar esses parâmetros em log, trate a pageurl e qualquer token capturado como dado potencialmente sensível sob a LGPD — evite persisti-los em texto puro sem necessidade real.

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

Classe completa: detecte, resolva e envie automaticamente

Juntando as funções anteriores em uma única classe, PlaywrightCaptchaSolver identifica sozinha qual tipo de CAPTCHA está na página e aplica a resolução certa antes de enviar o formulário:

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": "user@example.com", "password": "pass123"},
        )
        print(f"Result: {result}")
    finally:
        await solver.stop()


asyncio.run(main())

Playwright ou Selenium: qual usar para resolver CAPTCHA?

Os dois frameworks funcionam bem com a CaptchaAI — a diferença está na experiência de automação, não na taxa de resolução do CAPTCHA:

Recurso Playwright Selenium
Nativo assíncrono Sim Não (exige threads separadas)
Configuração para automação Padrões prontos Exige mais configuração manual
Velocidade Menor latência, carregamento mais rápido Carregamento de página mais lento
Interceptação de requisições Nativa Requer proxy/extensão
Múltiplos navegadores Chromium, Firefox, WebKit Chrome, Firefox, Edge, Safari
Estilo de API Baseado em promises, moderno Imperativo, tradicional

Solução de problemas comuns no Playwright com CaptchaAI

Os sintomas abaixo aparecem com frequência em quem está adaptando esse fluxo a um site novo:

Sintoma Causa Correção
page.evaluate falha Conteúdo não carregado Use wait_until="networkidle"
O token não é aplicado no campo do formulário Seletor de elemento errado Inspecione com page.content() para achar o elemento real
A página identifica o navegador como automatizado Script de inicialização ausente Adicione a substituição da propriedade webdriver do navegador em add_init_script
Timeout em networkidle A página roda scripts com polling infinito Use wait_until="domcontentloaded" em vez disso
A captura de tela da imagem sai em branco Elemento oculto Role até o elemento: await element.scroll_into_view_if_needed()

Perguntas frequentes

Playwright ou Selenium: qual compensa mais para resolver CAPTCHA com a CaptchaAI?

Para projetos novos, o Playwright compensa mais: suporte assíncrono nativo, interceptação de requisições embutida e menos configuração para reduzir sinais óbvios de automação. Vale ficar no Selenium só se você já tem uma base de código grande construída em cima dele — a taxa de resolução em si não muda, porque a CaptchaAI processa o CAPTCHA na própria infraestrutura, fora do navegador.

Quantas páginas do Playwright posso resolver em paralelo?

O limite não é do Playwright, é do seu plano CaptchaAI. O BASIC (US$ 15/mês, 5 threads) já permite 5 resoluções simultâneas; para escalar além disso, o ADVANCE (US$ 90/mês, 50 threads) ou um plano superior libera mais threads em paralelo, com solves ilimitados por thread durante o mês.

Por que o Playwright não encontra a sitekey do reCAPTCHA na página?

Geralmente porque o widget carrega depois do networkidle disparar, ou porque o data-sitekey está em um atributo diferente do que a regex espera. Troque wait_until="networkidle" por page.wait_for_selector(".g-recaptcha, [data-sitekey]") antes de ler o HTML, e confira page.content() para ver o atributo real que o site usa.

hCaptcha funciona com a CaptchaAI no Playwright?

Não. O hCaptcha não é suportado atualmente pela CaptchaAI, independentemente do framework de automação usado — o Playwright só cuida do navegador, quem resolve o desafio é a API. Este guia cobre reCAPTCHA v2, Cloudflare Turnstile e CAPTCHA de imagem, os tipos com disponibilidade geral mais usados em automação com Playwright.


Comece a resolver CAPTCHA com Playwright e a CaptchaAI

Python Playwright + CaptchaAI entrega uma pilha de automação assíncrona completa: PlaywrightCaptchaSolver detecta o tipo de CAPTCHA, resolve e envia o formulário em um único fluxo, com interceptação de requisições e uma configuração de navegador pronta para automação estável.

Pegue sua chave de API em captchaai.com e adapte a classe acima ao primeiro site que você precisa automatizar.

Artigos relacionados

Os comentários estão desativados para este artigo.