Guida generale su come scaricare le pagine con Node.js, estrarne i dati e scalare il tutto fino a uno scraper di produzione: codifiche, multithreading e concorrenza, proxy, TOR, SSL, cookie, header, code di URL e le insidie da conoscere. Con i link alle librerie ufficiali.
Indice
- Che cos’è il web scraping e di che cosa si compone
- Come scarichiamo la pagina: i client HTTP
- Librerie per il parsing dei contenuti
- Leggere il codice di stato e gli altri header
- Risolvere i problemi di codifica dei caratteri
- Lavorare con i cookie
- Lavorare con HTTPS / SSL
- Usare i proxy
- Scraping attraverso TOR
- Multi: multithreading e concorrenza
- Memorizzare URL e code (panoramica)
- Framework pronti all’uso
- Anti-bot, robots.txt, retry (quello che spesso si dimentica)
- Principali vantaggi e svantaggi dell’implementazione in JavaScript
1. Che cos’è il web scraping e di che cosa si compone
Lo scraping di un sito si scompone quasi sempre in tre livelli indipendenti, ed è proprio su questi livelli che conviene progettare lo scraper:
- Trasporto — come ottenere i byte della pagina (client HTTP o browser headless).
- Estrazione — come ricavare dall’HTML/JSON i campi che servono (parser del DOM, selettori).
- Orchestrazione — come visitare molti URL senza farsi bannare: code, concorrenza, proxy, retry, deduplicazione.
Tutta la guida procede in crescendo: prima «scaricare una pagina», alla fine «un crawler distribuito e robusto».
Un bivio importante fin dall’inizio:
- Sito statico (i dati sono già nell’HTML) → bastano un client HTTP + un parser del DOM. Veloce, economico, migliaia di pagine al minuto.
- Sito dinamico (i dati li carica JavaScript) → serve o un browser headless (Playwright / Puppeteer), oppure il reverse engineering dell’API interna del sito (spesso i dati stanno in un endpoint JSON e il browser non serve).
Prima di scomodare un browser pesante, controlla sempre la scheda Network dei DevTools: se la pagina va a prendere i dati da /api/... e riceve JSON, è quel JSON che va analizzato, non il DOM renderizzato.
2. Come scarichiamo la pagina: i client HTTP
2.1. fetch nativo (Node 18+): la scelta di default
A partire da Node.js 18 fetch è integrato a livello globale, è stabile da Node 21 ed è supportato nei rami LTS 22 e 24. Sotto il cofano gira su undici, quindi per le attività di base i pacchetti separati come node-fetch non servono più.
const res = await fetch('https://example.com');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const html = await res.text();Due cose su cui inciampano tutti i principianti:
fetchnon lancia un’eccezione su 404/500: devi controllareres.okper conto tuo.fetchnon ha un timeout di default: un socket appeso può restare lì per sempre. ImpostaAbortSignal.timeout():
const res = await fetch(url, { signal: AbortSignal.timeout(15_000) });2.2. undici direttamente: quando serve la massima velocità
undici è proprio il «motore» del fetch nativo, ma la sua API di basso livello (request, pool di connessioni, pipelining) nei benchmark supera fetch, axios e got di parecchie volte. Ha senso quando il collo di bottiglia è ormai il throughput.
import { request } from 'undici';
const { statusCode, headers, body } = await request('https://example.com');
const html = await body.text();2.3. got e got-scraping: comodità + «camuffamento da browser»
got è un client maturo con retry integrati, hook, supporto del cookie jar e HTTP/2.
Per lo scraping è più interessante il fork got-scraping di Apify: genera automaticamente header da browser verosimili e nell’ordine giusto, il che riduce la probabilità di ban. È proprio quello che usa CheerioCrawler in Crawlee.
import { gotScraping } from 'got-scraping';
const { body } = await gotScraping({ url: 'https://example.com' });2.4. axios: se ti servono gli interceptor e un’API familiare
axios (repository) resta il client più popolare grazie agli interceptor, alla gestione comoda dei proxy e al parsing automatico del JSON. Per lo scraping non è più veloce di fetch, ma il suo ecosistema (per esempio axios-retry) fa risparmiare tempo.
2.5. Altri
ky— wrapper sottile soprafetchcon default ragionevoli (retry, timeout).node-fetch— legacy, serve solo su versioni di Node molto vecchie; su quelle moderne usa ilfetchintegrato.- I moduli integrati
http/https— controllo massimo, ma tanta impalcatura manuale; di solito servono solo sotto il cofano di agent e proxy.
Cosa scegliere
| Scenario | Raccomandazione |
|---|---|
| La maggior parte delle attività, Node 18+ | fetch nativo |
| Migliaia di richieste, priorità alle prestazioni | undici (request/Pool) |
| Camuffamento degli header già pronto | got-scraping |
| Interceptor, API familiare, codebase legacy | axios |
| Sito dinamico con rendering JS | Playwright / Puppeteer (vedi §3.5) |
3. Librerie per il parsing dei contenuti
Una volta ottenuta la stringa HTML, bisogna trasformarla in dati. L’HTML non si analizza con le espressioni regolari: è un approccio fragile che si rompe al primo tag annidato. Usa un parser vero.
3.1. Cheerio: lo standard per le pagine statiche
Cheerio (repository) è un parser veloce lato server con un’API in stile jQuery. Non esegue JS, non renderizza: costruisce semplicemente l’albero e ti lascia percorrerlo con i selettori. Ideale in coppia con 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: un DOM quasi vero
jsdom implementa una parte considerevole del DOM del browser e può perfino eseguire gli script della pagina. È più pesante di Cheerio, ma offre i familiari querySelectorAll e document, e torna utile quando serve un’API del DOM più «fedele».
3.3. Alternative leggere e veloci
node-html-parser— velocissimo, con selettori CSS.htmlparser2— parser di basso livello in streaming (su cui è costruito Cheerio).parse5— parser HTML5 fedele alla specifica.linkedom— alternativa leggera a jsdom con API del DOM.
3.4. Estrazione per «ricette»
x-ray permette di descrivere l’estrazione in modo dichiarativo (selettore → campo) e di seguire subito la paginazione. Comodo per i prototipi.
3.5. Contenuti dinamici: Playwright e Puppeteer
Quando i contenuti li disegna JS, serve un browser headless:
- Playwright (repository) — il favorito moderno: Chromium, Firefox e WebKit con un’unica API, attese automatiche degli elementi, intercettazione delle richieste di rete, contesti per isolare i cookie.
- Puppeteer (repository) — lo standard de facto per Chrome/Chromium, un po’ più semplice, con un ecosistema 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();Il browser è la via più costosa in termini di risorse: decine o centinaia di MB di RAM per scheda. Usalo solo quando davvero non c’è né HTML statico né un’API interna.
Consiglio sull’approccio ibrido: spesso la soluzione ottimale è aprire la pagina nel browser una sola volta, estrarre l’HTML già renderizzato con
page.content()e poi analizzarlo con il velocissimo Cheerio: così unisci il rendering JS alla comodità dei selettori.
4. Leggere il codice di stato e gli altri header
Lo stato e gli header sono metà della diagnostica di uno scraper (ban, redirect, limiti, codifica).
Con il fetch nativo:
const res = await fetch(url, { redirect: 'follow' });
res.status; // 200, 404, 429, 503 ...
res.statusText; // 'OK', 'Too Many Requests'
res.ok; // true con 2xx
res.redirected; // se ci sono stati redirect
res.url; // URL finale dopo i redirect
res.headers.get('content-type'); // text/html; charset=windows-1252
res.headers.get('set-cookie'); // i cookie
res.headers.get('retry-after'); // quanto aspettare con 429/503
[...res.headers]; // tutti gli header in coppieBuone abitudini:
- 429 / 503 → leggi
Retry-Aftere applica un backoff, invece di continuare a martellare. - 301/302/308 → decidi se seguire il redirect (
redirect: 'manual'ti dà il controllo manuale). Content-Typeconcharset=→ la prima e principale fonte di verità sulla codifica (vedi §5).- Gestire gli header in uscita (
User-Agent,Accept-Language,Referer) è altrettanto importante: molti siti tagliano le richieste prive di unoUser-Agentverosimile.
In got/axios tutto questo è disponibile come response.statusCode e response.headers. Nel browser, tramite l’intercettazione della risposta: page.on('response', res => res.status()).
5. Risolvere i problemi di codifica dei caratteri
Un grande classico dei siti datati: la pagina è in windows-1252 (o ISO-8859-1) e tu ricevi caratteri illeggibili tipo perché al posto di «perché». La causa: res.text() decodifica sempre i byte come UTF-8, mentre il sito li ha inviati in un’altra codifica.
Regola: con le pagine non in UTF-8 non puoi usare res.text(). Prendi i byte grezzi (arrayBuffer) e decodificali con la codifica giusta tramite iconv-lite.
import iconv from 'iconv-lite';
const res = await fetch('https://vecchio-sito.example/');
const buf = Buffer.from(await res.arrayBuffer());
// 1) proviamo a ricavare la codifica dall'header Content-Type
let charset = (res.headers.get('content-type') || '').match(/charset=([^;]+)/i)?.[1];
// 2) se nell'header non c'è, la cerchiamo nel <meta> (decodifichiamo il frammento come latin1 per leggere il 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); // accenti correttiSe la codifica non è dichiarata da nessuna parte, si può rilevare in modo euristico:
import jschardet from 'jschardet';
const guess = jschardet.detect(buf); // { encoding: 'windows-1252', confidence: 0.99 }Inoltre:
- Cheerio sa decodificare da solo, se gli passi il buffer e un suggerimento:
cheerio.load(buf, { decodeEntities: true })— ma uniconv.decodeesplicito è più affidabile. - In un browser headless il problema della codifica di solito non esiste: il browser decodifica la pagina per conto suo e
page.content()restituisce UTF-8 corretto. - Non dimenticare le entità HTML (
,è): i parser fatti bene (Cheerio, parse5) le decodificano per te.
6. Lavorare con i cookie
I cookie servono per le aree con autenticazione, le sessioni, i carrelli e per superare le schermate della «prima visita». Ci sono tre livelli.
6.1. A mano, tramite gli header
const res = await fetch(url, { headers: { cookie: 'sid=abc123; lang=it' } });
const setCookie = res.headers.get('set-cookie'); // da analizzare e rispedire alla richiesta successivaVa bene per i casi semplici, ma mantenere a mano l’insieme dei cookie tra una richiesta e l’altra è una sofferenza.
6.2. Archivio di cookie (cookie jar): la via consigliata
tough-cookie è l’implementazione di riferimento di un archivio di cookie che rispetta dominio, percorso, scadenza e flag. Molti client si integrano con lui di serie.
got accetta il jar direttamente e gestisce la sessione da solo:
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 }); // i cookie si aggiungono da soliPer axios esiste il wrapper axios-cookiejar-support; con il fetch nativo dovrai collegare tough-cookie a mano oppure usare got/undici.
6.3. Nel browser
In Playwright/Puppeteer i cookie vivono nel contesto e si possono salvare e ripristinare: comodissimo per fare il login una volta sola e riutilizzare la sessione:
// salvare lo stato (cookie + localStorage)
await context.storageState({ path: 'state.json' });
// ripristinarlo in una nuova esecuzione
const context = await browser.newContext({ storageState: 'state.json' });7. Lavorare con HTTPS / SSL
Un normale sito HTTPS non richiede alcuno sforzo: fetch/got/axios verificano il certificato in automatico. Casi particolari:
7.1. Certificati autofirmati o scaduti
A volte bisogna disattivare la verifica (per esempio quando lavori attraverso un proxy MITM o con un ambiente di test). Fallo con cognizione di causa: elimina la protezione contro la manomissione del traffico.
// undici / fetch nativo — tramite il dispatcher
import { Agent, setGlobalDispatcher } from 'undici';
setGlobalDispatcher(new Agent({ connect: { rejectUnauthorized: false } }));
// got / axios — tramite 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 })La «mannaia» globale NODE_TLS_REJECT_UNAUTHORIZED=0 disattiva la verifica per l’intero processo: meglio non farlo in produzione.
7.2. Certificati root propri / certificati client (mTLS)
import https from 'node:https';
import fs from 'node:fs';
const agent = new https.Agent({
ca: fs.readFileSync('./ca.pem'), // autorità di certificazione propria
cert: fs.readFileSync('./client.pem'), // certificato client per mTLS
key: fs.readFileSync('./client.key'),
});7.3. Fingerprint TLS (JA3): anti-bot avanzato
Le protezioni moderne (Cloudflare, DataDome) sanno distinguere i client dall’handshake TLS (JA3/JA4): quello di un client Node non è uguale a quello di un vero Chrome, e questo smaschera il bot anche con header impeccabili. Node puro non può «ripararlo»; aiutano:
got-scraping— camuffa in parte il livello degli header;- CycleTLS — sostituzione del fingerprint TLS;
- un vero browser headless (Playwright) — fornisce l’handshake TLS «autentico» di un browser.
8. Usare i proxy
I proxy servono a distribuire il carico su più IP, aggirare le restrizioni geografiche e i ban per IP. Tipi: HTTP, HTTPS e SOCKS5 (l’ultimo è il più universale: trasporta qualsiasi traffico e anche il DNS).
8.1. fetch nativo (particolarità importante del 2026!)
Il fetch nativo non ha la vecchia opzione { agent }. Il proxy si imposta tramite il dispatcher di undici, ProxyAgent:
import { ProxyAgent, setGlobalDispatcher } from 'undici';
// a livello globale: tutte le fetch passeranno dal proxy
setGlobalDispatcher(new ProxyAgent('http://user:pass@proxy.host:8080'));
const res = await fetch('https://example.com');
// oppure in modo mirato, per una singola richiesta
const res2 = await fetch('https://example.com', {
dispatcher: new ProxyAgent('http://user:pass@proxy.host:8080'),
});Su Node 24+ si può attivare la lettura di HTTP_PROXY/HTTPS_PROXY dall’ambiente con il flag NODE_USE_ENV_PROXY=1 (o --use-env-proxy), ma un ProxyAgent esplicito è più affidabile.
8.2. got / axios tramite agent
Con gli agent 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 attraverso il 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. Rotazione e pool di proxy
Per scalare serve un pool di proxy con rotazione e scarto degli indirizzi «morti». La variante più semplice: scegliere un proxy casuale o in round-robin a ogni richiesta. Strumenti pronti:
proxy-chaindi Apify — avvia un proxy locale che inoltra verso l’upstream (anche con autenticazione, cosa importante per Chromium, che non accetta login e password in--proxy-server).- In Crawlee la rotazione di proxy e sessioni è integrata (
ProxyConfiguration).
// un proxy casuale dal pool a ogni richiesta
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()) });Tipi di proxy per qualità: datacenter (economici, facili da individuare) → residenziali → mobili (costosi, quasi mai bannati). La scelta dipende dall’aggressività della protezione del sito target.
9. Scraping attraverso TOR
TOR offre una rotazione di IP gratuita: il traffico passa attraverso una catena di nodi e si può cambiare l’IP di uscita a comando. È comodo per imparare e per i piccoli progetti, ma l’approccio ha limiti seri (vedi la fine della sezione).
9.1. Configurazione
TOR avvia un proxy SOCKS sulla porta 9050 e una porta di controllo 9051 per la gestione. Nel file di configurazione torrc:
SocksPort 9050
ControlPort 9051
# la password si genera con il comando: tor --hash-password "la_tua_password"
HashedControlPassword 16:....
CookieAuthentication 19.2. Richieste attraverso TOR
Basta puntare il client sul SOCKS5 locale (usa socks5h, così anche il DNS si risolve attraverso TOR; altrimenti l’IP reale trapela):
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 di uscita attuale di TOR9.3. Cambiare l’IP di uscita (nuova identità)
Per ottenere un nuovo IP di uscita si invia alla porta di controllo il segnale NEWNYM. Puoi usare la libreria già pronta tor-request oppure fare a mano, con un normale socket TCP e senza dipendenze:
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();
});
}
// tra una richiesta e l'altra:
await newTorIdentity('la_tua_password');Importante: TOR mantiene un cooldown di ~10 secondi tra un cambio di circuito e l’altro: più spesso di così l’IP non si può ruotare.
9.4. Più istanze per aumentare il throughput
Un TOR = un solo IP di uscita per volta e un cooldown lento. Per un pool di «proxy gratuiti» si avviano più processi TOR su porte diverse (9050/9051, 9052/9053, …) e si distribuiscono le richieste in round-robin. C’è un’immagine Docker pronta per questo: rotating-tor-http-proxy (più istanze dietro un unico endpoint HTTP tramite HAProxy).
9.5. Limiti (da leggere assolutamente)
- I nodi di uscita di TOR sono circa 1.500, le loro liste sono pubbliche e Cloudflare/DataDome e la maggior parte dei sistemi anti-bot li bloccano in anticipo: sui target protetti TOR è quasi inutile.
- La velocità è bassa e instabile, e il nuovo IP non è garantito «pulito» e funzionante.
- Va bene per imparare e per piccoli target senza protezione; per la produzione usa proxy residenziali o mobili.
- TOR è uno strumento di privacy; usalo nel rispetto della legge e delle regole dei siti.
10. Concorrenza e multithreading
Qui è importante distinguere due concetti diversi.
10.1. Prima di tutto: la concorrenza asincrona (non i thread)
Lo scraping è un’attività I/O-bound (si aspetta la rete). Node, con un solo thread e grazie all’event loop, regge senza fatica centinaia di richieste simultanee: i thread veri qui quasi mai servono. Il pericolo è esattamente l’opposto: lanciare un Promise.all su 10.000 URL in un colpo solo e mettere in ginocchio sia la propria rete sia il server target. Per questo la concorrenza si limita.
p-limit — limitatore di task simultanei:
import pLimit from 'p-limit';
const limit = pLimit(5); // massimo 5 richieste in contemporanea
const results = await Promise.all(
urls.map(url => limit(() => scrape(url)))
);Parenti stretti:
p-queue— coda con priorità, intervalli e rate limit (per esempio «non più di 10 richieste al secondo»).p-map— unmapcon limite di concorrenza.bottleneck— rate limiter avanzato (anche distribuito via Redis).
10.2. worker_threads: per il parsing pesante (CPU-bound)
Se il collo di bottiglia non è la rete ma il parsing pesante (HTML/JSON giganteschi, espressioni regolari, post-elaborazione), ha senso spostarlo nei thread con worker_threads, per non bloccare l’event loop. Un wrapper comodo sono i pool come piscina.
import { Worker } from 'node:worker_threads';
// ogni worker analizza il proprio frammento di HTML in parallelo, senza bloccare il thread principale10.3. cluster / più processi: scalare sui core della CPU
cluster, o semplicemente l’avvio di N processi (spesso in Docker), distribuisce il carico sui core della CPU e dà tolleranza ai guasti. In pratica, per un crawler questo di solito significa «più worker leggono da una coda comune (Redis)»; vedi §11.
10.4. Autoscaling «di serie»
Crawlee adatta da solo la concorrenza a CPU/RAM disponibili (AutoscaledPool): meno rischio di crollare in un container piccolo e massima resa in uno grande.
Ricetta pratica: per la maggior parte degli scraper, fetch + p-limit/p-queue con un limite di 5–20 richieste simultanee. Aggiungi thread o processi solo quando hai raggiunto il limite della CPU o di un singolo processo.
11. Memorizzare URL e code (panoramica)
Non appena il crawler visita più di una pagina compare il frontier, il fronte di scansione: la coda degli URL «da visitare» più l’insieme di quelli «già visitati».
Compiti chiave:
- Deduplicazione. Lo stesso URL non va visitato due volte. In memoria, un semplice
Setsull’URL normalizzato; su grandi volumi, un filtro di Bloom (compatto, al prezzo di rare false corrispondenze), per esempiobloom-filters. - Normalizzazione degli URL. Portali alla forma canonica (ordinare la query, togliere
#, lo slash finale e i parametri utm), altrimenti i «duplicati» si moltiplicano. Aiutanormalize-url. - Persistenza. Se il processo cade, la coda non deve andare persa. La memoria non va bene per i progetti seri.
- Priorità e ordine di visita — in ampiezza (BFS) o in profondità (DFS), con priorità per le sezioni importanti.
Dove memorizzare:
| Scala | Soluzione |
|---|---|
| Piccolo script usa e getta | Set + array in memoria |
| Worker singolo con riavvii | file / SQLite, oppure la RequestQueue di Crawlee |
| Più worker / distribuito | Redis (ioredis) come coda comune + insieme dei visitati |
| Coda di task di livello industriale | BullMQ (repository) sopra Redis: retry, ritardi, priorità, concorrenza |
Crawlee offre una RequestQueue persistente integrata, con deduplicazione e visita in ampiezza o in profondità: se non vuoi montare il frontier a mano, è la strada più rapida.
L’architettura tipica «da grandi»: Redis/BullMQ come coda di URL → un pool di worker prende i task, analizza le pagine, rimette in coda i link trovati (dopo la deduplicazione) e scrive il risultato nel database o in un file.
12. Framework pronti all’uso
Se non vuoi assemblare a mano tutto quello che abbiamo elencato:
- Crawlee (repository) — il principale framework moderno per Node.js/TS, di Apify. Interfaccia unica per il crawling via HTTP e via browser (
CheerioCrawler,PuppeteerCrawler,PlaywrightCrawler), coda di URL persistente, rotazione di proxy e sessioni, autoscaling, fingerprint del browser «umani», retry. Le versioni recenti aggiungono un crawler adattivo (decide da solo se serve il rendering JS) e funzionalità orientate all’AI. Richiede 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(); // trova i link da solo e li mette in coda con la deduplicazione }, }); await crawler.run(['https://example.com']); ```
node-crawler— un crawler più classico con coda, limiti e Cheerio integrato.x-ray— estrazione dichiarativa + paginazione.
Per la maggior parte dei progetti seri in JS la risposta di default oggi è Crawlee.
13. Anti-bot, robots.txt, retry (quello che spesso si dimentica)
Questi temi non figuravano nel piano iniziale, ma senza di essi uno scraper di produzione non sopravvive.
13.1. Camuffarsi da client normale
- Imposta uno
User-Agentverosimile,Accept-LanguageeReferer. Un elenco di UA reali:user-agents. - Per generare set coerenti di header e fingerprint:
got-scrapinge fingerprint-suite di Apify. - Con le protezioni forti (Cloudflare e simili) salva solo un browser vero (Playwright) o la sostituzione del fingerprint TLS (vedi §7.3).
13.2. Cortesia e retry
- Rispetta il
robots.txtdove richiesto; per analizzarlo aiutarobots-parser. - Applica rate limiting e ritardi casuali tra le richieste (
p-queue/bottleneck). - Con 429/503 rispetta
Retry-After, usa un backoff esponenziale con jitter e limita il numero di tentativi. - Metti in cache quello che hai già scaricato, per non colpire di nuovo il sito a ogni riavvio.
14. Principali vantaggi e svantaggi dell’implementazione in JavaScript
Vantaggi
- Lo stesso linguaggio della pagina. I siti sono scritti in JS: selettori, logica del DOM e perfino l’esecuzione degli script della pagina convivono comodamente nello stesso ambiente.
- I migliori browser headless sono nativi JS. Playwright e Puppeteer sono cittadini di prima classe in Node; per la dinamica pesante è un vantaggio serio rispetto agli altri ecosistemi.
- Asincronia di serie. L’event loop si sposa alla perfezione con lo scraping I/O-bound: alta concorrenza in un solo processo, senza combattere con i thread.
- Ecosistema maturo.
fetch/undici, Cheerio, Crawlee, BullMQ, agent proxy già pronti: tutto a portata di mano. - Crawlee copre l’«orchestrazione» (code, proxy, fingerprint, scaling) quasi senza codice.
Svantaggi
- Il parsing CPU-bound (documenti enormi, post-elaborazione pesante) è il punto debole del Node a thread singolo; servono
worker_threadso più processi, mentre in Go/Rust è tutto più semplice. - La voracità dei browser. Playwright/Puppeteer consumano molta RAM/CPU; su larga scala sono costi tangibili.
- L’inferno di callback e promise: l’orchestrazione manuale senza framework degenera facilmente in spaghetti code.
- Il fingerprint TLS. I client Node si tradiscono con JA3/JA4; «sistemarlo» con Node puro è più difficile di quanto sembri (servono CycleTLS o un browser).
- La fragilità dei selettori. È il male comune dello scraping (il layout cambia), e l’ecosistema JS non ti libera dalla manutenzione manuale dei selettori CSS/XPath.
- La data science a valle. Python con pandas/numpy è più forte nell’analisi successiva dei dati raccolti; a volte conviene «raccogliere con JS, elaborare con Python».
Quando JS è una buona scelta: siti dinamici, necessità di un browser headless, un team già su Node, alta concorrenza di I/O o integrazione con servizi web in JS. Quando valutare un’alternativa: elaborazione puramente CPU-bound di terabyte di HTML o integrazione stretta con l’analisi dei dati in Python.