Scraping por linguagem 21 min de leitura

Web scraping em JavaScript: guia completo do básico ao avançado

Web scraping com JavaScript e Node.js: axios, cheerio, Puppeteer e Playwright, de páginas simples à dinâmica complexa.

EW
Equipe Web-Scraping.biz
Coleta de dados para as demandas do negócio
Publicado: 28 março 2025

Guia abrangente sobre como baixar páginas com Node.js, extrair delas os dados e escalar tudo isso até um scraper de produção: codificações, multithreading e concorrência, proxies, TOR, SSL, cookies, cabeçalhos, filas de URLs e outras armadilhas. Com links para as bibliotecas oficiais.


Sumário

  1. O que é web scraping e do que ele se compõe
  2. Como baixamos a página: clientes HTTP
  3. Bibliotecas para parsear o conteúdo
  4. Obter o código de status e outros cabeçalhos
  5. Solução de problemas de codificação de caracteres
  6. Trabalho com cookies
  7. Trabalho com HTTPS / SSL
  8. Uso de proxies
  9. Scraping via TOR
  10. Multi: multithreading e concorrência
  11. Armazenamento de URLs e filas (panorama)
  12. Frameworks prontos para usar
  13. Anti-bots, robots.txt, novas tentativas (o que costuma ser esquecido)
  14. Principais vantagens e desvantagens da implementação em JavaScript

1. O que é web scraping e do que ele se compõe

O scraping de um site quase sempre se decompõe em três camadas independentes, e o mais cômodo é projetar o scraper exatamente por essas camadas:

  1. Transporte — como obter os bytes da página (cliente HTTP ou navegador headless).
  2. Extração — como tirar do HTML/JSON os campos necessários (parser do DOM, seletores).
  3. Orquestração — como percorrer muitas URLs sem acabar bloqueado: filas, concorrência, proxies, novas tentativas, deduplicação.

Todo o guia vai do simples ao complexo: primeiro «baixar uma página», no final «um crawler distribuído e robusto».

Uma bifurcação importante logo de início:

  • Site estático (os dados já estão no HTML) → basta um cliente HTTP + um parser do DOM. Rápido, barato, milhares de páginas por minuto.
  • Site dinâmico (os dados são carregados pelo JavaScript) → é preciso ou um navegador headless (Playwright / Puppeteer), ou engenharia reversa da API interna do site (muitas vezes os dados estão em um endpoint JSON e o navegador é dispensável).

Antes de recorrer a um navegador pesado, verifique sempre a aba Network do DevTools: se a página busca os dados em /api/... e recebe JSON, o que deve ser parseado é esse JSON, não o DOM renderizado.


2. Como baixamos a página: clientes HTTP

2.1. fetch nativo (Node 18+): a opção padrão

Desde o Node.js 18, o fetch vem integrado de forma global, é estável desde o Node 21 e tem suporte nas linhas LTS 22 e 24. Por baixo dos panos ele roda sobre o undici, então pacotes à parte como o node-fetch já não são necessários para as tarefas básicas.

javascript
const res = await fetch('https://example.com');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const html = await res.text();

Duas coisas em que todo iniciante tropeça:

  • O fetch não lança exceção com 404/500: é preciso verificar res.ok por conta própria.
  • O fetch não tem timeout por padrão: um socket travado pode ficar assim para sempre. Configure AbortSignal.timeout():
javascript
const res = await fetch(url, { signal: AbortSignal.timeout(15_000) });

2.2. undici diretamente: quando é preciso o máximo de velocidade

O undici é justamente o «motor» do fetch nativo, mas sua API de baixo nível (request, pools de conexões, pipelining) supera nos benchmarks o fetch, o axios e o got por um fator de várias vezes. Faz sentido quando você bateu no teto de desempenho.

javascript
import { request } from 'undici';
const { statusCode, headers, body } = await request('https://example.com');
const html = await body.text();

2.3. got e got-scraping: praticidade + «camuflagem de navegador»

O got é um cliente maduro com novas tentativas integradas, hooks, suporte a cookie jar e HTTP/2.

Para o scraping, mais interessante é o fork got-scraping da Apify: ele gera automaticamente cabeçalhos de navegador verossímeis e na ordem correta, o que reduz a probabilidade de bloqueio. É exatamente ele que o CheerioCrawler usa no Crawlee.

javascript
import { gotScraping } from 'got-scraping';
const { body } = await gotScraping({ url: 'https://example.com' });

2.4. axios: se você precisa de interceptors e de uma API familiar

O axios (repositório) segue sendo o cliente mais popular graças aos interceptors, ao manuseio prático de proxies e ao parsing automático de JSON. Para scraping ele não é mais rápido que o fetch, mas seu ecossistema (por exemplo, o axios-retry) economiza tempo.

2.5. Outros

  • ky — wrapper fino sobre o fetch com padrões razoáveis (novas tentativas, timeouts).
  • node-fetchlegado, só necessário em versões muito antigas do Node; nas modernas use o fetch integrado.
  • Os módulos nativos http/https — controle máximo, mas muito encanamento manual; normalmente só aparecem por baixo dos agentes e proxies.

O que escolher

Cenário Recomendação
A maioria das tarefas, Node 18+ fetch nativo
Milhares de requisições, prioridade ao desempenho undici (request/Pool)
Camuflagem de cabeçalhos pronta para usar got-scraping
Interceptors, API familiar, base de código legada axios
Site dinâmico com renderização JS Playwright / Puppeteer (veja §3.5)

3. Bibliotecas para parsear o conteúdo

Obtida a string HTML, é preciso convertê-la em dados. HTML não se parseia com expressões regulares: é frágil e quebra na primeira tag aninhada. Use um parser de verdade.

3.1. Cheerio: o padrão para conteúdo estático

O Cheerio (repositório) é um parser rápido do lado do servidor com uma API no estilo jQuery. Ele não executa JS nem renderiza: apenas constrói a árvore e permite percorrê-la com seletores. Ideal em combinação com fetch/got.

javascript
import * as cheerio from 'cheerio';

const html = await (await fetch('https://example.com/products')).text();
const $ = cheerio.load(html);

const items = $('.product-card').map((_, el) => ({
  title: $(el).find('.title').text().trim(),
  price: $(el).find('.price').text().trim(),
  url: new URL($(el).find('a').attr('href'), 'https://example.com').href,
})).get();

3.2. jsdom: um DOM quase de verdade

O jsdom implementa uma parte considerável do DOM de navegador e pode até executar os scripts da página. É mais pesado que o Cheerio, mas oferece os familiares querySelectorAll e document, e é útil quando se precisa de uma API do DOM mais «autêntica».

3.3. Alternativas leves e rápidas

  • node-html-parser — muito rápido, com seletores CSS.
  • htmlparser2 — parser de baixo nível em streaming (é sobre ele que o Cheerio é construído).
  • parse5 — parser HTML5 fiel à especificação.
  • linkedom — alternativa leve ao jsdom com API do DOM.

3.4. Extração por «receitas»

O x-ray permite descrever a extração de forma declarativa (seletor → campo) e já percorrer a paginação. Prático para protótipos.

3.5. Dinâmica: Playwright e Puppeteer

Quando o conteúdo é desenhado pelo JS, é preciso um navegador headless:

  • Playwright (repositório) — o favorito moderno: Chromium, Firefox e WebKit com uma única API, esperas automáticas por elementos, interceptação de requisições de rede, contextos para isolar cookies.
  • Puppeteer (repositório) — o padrão de fato para Chrome/Chromium, um pouco mais simples, com um ecossistema enorme.
javascript
import { chromium } from 'playwright';

const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle' });
const titles = await page.$$eval('h2', els => els.map(e => e.textContent.trim()));
await browser.close();

O navegador é o caminho mais caro em recursos: dezenas ou centenas de MB de RAM por aba. Use-o somente quando de fato não há HTML estático nem API interna.

Dica sobre a abordagem híbrida: muitas vezes o ideal é abrir a página no navegador uma única vez, extrair o HTML já renderizado com page.content() e continuar a análise com o veloz Cheerio: assim você combina a renderização JS com a praticidade dos seletores.


4. Obter o código de status e outros cabeçalhos

O status e os cabeçalhos são metade do diagnóstico de um scraper (bloqueio, redirecionamento, limite, codificação).

Com o fetch nativo:

javascript
const res = await fetch(url, { redirect: 'follow' });

res.status;        // 200, 404, 429, 503 ...
res.statusText;    // 'OK', 'Too Many Requests'
res.ok;            // true com 2xx
res.redirected;    // houve redirecionamentos ou não
res.url;           // URL final após os redirecionamentos

res.headers.get('content-type');   // text/html; charset=windows-1252
res.headers.get('set-cookie');     // cookies
res.headers.get('retry-after');    // quanto esperar com 429/503
[...res.headers];                  // todos os cabeçalhos em pares

Bons hábitos:

  • 429 / 503 → leia o Retry-After e aplique backoff, em vez de continuar martelando.
  • 301/302/308 → decida se vai seguir o redirecionamento (redirect: 'manual' dá controle manual).
  • Content-Type com charset= → a primeira e principal fonte de verdade sobre a codificação (veja §5).
  • Gerenciar os cabeçalhos de saída (User-Agent, Accept-Language, Referer) não é menos importante: muitos sites cortam as requisições sem um User-Agent verossímil.

No got/axios, tudo isso está disponível como response.statusCode e response.headers. No navegador, pela interceptação da resposta: page.on('response', res => res.status()).


5. Solução de problemas de codificação de caracteres

Um clássico dos sites antigos: a página está em windows-1252 (ou ISO-8859-1) e você recebe caracteres quebrados do tipo informação em vez de «informação». A causa: res.text() sempre decodifica os bytes como UTF-8, mas o site os enviou em outra codificação.

Regra: com páginas que não estão em UTF-8 não dá para usar res.text(). Pegue os bytes crus (arrayBuffer) e decodifique-os com a codificação correta usando o iconv-lite.

javascript
import iconv from 'iconv-lite';

const res = await fetch('https://site-antigo.example/');
const buf = Buffer.from(await res.arrayBuffer());

// 1) tentamos descobrir a codificação pelo cabeçalho Content-Type
let charset = (res.headers.get('content-type') || '').match(/charset=([^;]+)/i)?.[1];

// 2) se não estiver no cabeçalho, procuramos no <meta> (decodificamos o trecho como latin1 para ler a tag)
if (!charset) {
  const head = iconv.decode(buf, 'latin1');
  charset = head.match(/<meta[^>]+charset=["']?([\w-]+)/i)?.[1]
         || head.match(/charset=([\w-]+)/i)?.[1];
}

charset = (charset || 'utf-8').toLowerCase().replace('windows-', 'win');

const html = iconv.decode(buf, charset); // acentos e cedilhas corretos

Se a codificação não está declarada em lugar nenhum, é possível detectá-la de forma heurística:

  • jschardet — port do Universal Charset Detector da Mozilla.
  • chardet — detector alternativo.
javascript
import jschardet from 'jschardet';
const guess = jschardet.detect(buf); // { encoding: 'windows-1252', confidence: 0.99 }

Além disso:

  • O Cheerio sabe decodificar sozinho se receber o buffer e uma dica: cheerio.load(buf, { decodeEntities: true }), mas um iconv.decode explícito é mais confiável.
  • Em um navegador headless o problema de codificação normalmente não existe: o navegador decodifica a página por conta própria e page.content() devolve UTF-8 correto.
  • Não esqueça as entidades HTML (&nbsp;, &atilde;): parsers decentes (Cheerio, parse5) as decodificam para você.

Cookies são necessários para as áreas com autenticação, as sessões, os carrinhos e para pular as telas de «primeira visita». Há três níveis.

6.1. À mão, via cabeçalhos

javascript
const res = await fetch(url, { headers: { cookie: 'sid=abc123; lang=pt' } });
const setCookie = res.headers.get('set-cookie'); // parseá-la e reenviá-la na requisição seguinte

Serve para casos simples, mas manter à mão o conjunto de cookies entre requisições é um suplício.

O tough-cookie é a implementação de referência de um armazém de cookies que respeita domínio, caminho, expiração e flags. Muitos clientes se integram a ele de fábrica.

O got aceita o jar diretamente e cuida da sessão sozinho:

javascript
import got from 'got';
import { CookieJar } from 'tough-cookie';

const cookieJar = new CookieJar();
await got('https://example.com/login', { cookieJar, method: 'POST', form: { user, pass } });
const profile = await got('https://example.com/account', { cookieJar }); // os cookies são adicionados sozinhos

Para o axios existe o wrapper axios-cookiejar-support; com o fetch nativo será preciso plugar o tough-cookie à mão ou usar got/undici.

6.3. No navegador

No Playwright/Puppeteer os cookies vivem no contexto e podem ser salvos e restaurados, algo muito prático para fazer login uma única vez e reutilizar a sessão:

javascript
// salvar o estado (cookies + localStorage)
await context.storageState({ path: 'state.json' });
// restaurá-lo em uma nova execução
const context = await browser.newContext({ storageState: 'state.json' });

7. Trabalho com HTTPS / SSL

Um site HTTPS normal não exige esforço nenhum: fetch/got/axios verificam o certificado automaticamente. Casos especiais:

7.1. Certificados autoassinados ou expirados

Às vezes é preciso desativar a verificação (por exemplo, ao trabalhar através de um proxy MITM ou com um ambiente de testes). Faça isso com conhecimento de causa: elimina a proteção contra a falsificação do tráfego.

javascript
// undici / fetch nativo — via dispatcher
import { Agent, setGlobalDispatcher } from 'undici';
setGlobalDispatcher(new Agent({ connect: { rejectUnauthorized: false } }));

// got / axios — via https.Agent
import https from 'node:https';
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
// got:  got(url, { agent: { https: httpsAgent } })
// axios: axios.get(url, { httpsAgent })

O «machadaço» global NODE_TLS_REJECT_UNAUTHORIZED=0 desativa a verificação para o processo inteiro; melhor não fazer isso em produção.

7.2. Certificados raiz próprios / certificados de cliente (mTLS)

javascript
import https from 'node:https';
import fs from 'node:fs';

const agent = new https.Agent({
  ca:  fs.readFileSync('./ca.pem'),     // autoridade certificadora própria
  cert: fs.readFileSync('./client.pem'), // certificado de cliente para mTLS
  key:  fs.readFileSync('./client.key'),
});

7.3. Impressão digital TLS (JA3): anti-bots avançado

As proteções modernas (Cloudflare, DataDome) sabem distinguir os clientes pelo handshake TLS (JA3/JA4): o de um cliente Node não é igual ao de um Chrome de verdade, e isso entrega o bot mesmo com cabeçalhos impecáveis. Node puro não consegue «consertar» isso; ajudam:

  • got-scraping — camufla parcialmente a camada de cabeçalhos;
  • CycleTLS — falsificação da impressão digital TLS;
  • um navegador headless de verdade (Playwright) — traz o handshake TLS «real» de um navegador.

8. Uso de proxies

Os proxies servem para distribuir a carga entre vários IPs, contornar restrições geográficas e bloqueios por IP. Tipos: HTTP, HTTPS e SOCKS5 (este último é o mais universal: transporta qualquer tráfego e também o DNS).

8.1. fetch nativo (particularidade importante de 2026!)

O fetch nativo não tem a velha opção { agent }. O proxy é configurado pelo dispatcher do undici, o ProxyAgent:

javascript
import { ProxyAgent, setGlobalDispatcher } from 'undici';

// globalmente: todos os fetch passarão pelo proxy
setGlobalDispatcher(new ProxyAgent('http://user:pass@proxy.host:8080'));
const res = await fetch('https://example.com');

// ou de forma pontual, para uma única requisição
const res2 = await fetch('https://example.com', {
  dispatcher: new ProxyAgent('http://user:pass@proxy.host:8080'),
});

No Node 24+ é possível ativar a leitura de HTTP_PROXY/HTTPS_PROXY do ambiente com a flag NODE_USE_ENV_PROXY=1 (ou --use-env-proxy), mas um ProxyAgent explícito é mais confiável.

8.2. got / axios via agentes

Com os agentes https-proxy-agent e socks-proxy-agent:

javascript
import got from 'got';
import { HttpsProxyAgent } from 'https-proxy-agent';
import { SocksProxyAgent } from 'socks-proxy-agent';

const httpsAgent  = new HttpsProxyAgent('http://user:pass@proxy:8080');
const socksAgent  = new SocksProxyAgent('socks5h://127.0.0.1:9050'); // h = DNS através do proxy

const r1 = await got('https://example.com', { agent: { https: httpsAgent } });
const r2 = await got('https://example.com', { agent: { http: socksAgent, https: socksAgent } });

8.3. Rotação e pools de proxies

Para escalar é preciso um pool de proxies com rotação e descarte dos endereços «mortos». A variante mais simples: escolher um proxy aleatório ou por round-robin a cada requisição. Ferramentas prontas:

  • proxy-chain da Apify — levanta um proxy local que encaminha ao upstream (inclusive com autenticação, algo importante para o Chromium, que não aceita usuário/senha em --proxy-server).
  • No Crawlee a rotação de proxies e sessões vem integrada (ProxyConfiguration).
javascript
// um proxy aleatório do pool a cada requisição
const pool = ['http://p1:8080', 'http://p2:8080', 'http://p3:8080'];
const pick = () => pool[Math.floor(Math.random() * pool.length)];
await fetch(url, { dispatcher: new ProxyAgent(pick()) });

Tipos de proxy por qualidade: datacenter (barato, fácil de detectar) → residencialmóvel (caro, quase nunca bloqueado). A escolha depende da agressividade da proteção do alvo.


9. Scraping via TOR

O TOR oferece rotação de IP gratuita: o tráfego passa por uma cadeia de nós e é possível trocar o IP de saída sob demanda. É útil para aprender e para tarefas pequenas, mas a abordagem tem limitações sérias (veja o final da seção).

9.1. Configuração

O TOR levanta um proxy SOCKS na porta 9050 e uma porta de controle 9051 para gerenciá-lo. No arquivo de configuração torrc:

code
SocksPort 9050
ControlPort 9051
# a senha é gerada com o comando: tor --hash-password "sua_senha"
HashedControlPassword 16:....
CookieAuthentication 1

9.2. Requisições via TOR

Basta apontar o cliente para o SOCKS5 local (use socks5h para que também o DNS seja resolvido através do TOR; caso contrário o IP real vaza):

javascript
import got from 'got';
import { SocksProxyAgent } from 'socks-proxy-agent';

const agent = new SocksProxyAgent('socks5h://127.0.0.1:9050');
const res = await got('https://httpbin.org/ip', {
  agent: { http: agent, https: agent },
});
console.log(JSON.parse(res.body).origin); // IP de saída atual do TOR

9.3. Troca do IP de saída (identity)

Para obter um novo IP de saída, envia-se à porta de controle o sinal NEWNYM. Dá para fazer isso com a biblioteca tor-request ou à mão, com um socket TCP comum e sem dependências:

javascript
import net from 'node:net';

function newTorIdentity(password = '') {
  return new Promise((resolve, reject) => {
    const socket = net.connect(9051, '127.0.0.1', () => {
      socket.write(`AUTHENTICATE "${password}"\r\nSIGNAL NEWNYM\r\nQUIT\r\n`);
    });
    socket.once('error', reject);
    socket.once('end', resolve);
    socket.resume();
  });
}

// entre as requisições:
await newTorIdentity('sua_senha');

Importante: o TOR mantém um intervalo de resfriamento de ~10 segundos entre trocas de circuito; não será possível rotacionar o IP com mais frequência.

9.4. Várias instâncias para aumentar a vazão

Um TOR = um único IP de saída a cada momento e um resfriamento lento. Para montar um pool de «proxies gratuitos», levantam-se vários processos do TOR em portas distintas (9050/9051, 9052/9053, ...) e as requisições são distribuídas por round-robin. Existe uma imagem Docker pronta para isso: rotating-tor-http-proxy (várias instâncias atrás de um único endpoint HTTP via HAProxy).

9.5. Limitações (leitura obrigatória)

  • Os nós de saída do TOR são cerca de 1500, suas listas são públicas e Cloudflare/DataDome e a maioria dos sistemas anti-bots os bloqueiam de antemão: em alvos protegidos o TOR é quase inútil.
  • A velocidade é baixa e instável, e não há garantia de que o novo IP esteja «limpo» e funcional.
  • Serve para aprender e para alvos pequenos sem proteção; para produção, use proxies residenciais/móveis.
  • O TOR é uma ferramenta de privacidade; use-o dentro da lei e das regras dos sites.

10. Concorrência e multithreading

Aqui é importante distinguir dois conceitos diferentes.

10.1. Primeiro: concorrência assíncrona (e não threads)

O scraping é uma tarefa I/O-bound (esperamos pela rede). O Node, com uma única thread e graças ao seu event loop, sustenta sem esforço centenas de requisições simultâneas: threads de verdade quase nunca são necessárias aqui. O perigo é exatamente o oposto: disparar um Promise.all sobre 10 000 URLs de uma vez e derrubar tanto a própria rede quanto o servidor alvo. Por isso a concorrência é limitada.

O p-limit — limitador de tarefas simultâneas:

javascript
import pLimit from 'p-limit';

const limit = pLimit(5); // no máximo 5 requisições por vez
const results = await Promise.all(
  urls.map(url => limit(() => scrape(url)))
);

Parentes próximos:

  • p-queue — fila com prioridades, intervalos e rate limit (por exemplo, «no máximo 10 requisições por segundo»).
  • p-map — um map com limite de concorrência.
  • bottleneck — rate limiter avançado (inclusive distribuído via Redis).

10.2. worker_threads: para o parsing com carga computacional (CPU-bound)

Se o gargalo não é a rede, mas o parsing pesado (HTML/JSON gigantescos, expressões regulares, pós-processamento), faz sentido levá-lo para threads com o worker_threads, para não bloquear o event loop. Um wrapper prático são os pools tipo piscina.

javascript
import { Worker } from 'node:worker_threads';
// cada worker parseia seu trecho de HTML em paralelo, sem bloquear a thread principal

10.3. cluster / vários processos: para escalar por núcleos

O cluster, ou simplesmente subir N processos (muitas vezes em Docker), distribui a carga entre os núcleos de CPU e traz tolerância a falhas. Na prática, para um crawler isso costuma ser «vários workers leem de uma fila comum (Redis)»; veja §11.

10.4. Autoescalonamento «de fábrica»

O Crawlee ajusta sozinho a concorrência à CPU/RAM disponíveis (AutoscaledPool): menos risco de cair em um contêiner pequeno e aproveitamento máximo em um grande.

Receita prática: para a maioria dos scrapers, fetch + p-limit/p-queue com um limite de 5--20 requisições simultâneas. Adicione threads ou processos apenas quando tiver esbarrado na CPU ou nos limites de um único processo.


11. Armazenamento de URLs e filas (panorama)

Assim que o crawler percorre mais de uma página, surge o frontier, a fronteira de rastreamento: a fila de URLs «a visitar» mais o conjunto das «já visitadas».

Tarefas-chave:

  • Deduplicação. Não se deve visitar a mesma URL duas vezes. Em memória, um simples Set sobre a URL normalizada; com grandes volumes, um filtro de Bloom (compacto, ao preço de raras coincidências falsas), por exemplo o bloom-filters.
  • Normalização de URLs. Leve-as à forma canônica (ordenar a query, remover #, a barra final e as tags utm); caso contrário as «duplicatas» se multiplicam. Ajuda o normalize-url.
  • Persistência. Se o processo cai, a fila não pode se perder. Memória não serve para tarefas sérias.
  • Prioridades e ordem de percurso — em largura (BFS) ou em profundidade (DFS), com prioridade para as seções importantes.

Onde armazenar:

Escala Solução
Script pequeno de uso único Set + array em memória
Worker único com reinícios arquivo / SQLite, ou a RequestQueue do Crawlee
Vários workers / distribuído Redis (ioredis) como fila comum + conjunto de visitadas
Fila de tarefas de nível industrial BullMQ (repositório) sobre Redis: novas tentativas, atrasos, prioridades, concorrência

O Crawlee oferece uma RequestQueue persistente integrada, com deduplicação e percurso em largura ou profundidade: se você não quer montar o frontier à mão, é o caminho mais rápido.

A arquitetura típica «de gente grande»: Redis/BullMQ como fila de URLs → um pool de workers pega as tarefas, parseia, devolve à fila os links encontrados (após o dedupe) e grava o resultado no banco de dados ou em um arquivo.


12. Frameworks prontos para usar

Se você não quer montar à mão tudo o que foi descrito acima:

  • Crawlee (repositório) — o principal framework moderno para Node.js/TS, da Apify. Interface única para o crawling por HTTP e por navegador (CheerioCrawler, PuppeteerCrawler, PlaywrightCrawler), fila de URLs persistente, rotação de proxies e sessões, autoescalonamento, impressões digitais de navegador «humanas», novas tentativas. As versões recentes adicionam um crawler adaptativo (que decide sozinho se a renderização JS é necessária) e recursos voltados à IA. Requer Node 16+.

```js import { CheerioCrawler } from 'crawlee';

const crawler = new CheerioCrawler({ maxConcurrency: 10, async requestHandler({ $, request, enqueueLinks, pushData }) { await pushData({ url: request.url, title: $('title').text() }); await enqueueLinks(); // encontra os links sozinho e os coloca na fila com deduplicação }, }); await crawler.run(['https://example.com']); ```

  • node-crawler — um crawler mais clássico com fila, limites e Cheerio integrado.
  • x-ray — extração declarativa + paginação.

Para a maioria dos projetos sérios em JS, a resposta padrão hoje é o Crawlee.


13. Anti-bots, robots.txt, novas tentativas (o que costuma ser esquecido)

Esses temas não constavam do plano inicial, mas sem eles um scraper de produção não sobrevive.

13.1. Camuflar-se como um cliente normal

  • Configure um User-Agent verossímil, Accept-Language e Referer. Lista de UAs reais: user-agents.
  • Geração de conjuntos coerentes de cabeçalhos e impressões digitais: got-scraping e o fingerprint-suite da Apify.
  • Com as proteções fortes (Cloudflare e similares), só salva um navegador de verdade (Playwright) ou a falsificação da impressão digital TLS (veja §7.3).

13.2. Cortesia e novas tentativas

  • Respeite o robots.txt onde couber; para parseá-lo ajuda o robots-parser.
  • Aplique rate limiting e atrasos aleatórios entre as requisições (p-queue/bottleneck).
  • Diante de 429/503, respeite o Retry-After, use backoff exponencial com jitter e limite o número de novas tentativas.
  • Guarde em cache o que já foi baixado, para não bater de novo no site após um reinício.

14. Principais vantagens e desvantagens da implementação em JavaScript

Vantagens

  • A mesma linguagem da página. Os sites são escritos em JS: os seletores, a lógica do DOM e até a execução dos scripts da página convivem confortavelmente no mesmo ambiente.
  • Os melhores navegadores headless são nativos do JS. Playwright e Puppeteer são cidadãos de primeira classe no Node; para a dinâmica pesada é uma vantagem séria frente a outros ecossistemas.
  • Assincronia de fábrica. O event loop encaixa perfeitamente com o scraping I/O-bound: alta concorrência em um único processo, sem brigar com threads.
  • Ecossistema maduro. fetch/undici, Cheerio, Crawlee, BullMQ, agentes de proxy prontos: tudo à mão.
  • O Crawlee cobre a «orquestração» (filas, proxies, impressões digitais, escalonamento) quase sem código.

Desvantagens

  • O parsing CPU-bound (documentos enormes, pós-processamento pesado) é o ponto fraco do Node monothread; são necessários worker_threads ou vários processos, enquanto em Go/Rust isso sai mais simples.
  • A voracidade dos navegadores. Playwright/Puppeteer consomem muita RAM/CPU; em escala são gastos palpáveis.
  • O inferno de callbacks/promises em uma orquestração manual sem framework degenera facilmente em espaguete.
  • A impressão digital TLS. Os clientes Node se entregam pelo JA3/JA4; «consertar» isso com Node puro é mais difícil do que parece (são necessários o CycleTLS ou um navegador).
  • A fragilidade dos seletores. É o mal comum do scraping (o layout muda), e o ecossistema JS não livra você da manutenção manual dos seletores CSS/XPath.
  • A ciência de dados posterior. Python com pandas/numpy é mais forte na análise do que foi coletado; às vezes é mais cômodo «coletar com JS, processar com Python».

Quando JS é uma boa escolha: sites dinâmicos, necessidade de um navegador headless, uma equipe que já trabalha com Node, alta concorrência de I/O ou integração com serviços web em JS. Quando considerar uma alternativa: processamento puramente CPU-bound de terabytes de HTML ou uma integração estreita com a análise de dados em Python.