Por Que Cheerio + axios É a Combinação Leve Perfeita para Scraping
Se você está raspando páginas com HTML estático, não precisa de um navegador headless pesado. Cheerio analisa HTML no servidor com uma API jQuery familiar, e axios faz requisições HTTP rápidas. Juntos, pesam frações de um Puppeteer e rodam em um container com 256 MB de RAM.
O problema? Sites bloqueiam IPs que fazem centenas de requisições. É aí que entra a integração com proxies Cheerio — especificamente, proxies residenciais rotativos que trocam de IP a cada requisição ou sessão.
Neste guia, você vai implementar:
- Scraping básico com axios + Cheerio
- Configuração de proxy com axios e
https-proxy-agent - Um interceptador axios reutilizável para rotação de proxies residenciais
- Scraping concorrente com
p-limit - Um exemplo real de e-commerce com 10k URLs
- Tratamento de erros com circuit breaker
Scraping Básico: axios + Cheerio em 10 Linhas
Cheerio funciona parseando o HTML que o axios retorna. Para páginas renderizadas no servidor (SSR), isso é tudo que você precisa:
const axios = require('axios');
const cheerio = require('cheerio');
async function scrapeProduct(url) {
const { data } = await axios.get(url);
const $ = cheerio.load(data);
return {
title: $('h1.product-title').text().trim(),
price: $('span.price-current').text().trim(),
availability: $('span.stock-status').text().trim(),
};
}
scrapeProduct('https://store.example.com/product/123')
.then(console.log)
.catch(console.error);Simples, certo? Mas sem proxy, seu IP será bloqueado após dezenas de requisições. Vamos corrigir isso.
Configurando Proxies com axios
O axios suporta proxies nativamente via proxy na configuração, mas para HTTPS você precisa do https-proxy-agent. Vamos ver ambas as abordagens.
Opção 1: Proxy Config Nativo do axios (HTTP apenas)
const axios = require('axios');
const client = axios.create({
proxy: {
protocol: 'http',
host: 'gate.proxyhat.com',
port: 8080,
auth: {
username: 'user-country-US',
password: 'PASSWORD',
},
},
});
// Requisição roteada via proxy residencial nos EUA
const { data } = await client.get('https://store.example.com/product/123');Opção 2: https-proxy-agent (HTTP + HTTPS)
Para targets HTTPS — o caso mais comum —, use https-proxy-agent como httpsAgent:
const axios = require('axios');
const { HttpsProxyAgent } = require('https-proxy-agent');
const agent = new HttpsProxyAgent('http://user-country-US:PASSWORD@gate.proxyhat.com:8080');
const client = axios.create({ httpsAgent: agent });
const { data } = await client.get('https://store.example.com/product/123');Com o ProxyHat, a geolocalização e o controle de sessão vão direto no campo username:
user-country-US— IP residencial nos EUAuser-country-DE-city-berlin— IP residencial em Berlimuser-session-abc123— sessão sticky (mesmo IP por até 30 min)
Quando Cheerio É Suficiente vs. Quando Você Precisa de Headless
Nem todo site precisa de Puppeteer. Aqui está a diferença:
| Critério | Cheerio + axios | Puppeteer / Playwright |
|---|---|---|
| Renderização | HTML estático (SSR) | JavaScript client-side (SPA) |
| Velocidade | ~50ms por página | ~2-5s por página |
| Memória | ~30-50 MB | ~300-500 MB por instância |
| Escalonamento | Fácil — centenas concorrentes | Difícil — requer fleet headless |
| Custo de infra | Baixo (1 vCPU, 512 MB) | Alto (2+ vCPU, 2+ GB) |
| Cobertura de sites | ~60-70% da web | ~95% da web |
Regra prática: Se curl ou axios retorna o HTML completo que você vê no navegador com JS desabilitado, Cheerio basta. Se os dados só aparecem após JavaScript executar, você precisa de headless.
Dica: Abra o site, desabilite JavaScript no DevTools e recarregue. Os dados ainda estão lá? Cheerio funciona. Sumiram? Hora de usar Puppeteer com proxies.
Interceptador axios para Rotação de Proxies Residenciais
Em vez de espalhar lógica de proxy por todo o código, vamos criar um interceptador axios reutilizável que rotaciona IPs automaticamente e faz retry com IPs diferentes em caso de bloqueio.
const axios = require('axios');
const { HttpsProxyAgent } = require('https-proxy-agent');
class ProxyRotator {
constructor({ username, password, host = 'gate.proxyhat.com', port = 8080, maxRetries = 3 }) {
this.username = username;
this.password = password;
this.host = host;
this.port = port;
this.maxRetries = maxRetries;
}
// Gera agent com geo-targeting e sessão aleatória por requisição
createAgent(country = null, session = null) {
let user = this.username;
if (country) user = `${user}-country-${country}`;
if (session) user = `${user}-session-${session}`;
else user = `${user}-session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
return new HttpsProxyAgent(
`http://${user}:${this.password}@${this.host}:${this.port}`
);
}
// Interceptador axios: rota IP por requisição, retry em 403/429
interceptor() {
return async (config) => {
const country = config.metadata?.country || null;
config.httpsAgent = this.createAgent(country);
config.httpAgent = config.httpsAgent;
return config;
};
}
createClient() {
const client = axios.create({ timeout: 15000 });
client.interceptors.request.use(this.interceptor());
// Retry automático em 403/429 com novo IP
client.interceptors.response.use(null, async (error) => {
const { config, response } = error;
if (!config) return Promise.reject(error);
const status = response?.status;
if ((status === 403 || status === 429) && (!config.__retryCount || config.__retryCount < this.maxRetries)) {
config.__retryCount = (config.__retryCount || 0) + 1;
// Novo IP = nova sessão no proxy
const country = config.metadata?.country || null;
config.httpsAgent = this.createAgent(country);
config.httpAgent = config.httpsAgent;
console.warn(`Retry ${config.__retryCount}/${this.maxRetries} — status ${status}, rotating IP`);
// Backoff exponencial
await new Promise(r => setTimeout(r, 1000 * 2 ** config.__retryCount));
return client(config);
}
return Promise.reject(error);
});
return client;
}
}
// Uso:
const rotator = new ProxyRotator({ username: 'user', password: 'PASSWORD' });
const client = rotator.createClient();
// Cada requisição usa um IP residencial diferente
const html = await client.get('https://store.example.com/product/123', {
metadata: { country: 'US' },
});Esse padrão é framework-idiomático: o interceptador encapsula toda a lógica de proxy, e o consumer do client nunca precisa saber sobre proxies. Basta passar metadata.country quando necessário.
Scraping Concorrente com p-limit
Scraping sequencial de 10.000 URLs levaria horas. p-limit controla a concorrência sem estourar memória:
const pLimit = require('p-limit');
const cheerio = require('cheerio');
// Limitar a 20 requisições concorrentes para não sobrecarregar
const limit = pLimit(20);
async function scrapePage(client, url) {
try {
const { data } = await client.get(url);
const $ = cheerio.load(data);
return {
url,
title: $('h1').text().trim(),
price: $('span.price').text().trim(),
status: 'success',
};
} catch (err) {
return { url, status: 'error', error: err.message };
}
}
async function scrapeBatch(client, urls) {
const tasks = urls.map(url => limit(() => scrapePage(client, url)));
const results = await Promise.all(tasks);
return results;
}
// Executar:
const urls = Array.from({ length: 10000 }, (_, i) => `https://store.example.com/product/${i + 1}`);
const results = await scrapeBatch(client, urls);
const successes = results.filter(r => r.status === 'success');
const errors = results.filter(r => r.status === 'error');
console.log(`Sucesso: ${successes.length}, Erro: ${errors.length}`);Exemplo Real: E-commerce com 10k URLs e Rotação de Proxies
Vamos juntar tudo — interceptador de proxy, Cheerio, concorrência e circuit breaker — em um scraper de produção:
const fs = require('fs');
const pLimit = require('p-limit');
const cheerio = require('cheerio');
class CircuitBreaker {
constructor({ threshold = 0.5, window = 100, cooldown = 30000 }) {
this.threshold = threshold; // 50% de falhas
this.window = window; // últimas 100 requisições
this.cooldown = cooldown; // 30s de pausa
this.results = [];
this.openUntil = 0;
}
record(success) {
this.results.push(success ? 1 : 0);
if (this.results.length > this.window) this.results.shift();
}
isOpen() {
if (Date.now() < this.openUntil) return true;
const failures = this.results.filter(r => r === 0).length;
if (this.results.length >= 10 && failures / this.results.length > this.threshold) {
this.openUntil = Date.now() + this.cooldown;
console.error(`⚠ Circuit breaker OPEN — ${failures}/${this.results.length} falhas. Pausando ${this.cooldown / 1000}s`);
return true;
}
return false;
}
}
async function runEcommerceScraper() {
const rotator = new ProxyRotator({
username: 'user',
password: 'PASSWORD',
maxRetries: 3,
});
const client = rotator.createClient();
const breaker = new CircuitBreaker({ threshold: 0.4, window: 50, cooldown: 30000 });
const limit = pLimit(25);
// Gerar lista de 10k URLs de produto
const urls = Array.from({ length: 10000 }, (_, i) =>
`https://store.example.com/product/${i + 1}`
);
async function scrapeProduct(url) {
if (breaker.isOpen()) {
return { url, status: 'skipped', reason: 'circuit_open' };
}
try {
const { data } = await client.get(url, {
metadata: { country: 'US' },
});
const $ = cheerio.load(data);
const result = {
url,
title: $('h1.product-title').text().trim(),
price: $('span.price-current').text().trim(),
sku: $('[data-sku]').attr('data-sku'),
availability: $('span.stock-status').text().trim(),
status: 'success',
};
breaker.record(true);
return result;
} catch (err) {
breaker.record(false);
return {
url,
status: 'error',
error: err.response?.status || err.message,
};
}
}
// Processar em batches para salvar progresso incremental
const BATCH_SIZE = 500;
for (let i = 0; i < urls.length; i += BATCH_SIZE) {
const batch = urls.slice(i, i + BATCH_SIZE);
const tasks = batch.map(url => limit(() => scrapeProduct(url)));
const results = await Promise.all(tasks);
// Salvar resultados incrementais
const successResults = results.filter(r => r.status === 'success');
fs.appendFileSync(
'products.jsonl',
successResults.map(r => JSON.stringify(r)).join('\n') + '\n'
);
const errorCount = results.filter(r => r.status === 'error').length;
console.log(
`Batch ${Math.floor(i / BATCH_SIZE) + 1}/${Math.ceil(urls.length / BATCH_SIZE)} — ` +
`${successResults.length} OK, ${errorCount} erros`
);
}
}
runEcommerceScraper().catch(console.error);Tratamento de Erros: 403, 429 e Circuit Breaker
Erros em scraping seguem padrões previsíveis. Aqui está como lidar com cada um:
403 Forbidden
O site detectou seu padrão e bloqueou o IP. Solução: rotacionar IP via nova sessão no proxy. O interceptador já faz isso automaticamente com retry.
429 Too Many Requests
Você está fazendo requisições rápido demais. Solução: reduzir concorrência e adicionar backoff. O retry no interceptador já usa backoff exponencial.
Timeouts
O proxy ou o site está lento. Solução: timeout agressivo (10-15s) e retry com IP diferente.
Circuit Breaker
Quando a taxa de erros ultrapassa o limiar, o circuit breaker pausa todas as requisições por um período de cooldown. Isso evita desperdiçar proxies e acelerar bloqueios. No código acima, o breaker abre quando mais de 40% das últimas 50 requisições falham.
Regra de ouro: se mais de 30-40% das requisições estão falhando, pare, espere e ajuste a concorrência. Continuar batendo só piora o bloqueio.
Estratégias de Rotação de IP com ProxyHat
O ProxyHat oferece dois modos de rotação que se integram ao interceptador:
| Modo | Formato do Username | Use Case |
|---|---|---|
| Rotação por requisição | user-country-US | Scraping em massa, SERP, preço |
| Sessão sticky | user-session-abc123-country-US | Login, multi-step, carrinho |
Para scraping em massa de e-commerce, rotação por requisição é o padrão. Cada GET recebe um IP residencial diferente — impossível de fingerprintar.
Para fluxos que exigem estado (login, navegação por páginas de categoria), use sessão sticky. O IP permanece o mesmo por até 30 minutos:
// Sessão sticky — mesmo IP por 30 min
const stickyClient = rotator.createClient();
const sessionId = `order-${Date.now()}`;
// Todas essas requisições usam o mesmo IP residencial
await stickyClient.get('https://store.example.com/login', {
metadata: { country: 'US', session: sessionId },
});
await stickyClient.get('https://store.example.com/cart', {
metadata: { country: 'US', session: sessionId },
});Escalonamento: Containers, Filas e Persistência
Para 10k+ URLs em produção, você precisa de mais do que concorrência no Node.js:
- Containerização: Empacote o scraper em Docker. Rode múltiplas instâncias com
docker-compose scale scraper=5. - Fila de mensagens: Use Redis + BullMQ para distribuir URLs entre workers. Cada worker processa um chunk.
- Persistência incremental: Salve resultados em JSONL (como no exemplo) ou diretamente em um banco. Nunca armazene tudo em memória.
- Monitoramento: Logue taxa de sucesso, latência média e status do circuit breaker. Alerta quando o breaker abrir.
Arquitetura típica:
[Redis Queue] → [Worker 1 (25 concurrent)] → [JSONL / DB]
[Worker 2 (25 concurrent)] → [JSONL / DB]
[Worker 3 (25 concurrent)] → [JSONL / DB]Pontos-chave
- Cheerio + axios é suficiente para ~60-70% dos sites — páginas com HTML estático ou SSR.
- Proxies residenciais rotativos são obrigatórios para scraping em escala. O interceptador axios encapsula toda a lógica.
- Rotação por requisição para scraping em massa; sessão sticky para fluxos com estado.
- p-limit controla concorrência sem explodir memória — 20-30 concorrentes é um bom começo.
- Circuit breaker pausa o scraping quando a taxa de erro é alta, evitando desperdício de proxies.
- Salve incrementalmente — nunca armazene 10k resultados em memória antes de persistir.
- Confira os planos do ProxyHat para proxies residenciais com rotação automática e geo-targeting por país/cidade.
Perguntas Frequentes
Cheerio consegue executar JavaScript de uma página?
Não. Cheerio é um parser de HTML estático. Ele não executa JavaScript. Se os dados dependem de JS client-side (React, Vue), use Puppeteer ou Playwright com proxies para scraping headless.
Qual a diferença entre proxy datacenter e residencial para scraping?
Proxies datacenter são rápidos e baratos, mas fáceis de detectar. Proxies residenciais usam IPs de ISPs reais, tornando cada requisição indistinguível de um usuário normal. Para scraping em escala contra sites com anti-bot, residenciais são essenciais.
Quantas requisições concorrentes devo usar?
Comece com 20-25 concorrentes com p-limit. Monitore a taxa de erro. Se estiver abaixo de 5%, aumente gradualmente. Se passar de 15%, reduza. O circuit breaker ajuda a ajustar automaticamente.
O interceptador de proxy funciona com SOCKS5?
Sim. Basta usar socks-proxy-agent no lugar de https-proxy-agent e a URL socks5://user:pass@gate.proxyhat.com:1080. A lógica do interceptador é idêntica.
Como lidar com CAPTCHAs ao usar Cheerio?
Cheerio não renderiza páginas, então CAPTCHAs baseados em JS não são acionados. Se o site retorna CAPTCHA no HTML, use sessões sticky para manter um IP limpo e reduza a concorrência. Para CAPTCHAs persistentes, considere um serviço de resolução ou mude para Puppeteer.






