O Crawlee organiza sessões, proxies e novas tentativas — mas não decifra nenhum CAPTCHA, e não é para isso que ele serve. Quando um spider Crawlee esbarra em um reCAPTCHA v2, quem entra em ação é a CaptchaAI: você extrai o sitekey da página, envia para a API, recebe o token e injeta de volta antes do Crawlee seguir para a próxima URL. Este guia mostra essa integração em três cenários — CheerioCrawler, PlaywrightCrawler e o pool de sessão do Crawlee — com o mesmo código Node.js que roda em produção.
O que o Crawlee já resolve — e o que sobra para a CaptchaAI
As duas ferramentas não competem: o Crawlee cuida do fluxo de scraping, a CaptchaAI resolve o desafio que aparece no meio dele.
| Recurso do Crawlee | Como ajuda quando aparece um CAPTCHA |
|---|---|
| Pool de sessão | Mantém IP e cookies estáveis entre a resolução do token e o envio do formulário |
failedRequestHandler |
Reencaminha a URL para nova tentativa depois que o token é aplicado |
| Rotação de proxy | Compatível com o parâmetro proxy opcional da API da CaptchaAI |
| Fila de requisições | Enfileira a resolução do CAPTCHA no mesmo fluxo do scraping, sem workers separados |
Qual crawler usar quando aparece um CAPTCHA
A escolha do crawler muda onde você lê o sitekey e como injeta o token de volta na página:
| Crawler | Quando usar | E o CAPTCHA? |
|---|---|---|
CheerioCrawler |
Páginas estáticas — o HTML inicial já traz o widget | Extraia o sitekey direto do HTML, sem abrir navegador |
PlaywrightCrawler |
Conteúdo renderizado em JavaScript; o CAPTCHA só existe depois do carregamento | Leia o sitekey do DOM renderizado e injete o token na página real |
PuppeteerCrawler |
Alternativa ao Playwright quando o projeto já usa Puppeteer | Mesmo padrão do PlaywrightCrawler, trocando a API do navegador |
Rodando os workers do Crawlee na região sa-east-1 (São Paulo) da AWS, a latência de rede até o endpoint da CaptchaAI costuma ser pequena perto do próprio tempo de resolução — mas meça no seu ambiente antes de apertar o requestHandlerTimeoutSecs, como nos 180 s usados nos exemplos abaixo. Os três exemplos seguem a ordem da tabela: CheerioCrawler para o caso simples, PlaywrightCrawler para páginas renderizadas e, por fim, o pool de sessão para reaproveitar o token entre requisições.
Integração básica com CheerioCrawler
A função solveCaptcha envia sitekey e pageurl para in.php, aguarda 15 segundos e faz o polling em res.php a cada 5 segundos até o token voltar ou até as 24 tentativas se esgotarem. No requestHandler, o Crawlee detecta o CAPTCHA pelo atributo data-sitekey, chama solveCaptcha e envia o formulário com g-recaptcha-response preenchido antes de seguir para a extração dos dados da tabela:
const { CheerioCrawler } = require('crawlee');
const https = require('https');
const API_KEY = process.env.CAPTCHAAI_API_KEY;
async function solveCaptcha(sitekey, pageurl) {
// Submit task
const submitData = new URLSearchParams({
key: API_KEY,
method: 'userrecaptcha',
googlekey: sitekey,
pageurl: pageurl,
json: '1',
});
const submitResp = await fetch('https://ocr.captchaai.com/in.php', {
method: 'POST',
body: submitData,
});
const submitResult = await submitResp.json();
if (submitResult.status !== 1) {
throw new Error(`Submit error: ${submitResult.request}`);
}
const taskId = submitResult.request;
// Poll for result
await new Promise(r => setTimeout(r, 15000));
for (let i = 0; i < 24; i++) {
const pollResp = await fetch(
`https://ocr.captchaai.com/res.php?key=${API_KEY}&action=get&id=${taskId}&json=1`
);
const pollResult = await pollResp.json();
if (pollResult.status === 1) return pollResult.request;
if (pollResult.request !== 'CAPCHA_NOT_READY') {
throw new Error(`Solve error: ${pollResult.request}`);
}
await new Promise(r => setTimeout(r, 5000));
}
throw new Error('Solve timeout');
}
// Crawlee spider with CAPTCHA handling
const crawler = new CheerioCrawler({
maxConcurrency: 5,
requestHandlerTimeoutSecs: 180,
async requestHandler({ request, $, log }) {
// Check if page has CAPTCHA
const captchaDiv = $('[data-sitekey]');
if (captchaDiv.length > 0) {
const sitekey = captchaDiv.attr('data-sitekey');
log.info(`CAPTCHA found on ${request.url}, solving...`);
const token = await solveCaptcha(sitekey, request.url);
log.info('CAPTCHA solved, submitting form');
// Submit form with token
const formData = new URLSearchParams({
'g-recaptcha-response': token,
});
const resp = await fetch(request.url, {
method: 'POST',
body: formData,
});
const html = await resp.text();
// Parse the result page...
}
// Extract data
const title = $('title').text();
const data = $('table tr').map((i, row) => ({
col1: $(row).find('td:eq(0)').text().trim(),
col2: $(row).find('td:eq(1)').text().trim(),
})).get();
log.info(`Scraped ${data.length} rows from ${request.url}`);
},
failedRequestHandler({ request, log }) {
log.error(`Failed: ${request.url}`);
},
});
// Run
(async () => {
await crawler.run([
'https://example.com/page1',
'https://example.com/page2',
]);
})();
Se as colunas extraídas em data guardarem informação pessoal — nome, e-mail, CPF —, essa coleta entra no escopo da LGPD; defina retenção curta e acesso restrito antes de deixar esse spider rodando sem supervisão.
PlaywrightCrawler com CAPTCHA renderizado em JavaScript
Quando o sitekey só existe depois do JavaScript da página rodar, o CheerioCrawler simplesmente não vê o widget — é preciso o navegador real do PlaywrightCrawler. O exemplo abaixo lê o sitekey do DOM já renderizado, injeta o token no campo oculto do widget e dispara o callback do reCAPTCHA manualmente antes de clicar em enviar:
const { PlaywrightCrawler } = require('crawlee');
const crawler = new PlaywrightCrawler({
maxConcurrency: 3,
requestHandlerTimeoutSecs: 180,
launchContext: {
launchOptions: {
headless: true,
args: ['--disable-blink-features=AutomationControlled'],
},
},
async requestHandler({ request, page, log }) {
await page.goto(request.url, { waitUntil: 'networkidle' });
// Check for reCAPTCHA
const sitekey = await page.evaluate(() => {
const el = document.querySelector('[data-sitekey]');
return el ? el.getAttribute('data-sitekey') : null;
});
if (sitekey) {
log.info(`CAPTCHA detected, solving for ${request.url}`);
const token = await solveCaptcha(sitekey, request.url);
// Inject token
await page.evaluate((t) => {
const ta = document.querySelector('[name="g-recaptcha-response"]');
if (ta) {
ta.style.display = 'block';
ta.value = t;
}
// Trigger callback
const widget = document.querySelector('.g-recaptcha');
if (widget) {
const cb = widget.getAttribute('data-callback');
if (cb && typeof window[cb] === 'function') {
window[cb](t);
}
}
}, token);
await page.click('button[type="submit"]');
await page.waitForNavigation({ waitUntil: 'networkidle' });
}
// Extract data
const title = await page.title();
const content = await page.textContent('body');
log.info(`Page: ${title}, length: ${content.length}`);
},
});
Reaproveitando o token entre requisições da mesma sessão
Com useSessionPool ativado, o Crawlee mantém cada sessão por até 50 usos. Ao resolver o CAPTCHA uma vez, o exemplo grava o token em session.userData junto com o horário — requisições seguintes na mesma sessão podem reaproveitar esse token em vez de chamar a CaptchaAI de novo, desde que ainda esteja dentro da validade:
const { CheerioCrawler, Session } = require('crawlee');
const crawler = new CheerioCrawler({
useSessionPool: true,
sessionPoolOptions: {
maxPoolSize: 10,
sessionOptions: {
maxUsageCount: 50,
},
},
async requestHandler({ request, $, session, log }) {
// If blocked, solve CAPTCHA and mark session as usable
if ($('.captcha-container').length > 0) {
const sitekey = $('[data-sitekey]').attr('data-sitekey');
const token = await solveCaptcha(sitekey, request.url);
// Store token in session for subsequent requests
session.userData = session.userData || {};
session.userData.captchaToken = token;
session.userData.tokenTime = Date.now();
log.info('CAPTCHA solved, session updated');
}
// Normal scraping
const items = $('div.item').map((i, el) => ({
name: $(el).find('.name').text().trim(),
price: $(el).find('.price').text().trim(),
})).get();
log.info(`Found ${items.length} items`);
},
});
Perguntas frequentes
O Crawlee resolve CAPTCHA sem nenhuma API externa?
Não. O Crawlee cuida de sessão, proxy e novas tentativas, mas a resolução em si depende de um serviço como a CaptchaAI — o framework não decifra reCAPTCHA, Turnstile ou nenhum outro desafio sozinho.
CheerioCrawler ou PlaywrightCrawler: qual escolher quando o CAPTCHA aparece só depois do JavaScript carregar?
PlaywrightCrawler. Se o sitekey não está no HTML inicial — só surge depois do JavaScript da página rodar —, o CheerioCrawler simplesmente não vê o widget. PuppeteerCrawler resolve o mesmo problema com uma API de navegador diferente.
O polling em res.php trava o requestHandler do Crawlee?
Só a requisição que está resolvendo o CAPTCHA. Como o Crawlee processa cada requestHandler de forma independente, as demais URLs continuam avançando em paralelo até o limite definido em maxConcurrency — os exemplos deste guia usam de 3 a 5.
Quantas threads da CaptchaAI preciso para acompanhar o maxConcurrency do Crawlee?
Combine os dois números. Um maxConcurrency: 5 no Crawlee já cabe no plano BASIC (US$ 15/mês, 5 threads); para maxConcurrency mais alto, o STANDARD (US$ 30/mês, 15 threads) e o ADVANCE (US$ 90/mês, 50 threads) escalam sem mudar o código.
Posso reaproveitar o token resolvido em mais de uma requisição da mesma sessão?
Sim, enquanto a sessão continuar válida no site de destino — é para isso que serve o session.userData no exemplo com useSessionPool. Ainda assim, trate isso como cache de curta duração: o token expira, e o site pode invalidar a sessão a qualquer momento.
Guias relacionados
- Scrapy Spider Middleware para a CaptchaAI
- Como montar uma estrutura de scraping personalizada com a CaptchaAI
Resolva o primeiro CAPTCHA do seu spider Crawlee ainda hoje — crie sua chave da CaptchaAI.