Use Cases

Como resolver CAPTCHA no web scraping com Node.js

Seu scraper em Node.js roda liso até bater em um reCAPTCHA ou no Cloudflare Turnstile: a requisição trava e a coleta para ali. A saída não é abrir um navegador completo — é resolver o desafio via API e reinjetar o token no mesmo fluxo HTTP. Este tutorial monta esse fluxo com axios e Cheerio, da sitekey ao scraping paralelo.

Quando usar a API em vez de um navegador completo

Nem todo scraper protegido por CAPTCHA precisa de um navegador headless rodando em segundo plano. Antes de montar a infraestrutura, vale decidir qual caminho cabe no seu caso:

  • Formulário simples com POST direto — extraia a sitekey, resolva o token via API e reenvie com axios.post(). É a rota mais rápida e a que consome menos threads do plano.
  • Conteúdo que só existe depois do JavaScript rodar — combine esta API com o Puppeteer (veja o guia relacionado) só para renderizar a página; a resolução do CAPTCHA continua pela API.
  • Alto volume em paralelo — rodar dezenas de navegadores headless consome memória rápido; várias threads na API custam menos e escalam melhor, como mostra a seção a seguir.

O que você vai precisar

Requisito Detalhes
Node.js 16 ou superior Inclui o npm
axios npm install axios
cheerio npm install cheerio
Chave de API da CaptchaAI Obtida em captchaai.com

Construindo o módulo de resolução de CAPTCHA

O módulo concentra a comunicação com a API da CaptchaAI: envia a tarefa a in.php e faz o polling em res.php até o token ficar pronto. Reaproveite essa classe em qualquer script que precise resolver reCAPTCHA ou Turnstile.

// captcha-solver.js
const axios = require("axios");

class CaptchaSolver {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.baseUrl = "https://ocr.captchaai.com";
  }

  async _submit(params) {
    params.key = this.apiKey;
    const resp = await axios.get(`${this.baseUrl}/in.php`, { params });
    if (!resp.data.startsWith("OK|")) {
      throw new Error(`Submit error: ${resp.data}`);
    }
    return resp.data.split("|")[1];
  }

  async _poll(taskId, timeout = 300000) {
    const deadline = Date.now() + timeout;
    while (Date.now() < deadline) {
      await new Promise((r) => setTimeout(r, 5000));
      const resp = await axios.get(`${this.baseUrl}/res.php`, {
        params: { key: this.apiKey, action: "get", id: taskId },
      });
      if (resp.data === "CAPCHA_NOT_READY") continue;
      if (resp.data.startsWith("OK|")) return resp.data.split("|")[1];
      throw new Error(`Solve error: ${resp.data}`);
    }
    throw new Error("Solve timed out");
  }

  async solveRecaptchaV2(siteKey, pageUrl) {
    const taskId = await this._submit({
      method: "userrecaptcha",
      googlekey: siteKey,
      pageurl: pageUrl,
    });
    return this._poll(taskId);
  }

  async solveRecaptchaV3(siteKey, pageUrl, action = "verify") {
    const taskId = await this._submit({
      method: "userrecaptcha",
      googlekey: siteKey,
      pageurl: pageUrl,
      version: "v3",
      action,
    });
    return this._poll(taskId);
  }

  async solveTurnstile(siteKey, pageUrl) {
    const taskId = await this._submit({
      method: "turnstile",
      sitekey: siteKey,
      pageurl: pageUrl,
    });
    return this._poll(taskId);
  }
}

module.exports = CaptchaSolver;

Scraping de uma página protegida por reCAPTCHA v2

O fluxo é sempre o mesmo: carregar a página, extrair a sitekey do atributo data-sitekey, resolver o token e reenviar o formulário com esse token no campo g-recaptcha-response. Troque a URL e o seletor pelo formulário real.

const axios = require("axios");
const cheerio = require("cheerio");
const CaptchaSolver = require("./captcha-solver");

const solver = new CaptchaSolver("YOUR_API_KEY");

async function scrapeProtectedPage(url) {
  // Step 1: Load the page
  const { data: html } = await axios.get(url, {
    headers: {
      "User-Agent":
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
    },
  });

  const $ = cheerio.load(html);

  // Step 2: Extract site key
  const siteKey = $(".g-recaptcha").attr("data-sitekey");
  if (!siteKey) {
    console.log("No CAPTCHA found, page loaded directly");
    return html;
  }

  console.log("Site key found:", siteKey);

  // Step 3: Solve the CAPTCHA
  const token = await solver.solveRecaptchaV2(siteKey, url);
  console.log("Token received:", token.substring(0, 50));

  // Step 4: Submit with the token
  const result = await axios.post(
    url,
    new URLSearchParams({
      "g-recaptcha-response": token,
      q: "search query",
    }),
    {
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
        "User-Agent":
          "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
      },
    }
  );

  return result.data;
}

Scraping paralelo com múltiplos workers

Rodar um worker por vez desperdiça o tempo de espera do polling. O padrão abaixo roda concurrency workers em paralelo com uma fila. Hospedar o scraper em sa-east-1 (São Paulo) reduz a latência, mas o limite real é o número de threads do plano: o BASIC (US$ 15/mês) libera 5, o STANDARD (US$ 30/mês) libera 15.

async function scrapePages(urls, siteKey, concurrency = 3) {
  const results = [];
  const queue = [...urls];

  const worker = async () => {
    while (queue.length > 0) {
      const url = queue.shift();
      try {
        const token = await solver.solveRecaptchaV2(siteKey, url);
        const { data } = await axios.post(
          url,
          new URLSearchParams({ "g-recaptcha-response": token }),
          {
            headers: {
              "User-Agent":
                "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
            },
          }
        );
        results.push({ url, data, success: true });
        console.log(`Scraped: ${url}`);
      } catch (err) {
        results.push({ url, error: err.message, success: false });
        console.error(`Failed: ${url} - ${err.message}`);
      }
    }
  };

  // Run workers concurrently
  const workers = Array(concurrency)
    .fill(null)
    .map(() => worker());
  await Promise.all(workers);

  return results;
}

// Usage
const urls = [
  "https://example.com/page/1",
  "https://example.com/page/2",
  "https://example.com/page/3",
];
const results = await scrapePages(urls, "6Le-wvkS...", 3);

Mantendo cookies e sessões entre requisições

Sites que exigem login derrubam requisições isoladas com 403. Use o axios com um cookie jar persistente para manter a sessão entre o carregamento da página e o envio do token:

const { wrapper } = require("axios-cookiejar-support");
const { CookieJar } = require("tough-cookie");

const jar = new CookieJar();
const client = wrapper(
  axios.create({
    jar,
    headers: {
      "User-Agent":
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
    },
  })
);

async function scrapeWithSession(url, siteKey) {
  // Initial page load sets cookies
  await client.get(url);

  // Solve CAPTCHA
  const token = await solver.solveRecaptchaV2(siteKey, url);

  // Submit with maintained cookies
  const result = await client.post(
    url,
    new URLSearchParams({ "g-recaptcha-response": token })
  );

  return result.data;
}

Extraindo dados da página com Cheerio

Depois de resolvido o CAPTCHA e enviado o formulário, o Cheerio extrai os dados da resposta com seletores CSS, sem precisar de um navegador. Dados pessoais na coleta? Considere as obrigações da LGPD antes de armazená-los.

function parseResults(html) {
  const $ = cheerio.load(html);
  const items = [];

  $(".result-item").each((_, el) => {
    items.push({
      title: $(el).find(".title").text().trim(),
      url: $(el).find("a").attr("href"),
      description: $(el).find(".description").text().trim(),
    });
  });

  return items;
}

Boas práticas de coleta responsável

Scraping em produção exige mais cuidado que um script de teste local. Antes de apontar isso para um site real, revise três pontos:

  • Rate limit e User-Agent real — mantenha intervalos entre requisições e um User-Agent de navegador atual; picos de tráfego sem intervalo são a causa mais comum do erro ECONNREFUSED na tabela abaixo.
  • Rotação de proxies — distribua as requisições entre proxies para não concentrar tudo em um único IP, principalmente em execuções paralelas com vários workers.
  • LGPD e dados pessoais — se a coleta grava nome, e-mail ou CPF, trate isso como dado pessoal: documente a base legal e defina um prazo de retenção antes de persistir qualquer registro.

Erros comuns e como resolver

Mesmo com o módulo pronto, alguns erros aparecem com frequência assim que o scraper vai para produção. A tabela abaixo cobre as causas mais comuns e a correção direta:

Problema Causa Correção
CAPTCHA_NOT_READY em loop indefinido Sitekey errada ou solve lento Confira a sitekey; aumente o timeout
403 Forbidden no POST Faltam cookies ou headers Reaproveite os cookies; adicione o header Referer
Cheerio não encontra elementos Conteúdo renderizado via JS Use Puppeteer para páginas com JS
ECONNREFUSED Rate limit do site de destino Adicione intervalos e rotacione proxies

Perguntas frequentes

Esse fluxo funciona para reCAPTCHA v3, não só v2?

Sim — use solveRecaptchaV3() com version: "v3" e a action do site; o token vai no mesmo campo do formulário.

Rodando 10 workers em paralelo, de quantas threads da CaptchaAI eu preciso?

Pelo menos 10. O ADVANCE (US$ 90/mês, 50 threads) ou o PREMIUM (US$ 170/mês, 100 threads) evitam fila em picos de concorrência.

Como lidar com sites protegidos por Cloudflare Turnstile?

Chame solver.solveTurnstile(). Para o desafio completo, veja como resolver o desafio da Cloudflare Turnstile em staging, que retorna cookie_qa_validacao.

O CaptchaAI não resolve hCaptcha, certo?

Correto — hCaptcha ainda não está entre os tipos suportados. A API cobre reCAPTCHA v2/v3, Cloudflare Turnstile, GeeTest v3 e CAPTCHAs de imagem/OCR; adapte o scraper para esses tipos antes de automatizar contra um site novo.

Esse fluxo funciona com rotação de proxies?

Sim. Adicione a rotação de proxies no cliente axios entre requisições — o gargalo de escala normalmente é o número de threads liberado pelo seu plano CaptchaAI, não a rotação em si.

Guias relacionados

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