Scraping per linguaggio 21 min di lettura

Web scraping in JavaScript: guida completa, dalle basi alle tecniche avanzate

Web scraping con JavaScript e Node.js: axios, cheerio, Puppeteer e Playwright, dalle pagine semplici ai contenuti dinamici complessi.

TW
Team Web-Scraping.it
Raccolta dati per le esigenze del business
Pubblicato il 28 marzo 2025

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

  1. Che cos’è il web scraping e di che cosa si compone
  2. Come scarichiamo la pagina: i client HTTP
  3. Librerie per il parsing dei contenuti
  4. Leggere il codice di stato e gli altri header
  5. Risolvere i problemi di codifica dei caratteri
  6. Lavorare con i cookie
  7. Lavorare con HTTPS / SSL
  8. Usare i proxy
  9. Scraping attraverso TOR
  10. Multi: multithreading e concorrenza
  11. Memorizzare URL e code (panoramica)
  12. Framework pronti all’uso
  13. Anti-bot, robots.txt, retry (quello che spesso si dimentica)
  14. 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:

  1. Trasporto — come ottenere i byte della pagina (client HTTP o browser headless).
  2. Estrazione — come ricavare dall’HTML/JSON i campi che servono (parser del DOM, selettori).
  3. 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ù.

javascript
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:

  • fetch non lancia un’eccezione su 404/500: devi controllare res.ok per conto tuo.
  • fetch non ha un timeout di default: un socket appeso può restare lì per sempre. Imposta AbortSignal.timeout():
javascript
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.

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: 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.

javascript
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 sopra fetch con default ragionevoli (retry, timeout).
  • node-fetchlegacy, serve solo su versioni di Node molto vecchie; su quelle moderne usa il fetch integrato.
  • 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.

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: 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.
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();

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:

javascript
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 coppie

Buone abitudini:

  • 429 / 503 → leggi Retry-After e applica un backoff, invece di continuare a martellare.
  • 301/302/308 → decidi se seguire il redirect (redirect: 'manual' ti dà il controllo manuale).
  • Content-Type con charset= → 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 uno User-Agent verosimile.

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.

javascript
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 corretti

Se la codifica non è dichiarata da nessuna parte, si può rilevare in modo euristico:

  • jschardet — port dello Universal Charset Detector di Mozilla.
  • chardet — rilevatore alternativo.
javascript
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 un iconv.decode esplicito è 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 (&nbsp;, &egrave;): i parser fatti bene (Cheerio, parse5) le decodificano per te.

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

javascript
const res = await fetch(url, { headers: { cookie: 'sid=abc123; lang=it' } });
const setCookie = res.headers.get('set-cookie'); // da analizzare e rispedire alla richiesta successiva

Va bene per i casi semplici, ma mantenere a mano l’insieme dei cookie tra una richiesta e l’altra è una sofferenza.

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:

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 }); // i cookie si aggiungono da soli

Per 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:

javascript
// 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.

javascript
// 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)

javascript
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:

javascript
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:

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 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-chain di 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).
javascript
// 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) → residenzialimobili (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:

code
SocksPort 9050
ControlPort 9051
# la password si genera con il comando: tor --hash-password "la_tua_password"
HashedControlPassword 16:....
CookieAuthentication 1

9.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):

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 di uscita attuale di TOR

9.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:

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();
  });
}

// 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:

javascript
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 — un map con 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.

javascript
import { Worker } from 'node:worker_threads';
// ogni worker analizza il proprio frammento di HTML in parallelo, senza bloccare il thread principale

10.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 Set sull’URL normalizzato; su grandi volumi, un filtro di Bloom (compatto, al prezzo di rare false corrispondenze), per esempio bloom-filters.
  • Normalizzazione degli URL. Portali alla forma canonica (ordinare la query, togliere #, lo slash finale e i parametri utm), altrimenti i «duplicati» si moltiplicano. Aiuta normalize-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-Agent verosimile, Accept-Language e Referer. Un elenco di UA reali: user-agents.
  • Per generare set coerenti di header e fingerprint: got-scraping e 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.txt dove richiesto; per analizzarlo aiuta robots-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_threads o 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.