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
- O que é web scraping e do que ele se compõe
- Como baixamos a página: clientes HTTP
- Bibliotecas para parsear o conteúdo
- Obter o código de status e outros cabeçalhos
- Solução de problemas de codificação de caracteres
- Trabalho com cookies
- Trabalho com HTTPS / SSL
- Uso de proxies
- Scraping via TOR
- Multi: multithreading e concorrência
- Armazenamento de URLs e filas (panorama)
- Frameworks prontos para usar
- Anti-bots, robots.txt, novas tentativas (o que costuma ser esquecido)
- 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:
- Transporte — como obter os bytes da página (cliente HTTP ou navegador headless).
- Extração — como tirar do HTML/JSON os campos necessários (parser do DOM, seletores).
- 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.
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
fetchnão lança exceção com 404/500: é preciso verificarres.okpor conta própria. - O
fetchnão tem timeout por padrão: um socket travado pode ficar assim para sempre. ConfigureAbortSignal.timeout():
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.
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.
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 ofetchcom padrões razoáveis (novas tentativas, timeouts).node-fetch— legado, só necessário em versões muito antigas do Node; nas modernas use ofetchintegrado.- 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.
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.
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:
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 paresBons hábitos:
- 429 / 503 → leia o
Retry-Aftere aplique backoff, em vez de continuar martelando. - 301/302/308 → decida se vai seguir o redirecionamento (
redirect: 'manual'dá controle manual). Content-Typecomcharset=→ 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 umUser-Agentverossí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.
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 corretosSe a codificação não está declarada em lugar nenhum, é possível detectá-la de forma heurística:
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 umiconv.decodeexplí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 (
,ã): parsers decentes (Cheerio, parse5) as decodificam para você.
6. Trabalho com cookies
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
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 seguinteServe para casos simples, mas manter à mão o conjunto de cookies entre requisições é um suplício.
6.2. Armazém de cookies (cookie jar): o recomendado
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:
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 sozinhosPara 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:
// 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.
// 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)
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:
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:
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-chainda 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).
// 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) → residencial → mó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:
SocksPort 9050
ControlPort 9051
# a senha é gerada com o comando: tor --hash-password "sua_senha"
HashedControlPassword 16:....
CookieAuthentication 19.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):
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 TOR9.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:
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:
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— ummapcom 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.
import { Worker } from 'node:worker_threads';
// cada worker parseia seu trecho de HTML em paralelo, sem bloquear a thread principal10.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
Setsobre a URL normalizada; com grandes volumes, um filtro de Bloom (compacto, ao preço de raras coincidências falsas), por exemplo obloom-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 onormalize-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-Agentverossímil,Accept-LanguageeReferer. Lista de UAs reais:user-agents. - Geração de conjuntos coerentes de cabeçalhos e impressões digitais:
got-scrapinge 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.txtonde couber; para parseá-lo ajuda orobots-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_threadsou 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.