Integrations

aiohttp + CaptchaAI: Resolução de CAPTCHA assíncrono

Um scraper que resolve CAPTCHAs um de cada vez passa a maior parte do tempo esperando a API responder, não processando dados. A saída é assíncrona: aiohttp e a API da CaptchaAI enviam e consultam dezenas de desafios ao mesmo tempo, no mesmo loop de eventos, sem abrir uma thread do sistema operacional por requisição. Isso muda o cálculo de qualquer pipeline de alto volume — de um único worker local a jobs distribuídos rodando em sa-east-1 (São Paulo) para reduzir a latência até os sites monitorados.

O que você precisa antes de começar

Requisito Detalhes
Python 3.8+
aiohttp 3.8+
Chave de API da CaptchaAI Obtenha a sua aqui
pip install aiohttp

Construindo o cliente assíncrono da CaptchaAI

Esta classe concentra os três métodos usados no restante do guia: enviar a tarefa, consultar o resultado com polling e checar o saldo — tudo sem bloquear o event loop do aiohttp. Os métodos de submissão e consulta seguem exatamente o contrato da API (in.php para enviar, res.php para consultar), então você pode reaproveitar essa classe em qualquer tipo de CAPTCHA suportado apenas trocando o method.

import aiohttp
import asyncio


class AsyncCaptchaAI:
    def __init__(self, api_key):
        self.api_key = api_key
        self.base_url = "https://ocr.captchaai.com"

    async def submit(self, session, params):
        """Submit a CAPTCHA task and return the task ID."""
        params["key"] = self.api_key
        async with session.get(
            f"{self.base_url}/in.php", params=params
        ) as resp:
            text = await resp.text()

        if not text.startswith("OK|"):
            raise Exception(f"Submit failed: {text}")

        return text.split("|")[1]

    async def poll(self, session, task_id, timeout=300):
        """Poll for the result with a timeout."""
        params = {
            "key": self.api_key,
            "action": "get",
            "id": task_id,
        }
        deadline = asyncio.get_event_loop().time() + timeout

        while asyncio.get_event_loop().time() < deadline:
            await asyncio.sleep(5)

            async with session.get(
                f"{self.base_url}/res.php", params=params
            ) as resp:
                text = await resp.text()

            if text == "CAPCHA_NOT_READY":
                continue
            if text.startswith("OK|"):
                return text.split("|", 1)[1]
            raise Exception(f"Solve failed: {text}")

        raise TimeoutError(f"Task {task_id} timed out after {timeout}s")

    async def solve(self, session, params, timeout=300):
        """Submit and poll in one call."""
        task_id = await self.submit(session, params)
        return await self.poll(session, task_id, timeout)

    async def get_balance(self, session):
        """Check account balance."""
        params = {"key": self.api_key, "action": "getbalance"}
        async with session.get(
            f"{self.base_url}/res.php", params=params
        ) as resp:
            return float(await resp.text())

Testando com um único CAPTCHA

Antes de escalar para lotes, confirme que a chave de API funciona e resolva um único reCAPTCHA v2 de ponta a ponta — envio, polling e leitura do saldo.

import asyncio
import os

async def main():
    solver = AsyncCaptchaAI(os.environ["CAPTCHAAI_API_KEY"])

    async with aiohttp.ClientSession() as session:
        # Check balance
        balance = await solver.get_balance(session)
        print(f"Balance: ${balance:.2f}")

        # Solve reCAPTCHA v2
        token = await solver.solve(session, {
            "method": "userrecaptcha",
            "googlekey": "6Le-wvkS...",
            "pageurl": "https://example.com",
        })
        print(f"Token: {token[:50]}...")

asyncio.run(main())

Resolvendo vários CAPTCHAs em paralelo

Aqui está o ganho real do aiohttp: asyncio.gather dispara todas as tarefas de uma vez e cada uma resolve seu próprio CAPTCHA de forma independente, com return_exceptions=True garantindo que uma falha isolada não derrube o lote inteiro.

async def solve_batch(urls, site_key):
    solver = AsyncCaptchaAI(os.environ["CAPTCHAAI_API_KEY"])

    async with aiohttp.ClientSession() as session:
        tasks = [
            solver.solve(session, {
                "method": "userrecaptcha",
                "googlekey": site_key,
                "pageurl": url,
            })
            for url in urls
        ]

        results = await asyncio.gather(*tasks, return_exceptions=True)

        for url, result in zip(urls, results):
            if isinstance(result, Exception):
                print(f"FAILED {url}: {result}")
            else:
                print(f"SOLVED {url}: {len(result)} chars")

        return results


urls = [
    "https://example.com/page1",
    "https://example.com/page2",
    "https://example.com/page3",
    "https://example.com/page4",
    "https://example.com/page5",
]
asyncio.run(solve_batch(urls, "6Le-wvkS..."))

Web scraping com resolução automática de CAPTCHA

Na prática, a maioria das páginas não exibe CAPTCHA o tempo todo — só quando o site detecta um padrão de tráfego suspeito. Por isso a função abaixo verifica a presença do desafio antes de acionar o solver, economizando chamadas de API nas páginas sem CAPTCHA. Ao coletar e armazenar dados públicos em produção, vale considerar as obrigações da LGPD para retenção e finalidade do uso desses dados.

async def scrape_with_captcha(url, site_key):
    solver = AsyncCaptchaAI(os.environ["CAPTCHAAI_API_KEY"])

    async with aiohttp.ClientSession() as session:
        # Fetch the page
        async with session.get(url) as resp:
            html = await resp.text()

        # Check if page has a CAPTCHA
        if "g-recaptcha" not in html:
            return html  # No CAPTCHA, return content

        # Solve the CAPTCHA
        token = await solver.solve(session, {
            "method": "userrecaptcha",
            "googlekey": site_key,
            "pageurl": url,
        })

        # Submit with solved token
        async with session.post(url, data={
            "g-recaptcha-response": token,
        }) as resp:
            return await resp.text()

Limitando a concorrência com um semáforo

Disparar centenas de tarefas de uma só vez pode saturar a sua cota de threads simultâneas. Um asyncio.Semaphore mantém a concorrência dentro de um teto previsível — ajuste max_concurrent para ficar abaixo do número de threads do seu plano, evitando erros de limite e mantendo a fila estável:

async def solve_with_limit(urls, site_key, max_concurrent=10):
    solver = AsyncCaptchaAI(os.environ["CAPTCHAAI_API_KEY"])
    semaphore = asyncio.Semaphore(max_concurrent)

    async def solve_one(session, url):
        async with semaphore:
            return await solver.solve(session, {
                "method": "userrecaptcha",
                "googlekey": site_key,
                "pageurl": url,
            })

    async with aiohttp.ClientSession() as session:
        tasks = [solve_one(session, url) for url in urls]
        results = await asyncio.gather(*tasks, return_exceptions=True)

    solved = sum(1 for r in results if not isinstance(r, Exception))
    print(f"Solved {solved}/{len(urls)} CAPTCHAs")
    return results

Resolvendo Cloudflare Turnstile de forma assíncrona

O mesmo cliente serve para o Cloudflare Turnstile sem nenhuma mudança de estrutura — só o method muda para turnstile e o parâmetro sitekey substitui googlekey, já que o Turnstile não usa o modelo de site key do reCAPTCHA:

async def solve_turnstile(url, sitekey):
    solver = AsyncCaptchaAI(os.environ["CAPTCHAAI_API_KEY"])

    async with aiohttp.ClientSession() as session:
        token = await solver.solve(session, {
            "method": "turnstile",
            "sitekey": sitekey,
            "pageurl": url,
        })
        return token

Erros comuns e como corrigir

Erro Causa Correção
ClientConnectorError Problema de rede Verifique a conectividade
Submit failed: ERROR_ZERO_BALANCE Sem saldo Recarregue o saldo da conta
TimeoutError Resolução lenta Aumente o parâmetro de timeout
RuntimeError: Event loop is closed Uso de asyncio.run no Jupyter Use nest_asyncio

Perguntas frequentes

Preciso reescrever todo o scraper em asyncio para usar isso?

Não. Você pode manter o restante do código síncrono e isolar só a etapa de resolução de CAPTCHA em uma função assíncrona chamada via asyncio.run(). Migrar o scraper inteiro só compensa quando o volume de requisições realmente justifica o esforço de reescrita.

Quantas requisições simultâneas a API da CaptchaAI aguenta com esse cliente?

Mais de 100 requisições em paralelo. Na prática, o teto real costuma ser o número de threads do seu plano contratado — use o semáforo para manter a concorrência dentro desse limite em vez de descobrir o teto por tentativa e erro.

O que fazer se aparecer ERROR_ZERO_BALANCE no meio de um lote grande?

As tarefas que já estavam em andamento continuam falhando até você recarregar o saldo. Trate esse erro como definitivo, sem retentativa automática, e dispare um alerta — retomar o lote depois da recarga é mais simples do que tentar recuperar cada tarefa individualmente.

Esse cliente funciona bem para workers rodando em sa-east-1 (São Paulo)?

Sim, e ajuda: como a resolução acontece no servidor da CaptchaAI, a latência entre o seu worker e a origem do CAPTCHA importa menos do que a latência entre o worker e a própria API da CaptchaAI.

Vale mais usar aiohttp do que httpx nesse caso?

Para cargas de alta concorrência, sim — aiohttp é a biblioteca assíncrona mais madura do ecossistema Python, com o melhor desempenho documentado nesse cenário. O guia de integração com httpx cobre a alternativa, que também funciona bem para volumes menores.

Guias relacionados

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