Raspagem de Dados com Node.js, Cheerio e Proxies Rotativos: Guia Completo

Aprenda a construir scrapers em Node.js com Cheerio e axios, integrando proxies residenciais rotativos como interceptador reutilizável, scraping concorrente com p-limit e tratamento robusto de erros 403/429.

Raspagem de Dados com Node.js, Cheerio e Proxies Rotativos: Guia Completo
Neste artigo

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 EUA
  • user-country-DE-city-berlin — IP residencial em Berlim
  • user-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érioCheerio + axiosPuppeteer / Playwright
RenderizaçãoHTML 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
EscalonamentoFácil — centenas concorrentesDifícil — requer fleet headless
Custo de infraBaixo (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:

ModoFormato do UsernameUse Case
Rotação por requisiçãouser-country-USScraping em massa, SERP, preço
Sessão stickyuser-session-abc123-country-USLogin, 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.

Perguntas frequentes

Cheerio consegue executar JavaScript de uma página?

Não. Cheerio é um parser de HTML estático e não executa JavaScript. Se os dados dependem de JS client-side (React, Vue), use Puppeteer ou Playwright com proxies.

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 com p-limit?

Comece com 20-25 concorrentes. Monitore a taxa de erro: abaixo de 5%, aumente gradualmente; acima de 15%, reduza. Use um circuit breaker para pausar automaticamente quando a taxa de erro for alta.

O interceptador de proxy funciona com SOCKS5?

Sim. Use 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.

Pronto para começar?

Proxies residenciais, ISP e móveis em mais de 148 países. Crie uma conta grátis.

Criar conta grátis
← Voltar ao Blog