Use Cases

Web scraping em sites protegidos por CAPTCHA

Quando o CAPTCHA aparece no meio de uma coleta, a resposta cabe em quatro passos: leia a assinatura do widget no HTML, envie os parâmetros dele para a API da CaptchaAI, aguarde o token e reenvie a requisição com esse token no campo certo do formulário.

O que costuma faltar não é a chamada da API, é o desenho em volta dela: qual desafio a página usa, quantas resoluções simultâneas o job precisa e o que fazer quando o site recusa um token aparentemente correto. Este guia trata dos três pontos em Python.

Escopo autorizado e LGPD antes do primeiro requests.get

Os padrões deste guia pressupõem coleta autorizada: staging próprio, endpoints internos, monitoramento com permissão do dono do site, testes de integração.

Se os dados incluírem informação pessoal, a LGPD se aplica mesmo com a página pública — base legal, finalidade e prazo de retenção definidos antes da primeira requisição, e o mesmo vale sob o RGPD em Portugal. Na prática vira decisão de código: o que você grava no log e por quanto tempo guarda o HTML bruto.

Como descobrir qual CAPTCHA a página está usando

O diagnóstico começa no HTML. Cada tipo deixa uma assinatura e mapeia para um method da API:

CAPTCHA Assinatura no HTML Método CaptchaAI
reCAPTCHA v2 div.g-recaptcha + data-sitekey method=userrecaptcha
reCAPTCHA v3 recaptcha/api.js?render= method=userrecaptcha&version=v3
Cloudflare Turnstile div.cf-turnstile method=turnstile
Cloudflare Challenge interstício de página inteira method=turnstile_staging (QA)
Imagem/OCR <img> em formulários legados method=base64
GeeTest v3 gt.js + gt / challenge method=geetest

Duas ressalvas evitam retrabalho. hCaptcha e FunCaptcha (Arkose Labs) não são suportados pela CaptchaAI — se o HTML trouxer h-captcha, este fluxo não se aplica. E o GeeTest v4 ainda não está disponível (anunciado como em breve); só o v3 é resolvido. CaptchaFox, Friendly Captcha e Lemin entram no planejamento como beta.

Dimensionando as threads antes de escrever o loop

A CaptchaAI cobra por thread concorrente, com resoluções ilimitadas no plano. O número que importa não é o total mensal de páginas: é quantas resoluções em andamento ao mesmo tempo.

Junte isso ao tempo de resolução por tipo — Turnstile em menos de 10 s, Cloudflare Challenge em menos de 15 s, reCAPTCHA v2 em menos de 60 s. Um exemplo: uma equipe de QA em São Paulo roda workers em sa-east-1 e dispara 20 páginas em paralelo; se todas exibirem reCAPTCHA v2, cada resolução fica pendente por perto de um minuto e o BASIC (US$ 15/mês, 5 threads) satura na primeira rodada. Aí o ADVANCE (US$ 90/mês, 50 threads) é o degrau natural; só com Turnstile, o STANDARD (US$ 30/mês, 15 threads) cobre com folga.

Essa conta evita o diagnóstico errado mais comum: culpar a API quando o gargalo é a fila de threads do plano.

Padrão 1: detectar e resolver sob demanda

É o padrão mais robusto para páginas mistas: o coletor navega normalmente e só chama a API quando encontra um widget, sem gastar thread em página que passou limpa.

import requests
import time
from bs4 import BeautifulSoup

API_KEY = "YOUR_API_KEY"

class ProtectedScraper:
    def __init__(self):
        self.session = requests.Session()
        self.session.headers.update({
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
        })

    def scrape(self, url):
        resp = self.session.get(url)

        # Check for CAPTCHA
        if self._has_captcha(resp.text):
            resp = self._handle_captcha(resp.text, url)

        return resp.text

    def _has_captcha(self, html):
        indicators = ["g-recaptcha", "cf-turnstile", "h-captcha", "captcha"]
        return any(ind in html.lower() for ind in indicators)

    def _handle_captcha(self, html, url):
        soup = BeautifulSoup(html, "html.parser")

        # reCAPTCHA v2
        rc = soup.find("div", class_="g-recaptcha")
        if rc:
            token = self._solve_recaptcha(rc["data-sitekey"], url)
            return self.session.post(url, data={"g-recaptcha-response": token})

        # Cloudflare Turnstile
        ts = soup.find("div", class_="cf-turnstile")
        if ts:
            token = self._solve_turnstile(ts["data-sitekey"], url)
            return self.session.post(url, data={"cf-turnstile-response": token})

        raise Exception("Unknown CAPTCHA type")

    def _solve_recaptcha(self, site_key, page_url):
        resp = requests.get("https://ocr.captchaai.com/in.php", params={
            "key": API_KEY, "method": "userrecaptcha",
            "googlekey": site_key, "pageurl": page_url
        })
        return self._poll(resp.text.split("|")[1])

    def _solve_turnstile(self, site_key, page_url):
        resp = requests.get("https://ocr.captchaai.com/in.php", params={
            "key": API_KEY, "method": "turnstile",
            "sitekey": site_key, "pageurl": page_url
        })
        return self._poll(resp.text.split("|")[1])

    def _poll(self, task_id):
        for _ in range(60):
            time.sleep(5)
            result = requests.get("https://ocr.captchaai.com/res.php", params={
                "key": API_KEY, "action": "get", "id": task_id
            })
            if result.text == "CAPCHA_NOT_READY": continue
            if result.text.startswith("OK|"): return result.text.split("|")[1]
            raise Exception(result.text)
        raise TimeoutError()

# Usage
scraper = ProtectedScraper()
html = scraper.scrape("https://example.com/data")

Dois detalhes fazem esse código aguentar produção. O token volta para o campo certo de cada tipo — g-recaptcha-response no reCAPTCHA, cf-turnstile-response no Turnstile — e trocar um pelo outro é a causa mais frequente do "a API respondeu OK, mas o site recusou". E o _poll consulta a cada 5 s porque, antes disso, a resposta é quase sempre CAPCHA_NOT_READY.

Padrão 2: resolver antes de carregar a página

Quando uma rota sempre exibe o mesmo desafio — uma busca autenticada, um formulário interno —, a detecção vira desperdício. Com a sitekey conhecida, resolva primeiro e envie o POST direto, economizando um round-trip por página.

def scrape_known_captcha_page(url, site_key):
    # Solve before even loading the page
    token = solve_recaptcha(site_key, url)

    # Submit directly with token
    resp = requests.post(url, data={
        "g-recaptcha-response": token,
        "query": "search term"
    })
    return resp.text

O cuidado aqui é a validade do token: resolva imediatamente antes do envio, nunca em lote no início da execução. Um token gerado no minuto zero de um job de 40 minutos já expirou quando chega a vez dele.

Padrão 3: ambientes atrás do Cloudflare

O interstício do Cloudflare funciona por outro caminho: em vez de um token para o formulário, a resolução produz um cookie de validação que a sessão passa a carregar. Aqui o ambiente de QA devolve cookie_qa_validacao:

def get_cloudflare_clearance(url, proxy):
    resp = requests.get("https://ocr.captchaai.com/in.php", params={
        "key": API_KEY,
        "method": "turnstile_staging",
        "pageurl": url,
        "proxy": proxy,
        "proxytype": "HTTP"
    })
    task_id = resp.text.split("|")[1]

    for _ in range(60):
        time.sleep(5)
        result = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": API_KEY, "action": "get", "id": task_id
        })
        if result.text == "CAPCHA_NOT_READY": continue
        if "cookie_qa_validacao" in result.text:
            # Parse cookie_qa_validacao and user_agent from response
            return result.text
    raise TimeoutError()

Recebido o cookie, a regra é consistência: o mesmo egress autorizado e o mesmo user agent da resolução valem para as requisições seguintes. Mudou um dos dois, a validação cai e você volta ao interstício — sem erro útil.

Percorrendo várias páginas sem derrubar o job

Com os três padrões no lugar, falta o loop. A ideia é isolar a falha de cada página: se a página 7 quebrar, a 8 segue.

def scrape_multiple_pages(base_url, site_key, pages):
    scraper = ProtectedScraper()
    results = []

    for page in pages:
        url = f"{base_url}?page={page}"
        try:
            html = scraper.scrape(url)
            soup = BeautifulSoup(html, "html.parser")
            items = soup.find_all("div", class_="item")
            results.extend([item.text.strip() for item in items])
            print(f"Page {page}: {len(items)} items")
        except Exception as e:
            print(f"Page {page} failed: {e}")

        time.sleep(random.uniform(2, 5))

    return results

O intervalo aleatório (random.uniform(2, 5)) quebra o padrão metronômico que dispara verificação extra. O try/except impede que uma exceção encerre o job inteiro. E o log vira métrica de graça: se a contagem de itens zerar em várias páginas seguidas, o problema já não é CAPTCHA, é seletor de HTML desatualizado.

Troubleshooting: o que realmente quebra

Sintoma O que verificar
CAPTCHA em toda página Reduza o volume, aumente o intervalo, faça rotação de proxy no egress autorizado
Token aceito pela API, recusado pelo site Expirou (use em 120 s) ou campo errado: g-recaptcha-response vs cf-turnstile-response
Cloudflare barra com o cookie válido User agent ou IP de saída mudou; mantenha os dois fixos
Responde 200, mas sem os dados Redirecionamento ou cookie faltando após a validação
CAPCHA_NOT_READY até o timeout Normal nas primeiras consultas; se persistir, confira sitekey e pageurl

Perguntas frequentes

Dá para reaproveitar a sessão e resolver menos vezes?

Sim, na maioria dos casos. Se a sessão mantiver os cookies da primeira validação, as páginas seguintes do mesmo domínio costumam passar direto.

Como levar esse fluxo para o pipeline de testes da equipe?

Em staging com CAPTCHA ativo, resolver o desafio pela API permite exercitar o formulário de ponta a ponta sem desligar a proteção para rodar o teste. Guarde os IDs de tarefa nos logs de CI.

Por quanto tempo o token continua válido?

Trabalhe com cerca de 120 s, contados de quando a API devolve o token — não de quando você o usa. Tokens guardados para depois são a causa clássica de recusa silenciosa.

O que fazer quando o widget só aparece depois do JavaScript?

Renderize com Selenium, Puppeteer ou Playwright, extraia a sitekey e a URL do DOM já montado e só então chame a API. O guia como tratar CAPTCHA no Selenium com Python detalha esse caminho.

O coletor encontrou hCaptcha. E agora?

Não é suportado pela CaptchaAI, assim como o FunCaptcha (Arkose Labs). Os tipos cobertos são reCAPTCHA v2 e v3 (incluindo Enterprise), Cloudflare Turnstile e Challenge, GeeTest v3, imagem/OCR, grade de imagens e BLS, além de CaptchaFox, Friendly Captcha e Lemin em beta.

Leituras relacionadas

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