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-Agentde navegador atual; picos de tráfego sem intervalo são a causa mais comum do erroECONNREFUSEDna 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.