Guida d’insieme su come estrarre dati dalle pagine web in Java — dal semplice download di una singola pagina a un crawler multithread con proxy, TOR e la soluzione dei problemi più comuni. La struttura segue un ordine «dal semplice al complesso»: ogni sezione si appoggia alla precedente.
Indice
- Introduzione: che cos’è il web scraping e il quadro legale
- Come scarichiamo la pagina: client HTTP
- Librerie per il parsing del contenuto
- Risolvere i problemi con le lettere accentate (codifiche)
- Lettura dello status della risposta e degli header
- Lavorare con i cookie
- Lavorare con HTTPS/SSL
- Uso dei proxy
- Scraping attraverso TOR
- Multithreading
- Rendering JavaScript: siti dinamici
- Protezione anti-bot: User-Agent, ritardi, retry
- Archiviazione degli URL e code
- Framework di crawling pronti all’uso
- Pro e contro dell’implementazione in Java
1. Introduzione
Il web scraping è l’estrazione automatizzata di dati dalle pagine web. Il processo si divide, a grandi linee, in due fasi che è importante non confondere:
- Download (fetching / crawling) — ottenere l’HTML (o JSON/XML) via HTTP. Se ne occupa il client HTTP.
- Analisi (parsing / extraction) — trasformare l’HTML «grezzo» in una struttura e selezionare i dati che servono tramite i selettori. Se ne occupa il parser.
Quadro legale ed etico
Prima di scrivere codice, conviene tenere a mente alcune cose — non è una consulenza legale, ma un minimo di igiene:
robots.txt— il file nella root del sito (https://site.com/robots.txt) in cui il proprietario indica che cosa i robot possono scansionare. Quasi da nessuna parte ha valore legale, ma ignorarlo è cattiva educazione e una causa frequente di ban.- Le condizioni d’uso (ToS) del sito possono vietare espressamente la raccolta automatica. Violare i ToS è un rischio contrattuale e, in alcune giurisdizioni, anche più serio.
- Dati personali. La raccolta di dati personali è regolata dalla legge (nell’UE, il GDPR). Fai attenzione.
- Carico. Uno scraping aggressivo = un DoS per il server altrui. Inserisci dei ritardi, limita il numero di thread, rispetta
Retry-Aftere i codici429/503.
Tecnicamente Java permette quasi tutto. La responsabilità su «che cosa esattamente» e «a quale scopo» resta a te.
2. Come scarichiamo la pagina
È il fondamento: senza una risposta ottenuta correttamente non c’è nulla da analizzare. Java offre diverse opzioni di client HTTP.
2.1. java.net.http.HttpClient integrato (Java 11+)
Il client standard moderno. Non richiede dipendenze, supporta HTTP/2 e le modalità sincrona e asincrona.
import java.net.URI;
import java.net.http.*;
import java.time.Duration;
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.followRedirects(HttpClient.Redirect.NORMAL)
.version(HttpClient.Version.HTTP_2)
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://example.com"))
.timeout(Duration.ofSeconds(15))
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)")
.header("Accept-Language", "it-IT,it;q=0.9")
.GET()
.build();
HttpResponse<String> response =
client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("Status: " + response.statusCode());
String html = response.body();Una sfumatura importante sulla codifica: BodyHandlers.ofString() senza argomento decodifica il body come UTF-8. Se il sito usa un’altra codifica (per esempio ISO-8859-1), compariranno caratteri illeggibili (mojibake) — ne parla la sezione 4. Per evitarlo, spesso si prendono i byte e si determina la codifica a parte:
HttpResponse<byte[]> resp =
client.send(request, HttpResponse.BodyHandlers.ofByteArray());
byte[] raw = resp.body(); // decodifichiamo dopo, conoscendo il charset2.2. OkHttp
Popolare libreria di terze parti (Square). API comoda, pool di connessioni, interceptor, gestione semplice di proxy e cookie. Una buona scelta «di default» per lo scraping serio.
// build.gradle: implementation("com.squareup.okhttp3:okhttp:4.12.0")
import okhttp3.*;
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(Duration.ofSeconds(10))
.readTimeout(Duration.ofSeconds(15))
.build();
Request request = new Request.Builder()
.url("https://example.com")
.header("User-Agent", "Mozilla/5.0 ...")
.build();
try (Response response = client.newCall(request).execute()) {
int code = response.code();
byte[] bytes = response.body().bytes(); // di nuovo: meglio i byte
}2.3. Apache HttpClient 5
Libreria matura, potente, un po’ più verbosa. Regolazione fine di connessioni, pool e autenticazione. La si incontra spesso in ambito enterprise.
// org.apache.httpcomponents.client5:httpclient5:5.x
try (CloseableHttpClient httpclient = HttpClients.createDefault()) {
HttpGet httpGet = new HttpGet("https://example.com");
httpclient.execute(httpGet, response -> {
int status = response.getCode();
byte[] body = EntityUtils.toByteArray(response.getEntity());
return body;
});
}2.4. Download diretto con jsoup
jsoup (vedi la sezione 3) sa scaricare la pagina da solo. È comodo per i prototipi, ma il suo motore HTTP integrato è meno flessibile (proxy, pool, regolazioni fini), quindi per i task di produzione la combinazione tipica è: scarichi con un client potente → fai il parsing con jsoup.
Document doc = Jsoup.connect("https://example.com")
.userAgent("Mozilla/5.0 ...")
.timeout(15_000)
.get();Quale scegliere
| Client | Quando sceglierlo |
|---|---|
java.net.http.HttpClient |
Nessuna voglia di aggiungere dipendenze, Java 11+, HTTP/2 |
| OkHttp | Scelta universale; proxy/cookie/interceptor comodi |
| Apache HttpClient 5 | Enterprise, controllo fine, autenticazione complessa |
jsoup .connect() |
Prototipi e task semplici «scarica e analizza» |
3. Librerie per il parsing
Ottenuto l’HTML, bisogna estrarre i dati. Analizzarlo con le espressioni regolari è da evitare — l’HTML non è un linguaggio regolare, e un parser del genere si rompe su qualsiasi markup non standard.
3.1. jsoup — il cavallo di battaglia
jsoup (versione attuale nel 2026: 1.22.2) implementa la specifica WHATWG HTML5 e costruisce lo stesso DOM dei browser. Supporta i selettori CSS e XPath e perdona l’HTML «sporco».
// implementation("org.jsoup:jsoup:1.22.2")
import org.jsoup.Jsoup;
import org.jsoup.nodes.*;
import org.jsoup.select.Elements;
Document doc = Jsoup.parse(html, "https://example.com"); // 2° argomento — baseUri per i link assoluti
// selettori CSS
Elements links = doc.select("a[href]");
for (Element link : links) {
String text = link.text();
String absUrl = link.absUrl("href"); // URL assoluto
}
// selezione mirata
Element title = doc.selectFirst("h1.article-title");
String price = doc.select("span.price").text();
// attributi
String img = doc.selectFirst("img").attr("src");Alcune tecniche frequenti con i selettori:
doc.select("div.product"); // per classe
doc.select("#main-content"); // per id
doc.select("ul.menu > li"); // figli diretti
doc.select("a[href^=https]"); // l'attributo inizia con
doc.select("table tr:nth-child(2n)"); // pseudo-selettori
doc.select("p:contains(Prezzo)"); // per testo3.2. HtmlUnit — il browser headless
HtmlUnit è un browser GUI-less scritto in Java. Sa eseguire JavaScript (in modo limitato), il che a volte permette di ottenere dati dai siti dinamici senza il pesante Selenium. Più lento di jsoup, ma più potente.
3.3. Quando l’HTML non serve
Molto spesso i dati della pagina vengono caricati con una richiesta separata a una API interna (JSON). Apri i DevTools → scheda Network, individua l’XHR/fetch giusto — e fai il parsing del JSON puro con Jackson o Gson. È di gran lunga più affidabile e veloce che analizzare l’HTML. Controlla sempre prima questa strada.
// com.fasterxml.jackson.core:jackson-databind
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(jsonString);
String name = root.path("data").path("name").asText();4. Lettere accentate e codifiche
Il mal di testa più frequente nel web italiano è il mojibake (perché, più o ���). La causa è sempre la stessa: i byte sono stati decodificati con la codifica sbagliata.
Perché succede
HTTP restituisce byte. Per ottenere una stringa bisogna decodificarli con la codifica corretta. La codifica può essere indicata:
- nell’header HTTP
Content-Type: text/html; charset=ISO-8859-1; - in
<meta charset="...">o<meta http-equiv="Content-Type">dentro l’HTML; - da nessuna parte (e allora bisogna indovinarla).
Se leggi il body come UTF-8 ma il sito è in ISO-8859-1 (un classico del vecchio web italiano), otterrai spazzatura.
Regola: lavora con i byte, determina la codifica in modo esplicito
jsoup lo risolve quasi in automatico se gli passi i byte o un InputStream, e non una stringa già pronta — legge da solo il charset dall’header o dal <meta>:
// Corretto: jsoup rileva da solo la codifica da meta/charset
byte[] bytes = response.body(); // da HttpClient/OkHttp come byte[]
InputStream in = new ByteArrayInputStream(bytes);
Document doc = Jsoup.parse(in, null, "https://example.com");
// ^^^^ null = rilevamento automatico del charsetSe invece il charset è noto in anticipo, passalo esplicitamente:
Document doc = Jsoup.parse(in, "ISO-8859-1", "https://example.com");Decodifica manuale
Quando non fai il parsing con jsoup, decodifica i byte per conto tuo:
import java.nio.charset.Charset;
// se conosci la codifica
String html = new String(bytes, Charset.forName("ISO-8859-1"));
// per UTF-8
String html2 = new String(bytes, StandardCharsets.UTF_8);Rilevamento automatico della codifica
Se la codifica non è indicata da nessuna parte, aiutano le librerie di rilevamento: juniversalchardet (port di universalchardet di Mozilla) o ICU4J (CharsetDetector).
// anche org.apache.tika:tika-core sa rilevare il charset
import org.apache.tika.parser.txt.CharsetDetector;
CharsetDetector detector = new CharsetDetector();
detector.setText(bytes);
String charset = detector.detect().getName(); // es. "ISO-8859-1"
String html = new String(bytes, charset);Checklist per le lettere accentate
- Non convertire mai la risposta in stringa UTF-8 «alla cieca».
- Passa a jsoup un
InputStream/byte[], non unaString. - Salva i file con una codifica esplicita:
Files.write(path, html.getBytes(StandardCharsets.UTF_8)). - Anche in console compaiono artefatti — imposta
-Dfile.encoding=UTF-8e verifica la codifica del terminale.
5. Status della risposta e header
Fare il parsing del body alla cieca non si può — prima verifica che il server abbia restituito davvero la pagina, e non un redirect, un captcha o un errore.
HttpResponse<byte[]> resp = client.send(request, BodyHandlers.ofByteArray());
int status = resp.statusCode();
HttpHeaders headers = resp.headers();
// lettura di header specifici
String contentType = headers.firstValue("Content-Type").orElse("");
String server = headers.firstValue("Server").orElse("");
long length = headers.firstValueAsLong("Content-Length").orElse(-1);
// tutti gli header
headers.map().forEach((k, v) -> System.out.println(k + ": " + v));Che cosa è importante monitorare:
| Codice | Significato | Reazione dello scraper |
|---|---|---|
200 |
OK | facciamo il parsing |
301/302 |
redirect | seguiamo Location (o attiviamo il follow-redirects) |
403 |
accesso negato | probabilmente anti-bot — cambiamo UA/proxy |
404 |
non trovato | segniamo l’URL come morto |
429 |
troppe richieste | aspettiamo Retry-After, riduciamo il ritmo |
5xx |
errore del server | retry con ritardo esponenziale |
Header di risposta utili: Content-Type (tipo e codifica), Set-Cookie (vedi la sezione 6), Location (redirect), Retry-After (pausa su 429/503), ETag/Last-Modified (per le richieste condizionali e la scansione incrementale).
In OkHttp è tutto analogo: response.code(), response.header("Content-Type"), response.headers().
6. Lavorare con i cookie
I cookie servono per le sessioni, l’autenticazione, il superamento delle «verifiche anti-bot» e il mantenimento dello stato tra una richiesta e l’altra.
6.1. HttpClient integrato + CookieManager
import java.net.*;
CookieManager cookieManager = new CookieManager();
cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ALL);
HttpClient client = HttpClient.newBuilder()
.cookieHandler(cookieManager) // i cookie ora vengono salvati automaticamente
.build();
// dopo alcune richieste, ispezionare lo store:
CookieStore store = cookieManager.getCookieStore();
store.getCookies().forEach(c ->
System.out.println(c.getName() + "=" + c.getValue()));Ora i cookie viaggiano da soli tra le richieste — comodo per il login: prima un POST con username e password, poi le richieste alle pagine protette con lo stesso client.
6.2. OkHttp + CookieJar
// La via più semplice: un PersistentCookieJar già pronto o un CookieJar in-memory scritto da te
OkHttpClient client = new OkHttpClient.Builder()
.cookieJar(new JavaNetCookieJar(cookieManager))
.build();6.3. jsoup — passaggio manuale
jsoup conserva i cookie nell’oggetto Connection.Response, e puoi passarli avanti a mano:
Connection.Response login = Jsoup.connect("https://site.com/login")
.data("username", "user", "password", "pass")
.method(Connection.Method.POST)
.execute();
Map<String, String> cookies = login.cookies();
Document page = Jsoup.connect("https://site.com/profile")
.cookies(cookies) // passiamo la sessione
.get();Insidia: nello scraping multithread un’unica sessione condivisa può «trascinare» lo stato da un thread all’altro. Per account/proxy diversi crea store di cookie separati per ogni thread o worker.
7. HTTPS/SSL
La maggior parte dei siti è in HTTPS. In condizioni normali l’HttpClient integrato o OkHttp fanno tutto da soli: verificano il certificato con le CA radice attendibili del JDK. Serve codice a parte solo in due casi.
7.1. Certificati self-signed e «problematici»
A volte il sito target ha un certificato self-signed o scaduto. Viene la tentazione di disattivare la verifica:
// ⚠️ PERICOLOSO: disattiva tutta la verifica SSL — solo per test o reti fidate!
TrustManager[] trustAll = new TrustManager[]{
new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] c, String a) {}
public void checkServerTrusted(X509Certificate[] c, String a) {}
public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
}
};
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(null, trustAll, new SecureRandom());
HttpClient client = HttpClient.newBuilder()
.sslContext(sc)
.build();In produzione non va fatto — spalanca la porta agli attacchi MITM. La cosa corretta è aggiungere il certificato specifico a un truststore personalizzato:
keytool -import -alias mysite -file mysite.crt -keystore custom.jksKeyStore ks = KeyStore.getInstance("JKS");
try (InputStream in = Files.newInputStream(Path.of("custom.jks"))) {
ks.load(in, "changeit".toCharArray());
}
TrustManagerFactory tmf =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ks);
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(null, tmf.getTrustManagers(), null);7.2. Fingerprint TLS (JA3) — anti-bot avanzato
Le protezioni serie (Cloudflare e altre) distinguono i client dal fingerprint TLS (JA3/JA4): l’insieme di cifrari ed estensioni nel ClientHello. Quello del client Java standard «non sembra da browser», e riconoscere il bot diventa facile. Falsificarlo del tutto con un JDK puro è complicato; in pratica si usano wrapper del tipo curl-impersonate o utls oppure si fa passare il traffico da un browser reale (sezione 11). È un tema di nicchia, ma importante per i siti «tosti».
8. Proxy
I proxy servono per: distribuire il carico su IP diversi, aggirare le restrizioni geografiche e ridurre il rischio di ban per IP nello scraping massivo. Tipi: datacenter (economici, facili da rilevare), residenziali (costosi, sembrano un utente reale), mobili (i più «puliti»).
8.1. HttpClient integrato
import java.net.*;
HttpClient client = HttpClient.newBuilder()
.proxy(ProxySelector.of(new InetSocketAddress("proxy.host", 8080)))
.build();Con autenticazione del proxy tramite username e password:
Authenticator auth = new Authenticator() {
@Override protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication("user", "pass".toCharArray());
}
};
HttpClient client = HttpClient.newBuilder()
.proxy(ProxySelector.of(new InetSocketAddress("proxy.host", 8080)))
.authenticator(auth)
.build();Dettaglio del JDK: per il proxying può servire la proprietà di sistema
-Djdk.http.auth.tunneling.disabledSchemes="", altrimenti l’autenticazione Basic sul tunnel CONNECT fallisce in silenzio.
8.2. OkHttp (spesso più comodo per i proxy)
Proxy proxy = new Proxy(Proxy.Type.HTTP,
new InetSocketAddress("proxy.host", 8080));
OkHttpClient client = new OkHttpClient.Builder()
.proxy(proxy)
.proxyAuthenticator((route, response) -> {
String credential = Credentials.basic("user", "pass");
return response.request().newBuilder()
.header("Proxy-Authorization", credential)
.build();
})
.build();OkHttp supporta anche Proxy.Type.SOCKS — ci servirà per TOR.
8.3. Rotazione dei proxy
Per lo scraping massivo si tiene un pool di proxy e li si alterna in modo ciclico o al primo ban. La rotazione più semplice:
List<Proxy> pool = loadProxies();
AtomicInteger idx = new AtomicInteger();
Proxy next() {
return pool.get(idx.getAndIncrement() % pool.size());
}In pratica si aggiungono: il controllo di «vitalità» dei proxy, una ban-list di quelli «bruciati», il legame di sessione/cookie a un proxy specifico e, per ogni proxy, un proprio OkHttpClient già configurato (costano poco se condividono il pool di connessioni, ma è meglio riutilizzarli).
9. Scraping attraverso TOR
TOR offre anonimato e un cambio di IP gratuito, ma è lento e molti siti bloccano i suoi nodi di uscita. Va bene per task mirati, non per uno scraping ad alta velocità.
9.1. Preparazione
- Installa TOR (il pacchetto
toro Tor Browser). - TOR apre un proxy SOCKS5 locale, di solito su
127.0.0.1:9050(per Tor Browser,9150). - Per cambiare circuito (nuovo IP) si usa la ControlPort (
9051) con il comandoNEWNYM.
Un torrc minimo:
SocksPort 9050
ControlPort 9051
CookieAuthentication 19.2. Richieste attraverso TOR (SOCKS5)
Proxy torProxy = new Proxy(Proxy.Type.SOCKS,
new InetSocketAddress("127.0.0.1", 9050));
OkHttpClient client = new OkHttpClient.Builder()
.proxy(torProxy)
.build();
Request req = new Request.Builder()
.url("https://check.torproject.org/api/ip") // verifica che siamo dentro TOR
.build();
try (Response resp = client.newCall(req).execute()) {
System.out.println(resp.body().string()); // {"IsTor":true,...}
}Importante: con SOCKS5 la risoluzione DNS deve passare dal proxy (remote DNS), altrimenti la query DNS «trapela» dal tuo IP reale. OkHttp con
Proxy.Type.SOCKSne tiene conto; se usi il JDK puro, fai attenzione alle fughe DNS.
9.3. Cambio di IP con il comando NEWNYM
import java.io.*;
import java.net.Socket;
void newTorIdentity() throws IOException {
try (Socket s = new Socket("127.0.0.1", 9051);
PrintWriter out = new PrintWriter(s.getOutputStream(), true);
BufferedReader in = new BufferedReader(
new InputStreamReader(s.getInputStream()))) {
out.println("AUTHENTICATE \"\""); // oppure la password, se impostata
in.readLine(); // aspettiamo il 250 OK
out.println("SIGNAL NEWNYM"); // nuovo circuito
in.readLine();
}
}Dopo NEWNYM conviene fare una pausa (TOR impiega qualche secondo a costruire il nuovo circuito) e non invocare il comando troppo spesso.
10. Multithreading
Lo scraping è un task I/O-bound (per la maggior parte del tempo aspettiamo la rete), quindi la parallelizzazione dà un guadagno enorme. L’essenziale è non «stendere» il sito target e non beccarsi un ban.
10.1. ExecutorService — il classico
import java.util.concurrent.*;
ExecutorService pool = Executors.newFixedThreadPool(10);
List<Future<String>> futures = new ArrayList<>();
for (String url : urls) {
futures.add(pool.submit(() -> fetchAndParse(url)));
}
for (Future<String> f : futures) {
try {
String result = f.get(30, TimeUnit.SECONDS);
// salvare il risultato
} catch (Exception e) {
// log + retry/salto
}
}
pool.shutdown();10.2. Thread virtuali (Java 21+) — perfetti per l’I/O
I thread virtuali creano migliaia di thread «a basso costo» che restano in attesa sulla rete — esattamente quello che serve allo scraping:
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (String url : urls) {
executor.submit(() -> fetchAndParse(url));
}
} // close() attende il completamento di tutti i task10.3. CompletableFuture + HttpClient asincrono
List<CompletableFuture<HttpResponse<byte[]>>> calls = urls.stream()
.map(u -> client.sendAsync(buildRequest(u), BodyHandlers.ofByteArray()))
.toList();
CompletableFuture.allOf(calls.toArray(new CompletableFuture[0])).join();10.4. Che cosa considerare assolutamente
- Limite di concorrenza per dominio. 200 thread su un solo sito = DoS e ban immediato. Metti un limite, per esempio con un
Semaphoreper dominio. - Throttling. Inserisci ritardi/jitter tra le richieste (vedi la sezione 12).
- Thread-safety. Meglio non condividere un
Documentdi jsoup tra thread; per le collezioni condiviseConcurrentHashMap,ConcurrentLinkedQueue; per i contatoriAtomicInteger/LongAdder. - Backpressure. Non riversare in coda un milione di task in un colpo solo — usa una coda limitata (
ArrayBlockingQueue) eCallerRunsPolicy, così il producer rallenta. - Un solo
OkHttpClient/HttpClientper tutta l’applicazione. Sono thread-safe e mantengono il pool di connessioni; crearne uno a ogni richiesta è un antipattern.
11. Rendering JavaScript
jsoup vede solo l’HTML arrivato nella risposta. Se il sito è una SPA (React/Vue/Angular) e i dati vengono disegnati dal JavaScript nel browser, nell’HTML «grezzo» non ci saranno. Le opzioni:
- Trovare l’API interna (vedi 3.3) — quasi sempre la strada migliore: più veloce, più stabile, senza browser.
- Selenium WebDriver — il controllo di un browser vero (Chrome/Firefox via WebDriver). Pesante, lento, ma vede tutto quello che vede l’utente.
- Playwright for Java — l’alternativa moderna a Selenium, di Microsoft: più veloce, API più comoda, gestisce bene contenuti dinamici e attese.
- HtmlUnit — un browser leggero integrato in Java, esegue JS in modo limitato ma senza binari esterni.
// Selenium (Chrome headless)
ChromeOptions options = new ChromeOptions();
options.addArguments("--headless=new");
WebDriver driver = new ChromeDriver(options);
driver.get("https://spa-site.com");
// aspettare la comparsa dell'elemento
new WebDriverWait(driver, Duration.ofSeconds(10))
.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector(".item")));
String renderedHtml = driver.getPageSource();
// poi si può passare tutto a jsoup per un parsing comodo:
Document doc = Jsoup.parse(renderedHtml);
driver.quit();I motori browser risolvono di passaggio una parte dei problemi anti-bot (fingerprint TLS corretto, esecuzione delle challenge JS), ma lo pagano in risorse: una singola istanza di Chrome divora centinaia di megabyte di RAM, quindi scalarle costa caro.
12. Protezione anti-bot
Più lo scraping è massivo, più alta è la probabilità di un blocco. «Cortesia» di base e mimetizzazione:
- User-Agent. Imposta uno UA realistico di un browser vero; per i task massivi, ruota una lista di UA. Lo UA di default di Java è un campanello d’allarme per il sito.
- Set completo di header. Un browser reale invia
Accept,Accept-Language,Accept-Encoding,Referer,Sec-Fetch-*. Imitali. - Ritardi e jitter. Una pausa casuale (per esempio 1–5 s) tra le richieste, al posto di una «mitragliatrice» a ritmo costante.
- Retry con ritardo esponenziale su
429/5xx, rispettandoRetry-After. - Rotazione degli IP (sezione 8) — lo strumento principale contro i ban per IP.
- Captcha. Se compare un captcha, le opzioni sono: rallentare il ritmo, cambiare IP, usare servizi di risoluzione (2Captcha/Anti-Captcha) — ma qui siamo già in zona grigia, valuta i rischi.
- Cloudflare e challenge JS. Spesso si superano solo con un browser reale (sezione 11) o con librerie di bypass specializzate.
Scheletro dei retry:
int attempts = 0;
while (attempts < MAX_RETRIES) {
HttpResponse<byte[]> r = client.send(req, BodyHandlers.ofByteArray());
int code = r.statusCode();
if (code == 200) return r.body();
if (code == 429 || code >= 500) {
long backoff = (long) (Math.pow(2, attempts) * 1000)
+ ThreadLocalRandom.current().nextInt(500); // jitter
Thread.sleep(backoff);
attempts++;
} else {
break; // 403/404 — il retry non aiuterà
}
}13. Archiviazione degli URL e code
Un crawler è, in sostanza, la visita di un grafo di link. Servono due strutture:
- La coda (frontier) — gli URL ancora da visitare.
- L’insieme dei visitati (visited) — per non passare due volte sullo stesso URL e non finire in loop.
In-memory (per task piccoli)
Queue<String> frontier = new ConcurrentLinkedQueue<>();
Set<String> visited = ConcurrentHashMap.newKeySet();
if (visited.add(url)) { // add restituisce false se l'URL c'era già
frontier.offer(url);
}Parsing degli URL
Prima di normalizzare o filtrare un link, bisogna scomporlo nelle sue parti (schema, host, porta, path, query, frammento). In Java c’è per questo lo standard java.net.URI:
import java.net.URI;
URI uri = URI.create("https://Site.com:443/catalog/item?id=7&ref=a#section");
String scheme = uri.getScheme(); // https
String host = uri.getHost(); // Site.com
int port = uri.getPort(); // 443
String path = uri.getPath(); // /catalog/item
String query = uri.getQuery(); // id=7&ref=a
String fragment = uri.getFragment(); // section
// risoluzione di un link relativo in assoluto:
URI abs = uri.resolve("../other"); // https://Site.com:443/otherjsoup fa lo stesso in automatico per i link della pagina — link.absUrl("href") (vedi la sezione 3) restituisce già l’URL assoluto basandosi sul baseUri. La scomposizione in componenti con URI serve proprio per la normalizzazione successiva.
Gli stessi principi di analisi degli URL valgono anche negli altri stack — vedi l’articolo dedicato «Parsing di URL».
Che cosa conta
- Normalizzazione degli URL prima della deduplica (appoggiandosi al parsing degli URL): togliere le ancore
#..., ordinare i parametri della query, uniformare maiuscole e minuscole dell’host, decidere suhttp/httpse sulla barra finale. Altrimentisite.com/aesite.com/a/verranno contati come diversi. - Deduplica su grandi volumi. Tenere centinaia di milioni di URL in un
HashSetè irrealistico per la memoria — si usa un filtro di Bloom (BloomFilterdi Guava) o si sposta l’insieme in Redis/un database. - Persistenza. Nelle scansioni lunghe la coda e i visited devono sopravvivere a un riavvio: Redis (
List/Set), Kafka/RabbitMQ come coda di task, un database relazionale o uno storage embedded. - Priorità. A volte serve una
PriorityBlockingQueue(prima le sezioni importanti) o un limite di profondità della scansione. - Distribuzione. Quando si scala su molte macchine, coda e deduplica si spostano su un broker esterno/Redis, così i worker non duplicano il lavoro.
Architettura tipica: il producer estrae gli URL dal frontier → il pool di worker scarica e fa il parsing → i link estratti vengono filtrati, normalizzati, controllati contro visited e rimessi nel frontier; i dati vengono scritti nello storage.
14. Framework pronti all’uso
Per non scrivere un crawler da zero, esistono soluzioni già pronte:
- crawler4j — un crawler multithread semplice in Java, partenza rapida.
- webmagic — un framework flessibile (ispirato a Scrapy) con pipeline e scheduler.
- Apache Nutch — un crawler pesante e scalabile di livello industriale (spesso in coppia con Hadoop/Solr).
- StormCrawler — un crawler distribuito sopra Apache Storm per l’elaborazione in streaming.
Per la maggior parte dei task applicativi la combinazione «OkHttp/HttpClient + jsoup + un pool di thread proprio + una coda su Redis» è più che sufficiente, ed è più trasparente della «magia» dei framework.
15. Pro e contro
I pro dell’implementazione in Java
- Prestazioni e multithreading. La JVM regge benissimo una concorrenza alta; i thread virtuali (Java 21+) portano lo scraping I/O-bound a un altro livello.
- Ecosistema maturo. jsoup, OkHttp, Apache HttpClient, Selenium/Playwright, Jackson — tutto stabile e ben documentato.
- Affidabilità e sicurezza dei tipi. La tipizzazione rigorosa intercetta gli errori in compilazione; comoda per crawler grandi e longevi.
- Integrazione con lo stack enterprise. Facile da inserire in un servizio Spring e da collegare a Kafka, ai database, al monitoraggio.
- Multipiattaforma. Un solo JAR — e gira ovunque.
I contro
- Verbosità. Il codice è più lungo che in Python; un prototipo rapido con
requests + BeautifulSoupsi scrive in meno righe. - Meno strumenti specifici per lo scraping. In Python l’ecosistema di scraping (Scrapy e simili) è più ricco e popolare.
- I siti JavaScript sono una sofferenza. Senza un browser (Selenium/Playwright) i contenuti dinamici non si prendono, e i browser sono pesanti e voraci.
- La corsa agli armamenti anti-bot. Fingerprint TLS, analisi comportamentale, captcha — tutto questo richiede una manutenzione costante; il client Java standard si rileva con facilità.
- Codifiche. Le lettere accentate e
ISO-8859-1richiedono attenzione (sezione 4) — una fonte tipica di bug. - Fragilità. Qualsiasi scraper si rompe quando cambia il markup del sito; i selettori vanno mantenuti.
Quando Java è una buona scelta
Un crawler grande, longevo e ad alto carico; un team già su stack Java; il bisogno di un multithreading rigoroso e di integrazione con l’infrastruttura. Per il singolo «tirare fuori una tabellina da una pagina», Python di solito è più rapido in tempi di sviluppo.
Le versioni delle librerie sono indicate al 2026 (jsoup 1.22.2). Prima di usarle, controlla le versioni correnti su Maven Central — l’API potrebbe essere cambiata.