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

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.

Pronto para começar?

Acesse mais de 50M de IPs residenciais em mais de 148 países com filtragem por IA.

Ver preçosProxies residenciais
← Voltar ao Blog