Integrations

undetected-chromedriver com CaptchaAI: integração em Python

Resposta curta: o undetected-chromedriver reduz a quantidade de CAPTCHAs que a sua automação encontra, mas não elimina nenhum deles. Quando o desafio aparece mesmo assim, quem resolve é a API da CaptchaAI, que devolve um token para você colocar no formulário. É por isso que a maioria das suítes de QA em Python acaba usando as duas peças juntas — e todos os exemplos aqui apontam para staging.example.com, dentro de um escopo de QA autorizado.


Cada ferramenta resolve um problema diferente

  • O undetected-chromedriver é uma biblioteca Python que aplica patches no ChromeDriver do Selenium: cuida da correspondência de versão com o Chrome e ajusta os sinais de automação que o driver padrão expõe. Resultado: menos desafios em páginas que olham só para o driver.
  • A CaptchaAI é um serviço de resolução de CAPTCHA. Você envia a sitekey e a URL, ela devolve o token. Não abre navegador nem substitui o Selenium.

Um time brasileiro que roda a regressão de madrugada em sa-east-1 esbarra na consequência disso: o teste de login passa na máquina do desenvolvedor e falha no runner. A diferença raramente está no código — o runner sobe em modo headless, sem perfil de navegador, e a página exibe o reCAPTCHA v2 que nunca aparecia localmente. Tratar o desafio como caso esperado é o que deixa a suíte estável nos dois ambientes. Se houver coleta de dados, considere as obrigações da LGPD (RGPD em Portugal) sobre o que fica nos logs.


O que você precisa

Requisito Detalhes
Chave de API CaptchaAI Crie sua conta em captchaai.com
Python 3.8+
Navegador Chrome Instalado no sistema

O plano BASIC (US$ 15/mês, 5 threads) atende uma suíte pequena: a cobrança é por thread simultânea, não por resolução, e cada thread aceita resoluções ilimitadas no mês. Para dezenas de cenários em paralelo, o degrau seguinte é o STANDARD (US$ 30/mês, 15 threads).

Instale as dependências:

pip install undetected-chromedriver requests

Passo 1: suba o navegador corrigido

Em containers, --no-sandbox é obrigatório na prática, e fixar a janela evita elementos fora da viewport em runners sem tela.

import undetected_chromedriver as uc
import requests
import time


def create_qa_browser_browser():
    """Create an undetected Chrome browser instance."""
    options = uc.ChromeOptions()
    options.add_argument("--no-sandbox")
    options.add_argument("--window-size=1920,1080")

    driver = uc.Chrome(options=options)
    return driver

Em Docker, acrescente --disable-dev-shm-usage: o /dev/shm padrão dos containers é pequeno demais para o Chrome e causa travamentos que parecem problemas de rede.


Passo 2: encontre a sitekey na página

A sitekey é a chave pública do widget. Quase sempre está no atributo data-sitekey de uma div; em implementações antigas, só no src do iframe, após o parâmetro k=. A função abaixo tenta as duas rotas:

API_KEY = "YOUR_API_KEY"


def extract_recaptcha_sitekey(driver):
    """Extract reCAPTCHA v2 sitekey from the page."""
    try:
        element = driver.find_element("css selector", "[data-sitekey]")
        return element.get_attribute("data-sitekey")
    except Exception:
        # Try finding in iframe src
        iframes = driver.find_elements("css selector", "iframe[src*='recaptcha']")
        for iframe in iframes:
            src = iframe.get_attribute("src")
            if "k=" in src:
                return src.split("k=")[1].split("&")[0]
    return None

Se as duas devolverem None, não insista aumentando o tempo de espera: quase sempre o widget está dentro de outro iframe e falta um switch_to.frame antes da busca.


Passo 3: envie o desafio para a CaptchaAI

O envio vai para in.php com method=userrecaptcha e o resultado é consultado em res.php: espere um intervalo inicial e depois consulte em ciclo até receber o token ou estourar o tempo limite.

def solve_recaptcha_v2(sitekey, pageurl):
    """Submit reCAPTCHA v2 to CaptchaAI and return the token."""
    submit = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": API_KEY,
        "method": "userrecaptcha",
        "googlekey": sitekey,
        "pageurl": pageurl,
        "json": 1
    }).json()

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

    task_id = submit["request"]

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

        if result.get("status") == 1:
            return result["request"]
        if result.get("request") != "CAPCHA_NOT_READY":
            raise RuntimeError(f"Solve error: {result['request']}")
        time.sleep(5)

    raise TimeoutError("Solve timed out")

Dois detalhes evitam a maior parte dos problemas aqui:

  • A primeira consulta só faz sentido depois de cerca de 20 s. Antes disso, você só queima requisições que retornam CAPCHA_NOT_READY.
  • Qualquer resposta diferente de CAPCHA_NOT_READY é erro real (chave inválida, sitekey errada, saldo zerado) e deve interromper o fluxo na hora, em vez de entrar no ciclo de espera.

Passo 4: entregue o token à página

O token volta como uma string longa. Ele vai para o campo g-recaptcha-response e, em muitas implementações, também para a função de callback registrada pela página — sem isso o botão continua desabilitado mesmo com o campo preenchido.

def inject_recaptcha_token(driver, token):
    """Inject the solved token into the page and submit."""
    driver.execute_script(f'''
        document.getElementById("g-recaptcha-response").innerHTML = "{token}";
        document.getElementById("g-recaptcha-response").style.display = "block";
    ''')

    # If there's a callback function, trigger 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 && client.callback) {{
                    client.callback("{token}");
                }}
            }}
        }}
    ''')

Fluxo completo: login com reCAPTCHA v2

Tudo junto em um script executável, com três hábitos que economizam depuração: URL de staging, credenciais fictícias e finally fechando o navegador.

import undetected_chromedriver as uc
import requests
import time

API_KEY = "YOUR_API_KEY"


def solve_recaptcha(sitekey, pageurl):
    submit = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": API_KEY, "method": "userrecaptcha",
        "googlekey": sitekey, "pageurl": pageurl, "json": 1
    }).json()

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

    task_id = submit["request"]
    time.sleep(20)

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


def main():
    driver = uc.Chrome()

    try:
        # Navigate to target page
        driver.get("https://staging.example.com/qa-login")
        time.sleep(3)

        # Fill in form fields
        driver.find_element("id", "username").send_keys("user")
        driver.find_element("id", "password").send_keys("pass")

        # Extract sitekey
        element = driver.find_element("css selector", "[data-sitekey]")
        sitekey = element.get_attribute("data-sitekey")
        pageurl = driver.current_url
        print(f"Sitekey: {sitekey}")

        # Solve CAPTCHA
        token = solve_recaptcha(sitekey, pageurl)
        print(f"Token: {token[:50]}...")

        # Inject token
        driver.execute_script(
            f'document.getElementById("g-recaptcha-response").innerHTML = "{token}";'
        )

        # Submit form
        driver.find_element("id", "submit-btn").click()
        time.sleep(3)

        print(f"Current URL: {driver.current_url}")
    finally:
        driver.quit()


if __name__ == "__main__":
    main()

O mesmo fluxo para Cloudflare Turnstile

O Turnstile muda três coisas: o method passa a ser turnstile, o parâmetro é sitekey em vez de googlekey e o campo de destino é cf-turnstile-response. A espera inicial também é menor.

def solve_turnstile(sitekey, pageurl):
    submit = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": API_KEY, "method": "turnstile",
        "sitekey": sitekey, "pageurl": pageurl, "json": 1
    }).json()

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

    task_id = submit["request"]
    time.sleep(10)

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


# Inject Turnstile token
def inject_turnstile_token(driver, token):
    driver.execute_script(f'''
        var input = document.querySelector('[name="cf-turnstile-response"]');
        if (input) input.value = "{token}";
    ''')

O restante é idêntico, o que facilita unificar as duas funções com um parâmetro de tipo. Detalhamento dos campos em como resolver Cloudflare Turnstile pela API e como resolver reCAPTCHA v2 pela API.


Quando algo dá errado

Sintoma Causa provável O que fazer
Versão do Chrome incompatível O patch não achou driver correspondente Atualize o Chrome ou informe version_main
O CAPTCHA aparece mesmo assim A página avalia sinais além do driver Esperado — resolva pela CaptchaAI
O token entra, mas o botão não libera Falta acionar o callback Confira o widget na página
WebDriverException na subida O Chrome travou no container Use --no-sandbox e --disable-dev-shm-usage
CAPCHA_NOT_READY sem fim Espera inicial curta demais Aumente o intervalo antes da 1ª consulta
ERROR_ZERO_BALANCE Saldo esgotado Verifique o saldo antes de rodar a suíte

O modo headless funciona, mas aciona mais desafios que o modo com janela. Se a taxa de CAPTCHAs subir depois de migrar o runner, o comparativo entre Chrome headless e com janela explica o que muda.


Perguntas frequentes

O undetected-chromedriver sozinho elimina o CAPTCHA?

Não. Ele reduz a frequência do desafio, porque corrige sinais que o ChromeDriver padrão expõe. Páginas que avaliam comportamento, IP ou histórico de sessão continuam exibindo o widget — e é aí que a API entra.

Quantas threads eu preciso para uma suíte de testes?

Conte os cenários com CAPTCHA que rodam ao mesmo tempo, não o total de testes. Cinco jobs paralelos cabem no BASIC (US$ 15/mês, 5 threads); quinze pedem o STANDARD (US$ 30/mês, 15 threads). Como cada thread aceita resoluções ilimitadas no mês, o custo segue previsível mesmo em execução noturna diária.

Esse mesmo fluxo cobre hCaptcha?

Não. hCaptcha e FunCaptcha não são suportados. A API cobre reCAPTCHA v2 e v3 (incluindo Enterprise), Cloudflare Turnstile e Challenge, GeeTest v3, CAPTCHAs de imagem/OCR e de grade, mais CaptchaFox (beta), Friendly Captcha (beta) e Lemin (beta). GeeTest v4 consta como "em breve".

Dá para usar isso em Selenium Grid?

Sim, com um cuidado: o undetected-chromedriver aplica o patch no binário do driver local. Em execução distribuída, prepare o driver em cada nó antes de subir a grade — senão só o coordenador fica com a versão corrigida.

Preciso de autorização para rodar esses testes?

Sim. O uso previsto é o ambiente que você controla ou tem permissão explícita para testar: staging próprio, endpoints internos, dados fictícios. Valide também no servidor que o token foi aceito — sem isso, um teste pode passar só porque nada foi verificado.


Próximo passo

Pegue sua chave de API em captchaai.com, rode o script de login contra o seu staging e meça quanto tempo a resolução acrescenta ao teste. Com esse número, dá para dimensionar o plano antes de agendar a suíte noturna.


Guias relacionados

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