Le scraping par langage 21 min de lecture

Web scraping en Java : le guide complet

Guide complet du web scraping en Java : Jsoup, HtmlUnit, Selenium, multithreading et architecture industrielle de scrapers.

ÉW
Équipe Web-Scraping.fr
Collecte de données pour votre activité
Publié le: 26 mars 2025

Article de synthèse sur la collecte de données de pages web en Java — de la simple récupération d'une page unique au crawler multithread avec proxys, TOR et contournement des problèmes courants. La structure va « du simple au complexe » : chaque section s'appuie sur la précédente.


Sommaire

  1. Introduction : qu'est-ce que le web scraping et le cadre juridique
  2. Comment récupérer une page : les clients HTTP
  3. Bibliothèques d'analyse du contenu
  4. Résoudre les problèmes de cyrillique (encodages)
  5. Récupérer le statut de la réponse et les en-têtes
  6. Gestion des cookies
  7. Gestion du HTTPS/SSL
  8. Utilisation des proxys
  9. Le scraping via TOR
  10. Multithreading
  11. Rendu JavaScript : les sites dynamiques
  12. Protection anti-bot : User-Agent, délais, retries
  13. Stockage des URL et files d'attente
  14. Frameworks de crawling prêts à l'emploi
  15. Avantages et inconvénients de l'implémentation en Java

1. Introduction

Le web scraping — l'extraction automatisée de données depuis des pages web. Le processus se divise grossièrement en deux étapes qu'il est important de ne pas confondre :

  • Le téléchargement (fetching / crawling) — la récupération du HTML (ou JSON/XML) via HTTP. C'est le rôle du client HTTP.
  • L'analyse (parsing / extraction) — la transformation du HTML « brut » en structure et la sélection des données voulues via des sélecteurs. C'est le rôle du parseur.

Cadre juridique et éthique

Avant d'écrire du code, il faut garder plusieurs choses à l'esprit — ce n'est pas un conseil juridique, mais un minimum d'hygiène :

  • robots.txt — un fichier à la racine du site (https://site.com/robots.txt) où le propriétaire décrit ce que les robots sont autorisés à explorer. Il n'a presque nulle part de valeur juridique, mais l'ignorer est de mauvais ton et constitue un déclencheur fréquent de bannissement.
  • Les conditions d'utilisation (ToS) du site peuvent interdire explicitement la collecte automatique. Enfreindre les ToS est un risque contractuel, et dans certaines juridictions un risque bien plus grave.
  • Données personnelles. La collecte de données personnelles est encadrée par la loi. Soyez prudent.
  • Charge serveur. Un scraping agressif = un DoS pour le serveur d'autrui. Mettez des délais, limitez le nombre de threads, respectez Retry-After et les codes 429/503.

Techniquement, Java permet presque tout. La responsabilité du « quoi » et du « pourquoi » vous incombe.


2. Comment récupérer une page

C'est le fondement : sans réponse correctement récupérée, il n'y a rien à analyser. Java offre plusieurs options de client HTTP.

2.1. Le java.net.http.HttpClient intégré (Java 11+)

Le client standard moderne. Aucune dépendance requise, prise en charge de HTTP/2, modes synchrone et asynchrone.

java
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", "ru-RU,ru;q=0.9")
        .GET()
        .build();

HttpResponse<String> response =
        client.send(request, HttpResponse.BodyHandlers.ofString());

System.out.println("Status: " + response.statusCode());
String html = response.body();

Détail important sur l'encodage : BodyHandlers.ofString() sans argument décode le corps en UTF-8. Si le site est dans un autre encodage (par exemple windows-1251), vous obtiendrez du « charabia » — voir la section 4. Pour l'éviter, on récupère souvent les octets, et l'encodage est déterminé séparément :

java
HttpResponse<byte[]> resp =
        client.send(request, HttpResponse.BodyHandlers.ofByteArray());
byte[] raw = resp.body();   // on décode plus tard, en connaissant le charset

2.2. OkHttp

Bibliothèque tierce populaire (Square). API pratique, pools de connexions, intercepteurs, gestion simple des proxys et des cookies. Un bon choix « par défaut » pour du scraping sérieux.

java
// 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();   // encore : mieux vaut les octets
}

2.3. Apache HttpClient 5

Bibliothèque mature, puissante et un peu plus verbeuse. Réglage fin des connexions, des pools, de l'authentification. Fréquente en entreprise.

java
// 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. Le téléchargement directement via jsoup

jsoup (voir la section 3) sait télécharger la page lui-même. C'est pratique pour les prototypes, mais son moteur HTTP intégré est moins flexible (proxys, pools, réglage fin) ; pour des tâches en production, on combine donc souvent ainsi : on télécharge avec un client puissant → on analyse avec jsoup.

java
Document doc = Jsoup.connect("https://example.com")
        .userAgent("Mozilla/5.0 ...")
        .timeout(15_000)
        .get();

Que choisir

Client Quand l'utiliser
java.net.http.HttpClient Vous ne voulez pas ajouter de dépendances, Java 11+, besoin de HTTP/2
OkHttp Choix polyvalent, proxys/cookies/intercepteurs pratiques
Apache HttpClient 5 Entreprise, contrôle fin, authentification complexe
jsoup .connect() Prototypes et tâches simples « télécharger-analyser »

3. Bibliothèques d'analyse

Une fois le HTML récupéré, il faut en extraire les données. Analyser le HTML avec des expressions régulières est à éviter — le HTML n'est pas un langage régulier, et un tel parseur se casse sur le moindre balisage non standard.

3.1. jsoup — le cheval de trait

jsoup (version actuelle en 2026 — 1.22.2) implémente la spécification WHATWG HTML5 et construit le même DOM que les navigateurs. Il prend en charge les sélecteurs CSS et XPath, et pardonne le HTML « sale ».

java
// 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"); // 2e arg — baseUri pour les liens absolus

// sélecteurs CSS
Elements links = doc.select("a[href]");
for (Element link : links) {
    String text = link.text();
    String absUrl = link.absUrl("href");  // URL absolue
}

// sélection ciblée
Element title = doc.selectFirst("h1.article-title");
String price = doc.select("span.price").text();

// attributs
String img = doc.selectFirst("img").attr("src");

Quelques techniques courantes avec les sélecteurs :

java
doc.select("div.product");              // par classe
doc.select("#main-content");            // par id
doc.select("ul.menu > li");             // enfants directs
doc.select("a[href^=https]");           // l'attribut commence par
doc.select("table tr:nth-child(2n)");   // pseudo-sélecteurs
doc.select("p:contains(Цена)");         // par texte

3.2. HtmlUnit — le « navigateur sans interface »

HtmlUnit est un navigateur GUI-less en Java. Il sait exécuter du JavaScript (de façon limitée), ce qui permet parfois de récupérer des données de sites dynamiques sans le lourd Selenium. Plus lent que jsoup, mais plus puissant.

3.3. Quand le HTML n'est pas nécessaire

Très souvent, les données de la page sont chargées par une requête séparée vers une API interne (JSON). Ouvrez les DevTools → onglet Network, trouvez le XHR/fetch voulu — et analysez du JSON pur avec Jackson ou Gson. C'est bien plus fiable et rapide que de décortiquer du HTML. Vérifiez toujours cette piste en premier.

java
// com.fasterxml.jackson.core:jackson-databind
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(jsonString);
String name = root.path("data").path("name").asText();

4. Encodages et Unicode

Le problème le plus courant sur le web russophone — le « charabia » (привет ou ���). La cause est toujours la même : les octets ont été décodés dans le mauvais encodage.

Pourquoi cela se produit

HTTP renvoie des octets. Pour obtenir une chaîne, il faut les décoder dans le bon encodage. L'encodage peut être indiqué :

  1. dans l'en-tête HTTP Content-Type: text/html; charset=windows-1251 ;
  2. dans <meta charset="..."> ou <meta http-equiv="Content-Type"> à l'intérieur du HTML ;
  3. nulle part (il faut alors deviner).

Si vous avez lu le corps en UTF-8 alors que le site est en windows-1251 (grand classique du vieux web russophone) — vous obtiendrez des données illisibles.

Règle : travaillez avec les octets, déterminez l'encodage explicitement

jsoup gère cela presque automatiquement, si on lui donne des octets ou un InputStream, plutôt qu'une chaîne toute faite — il lit lui-même le charset depuis l'en-tête/<meta> :

java
// Correct : jsoup détecte lui-même l'encodage depuis meta/charset
byte[] bytes = response.body();  // depuis HttpClient/OkHttp sous forme de byte[]
InputStream in = new ByteArrayInputStream(bytes);
Document doc = Jsoup.parse(in, null, "https://example.com");
//                              ^^^^ null = détection automatique du charset

Si le charset est connu à l'avance, passez-le explicitement :

java
Document doc = Jsoup.parse(in, "windows-1251", "https://example.com");

Décodage manuel

Quand vous n'analysez pas via jsoup, décodez les octets vous-même :

java
import java.nio.charset.Charset;

// si vous connaissez l'encodage
String html = new String(bytes, Charset.forName("windows-1251"));

// pour l'UTF-8
String html2 = new String(bytes, StandardCharsets.UTF_8);

Détection automatique de l'encodage

Si l'encodage n'est indiqué nulle part, des bibliothèques de détection aident : juniversalchardet (portage de universalchardet de Mozilla) ou ICU4J (CharsetDetector).

java
// org.apache.tika:tika-core sait aussi détecter le charset
import org.apache.tika.parser.txt.CharsetDetector;

CharsetDetector detector = new CharsetDetector();
detector.setText(bytes);
String charset = detector.detect().getName(); // p. ex. "windows-1251"
String html = new String(bytes, charset);

Checklist encodages

  • Ne convertissez jamais la réponse en chaîne UTF-8 « à l'aveugle ».
  • Donnez à jsoup un InputStream/byte[], plutôt qu'un String.
  • Enregistrez les fichiers avec un encodage explicite : Files.write(path, html.getBytes(StandardCharsets.UTF_8)).
  • La console aussi peut produire des artefacts — définissez -Dfile.encoding=UTF-8 et vérifiez l'encodage du terminal.

5. Statut de la réponse et en-têtes

Il ne faut pas analyser le corps aveuglément — vérifiez d'abord que le serveur a réellement renvoyé une page, et non une redirection, un captcha ou une erreur.

java
HttpResponse<byte[]> resp = client.send(request, BodyHandlers.ofByteArray());

int status = resp.statusCode();
HttpHeaders headers = resp.headers();

// lecture d'en-têtes spécifiques
String contentType = headers.firstValue("Content-Type").orElse("");
String server      = headers.firstValue("Server").orElse("");
long length        = headers.firstValueAsLong("Content-Length").orElse(-1);

// tous les en-têtes
headers.map().forEach((k, v) -> System.out.println(k + ": " + v));

Ce qu'il faut surveiller :

Code Signification Réaction du scraper
200 OK on analyse
301/302 redirection on suit Location (ou on active follow-redirects)
403 accès refusé probablement un anti-bot — on change d'UA/de proxy
404 introuvable on marque l'URL comme morte
429 trop de requêtes on attend Retry-After, on ralentit le rythme
5xx erreur serveur retry avec backoff exponentiel

En-têtes de réponse utiles : Content-Type (type et encodage), Set-Cookie (voir la section 6), Location (redirection), Retry-After (pause en cas de 429/503), ETag/Last-Modified (pour les requêtes conditionnelles et le crawl incrémental).

Dans OkHttp, tout est analogue : response.code(), response.header("Content-Type"), response.headers().


Les cookies servent aux sessions, à l'authentification, au passage des « vérifications anti-bot » et à la conservation de l'état entre les requêtes.

6.1. Le HttpClient intégré + CookieManager

java
import java.net.*;

CookieManager cookieManager = new CookieManager();
cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ALL);

HttpClient client = HttpClient.newBuilder()
        .cookieHandler(cookieManager)   // les cookies sont désormais conservés automatiquement
        .build();

// après quelques requêtes, inspecter le stockage :
CookieStore store = cookieManager.getCookieStore();
store.getCookies().forEach(c ->
        System.out.println(c.getName() + "=" + c.getValue()));

Désormais, les cookies sont transmis automatiquement entre les requêtes — pratique pour la connexion : d'abord un POST avec identifiant/mot de passe, puis les requêtes vers les pages protégées avec le même client.

6.2. OkHttp + CookieJar

java
// Le plus simple — un PersistentCookieJar tout prêt ou votre propre CookieJar in-memory
OkHttpClient client = new OkHttpClient.Builder()
        .cookieJar(new JavaNetCookieJar(cookieManager))
        .build();

6.3. jsoup — transmission manuelle

jsoup stocke les cookies dans l'objet Connection.Response, et on peut les transmettre plus loin manuellement :

java
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)      // on transmet la session
        .get();

Piège : en scraping multithread, une session partagée unique peut « faire circuler » l'état entre les threads. Pour des comptes/proxys différents, créez des stockages de cookies séparés pour chaque thread ou worker.


7. HTTPS/SSL

La plupart des sites sont en HTTPS. En temps normal, le HttpClient/OkHttp intégré fait tout seul : il vérifie le certificat via les CA racines de confiance du JDK. Du code spécifique n'est nécessaire que dans deux cas.

7.1. Certificats auto-signés et « problématiques »

Parfois, le site cible a un certificat auto-signé ou expiré. La tentation est grande de désactiver la vérification :

java
// ⚠️ DANGER : désactive toute vérification SSL — uniquement pour un test / réseau de confiance !
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();

À ne pas faire en production — cela ouvre la porte aux attaques MITM. Il vaut mieux ajouter le certificat précis à un truststore personnalisé :

bash
keytool -import -alias mysite -file mysite.crt -keystore custom.jks
java
KeyStore 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. Empreinte TLS (JA3) — anti-bot avancé

Les protections sérieuses (Cloudflare et autres) distinguent les clients par leur empreinte TLS (JA3/JA4) : l'ensemble des chiffrements et extensions du ClientHello. Celle du client Java standard n'a « rien d'un navigateur », et permet de repérer facilement un bot. La falsifier entièrement avec un JDK pur est difficile ; en pratique, on utilise des outils du type wrappers curl-impl/utls ou on fait passer le trafic par un vrai navigateur (section 11). C'est un sujet de niche, mais important pour les sites « coriaces ».


8. Proxys

Les proxys servent à : répartir la charge sur différentes IP, contourner les restrictions géographiques et réduire le risque de bannissement par IP lors d'un scraping massif. Types : datacenter (bon marché, facilement détectable), residential (cher, ressemble à un vrai utilisateur), mobile (les plus « propres »).

8.1. Le HttpClient intégré

java
import java.net.*;

HttpClient client = HttpClient.newBuilder()
        .proxy(ProxySelector.of(new InetSocketAddress("proxy.host", 8080)))
        .build();

Avec authentification du proxy par identifiant/mot de passe :

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

Subtilité du JDK : le proxy peut nécessiter la propriété système -Djdk.http.auth.tunneling.disabledSchemes="", sinon l'authentification Basic sur le tunnel CONNECT échoue silencieusement.

8.2. OkHttp (souvent plus pratique pour les proxys)

java
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 prend aussi en charge Proxy.Type.SOCKS — cela nous servira pour TOR.

8.3. Rotation des proxys

Pour un scraping massif, on maintient un pool de proxys que l'on fait tourner en boucle ou en cas de bannissement. La rotation la plus simple :

java
List<Proxy> pool = loadProxies();
AtomicInteger idx = new AtomicInteger();

Proxy next() {
    return pool.get(idx.getAndIncrement() % pool.size());
}

En pratique, on ajoute : une vérification de la « validité » des proxys, une liste noire des proxys « grillés », l'association session/cookie à un proxy précis, et pour chaque proxy — son propre OkHttpClient assemblé (ils sont peu coûteux avec un pool de connexions commun, mais mieux vaut les réutiliser).


9. Le scraping via TOR

TOR offre l'anonymat et un changement d'IP gratuit, mais il est lent et de nombreux sites bloquent les nœuds de sortie. Convient à des tâches ponctuelles, pas à du scraping à haute vitesse.

9.1. Préparation

  1. Installez TOR (le paquet tor ou Tor Browser).
  2. TOR lance un proxy SOCKS5 en local, généralement sur 127.0.0.1:9050 (pour Tor Browser — 9150).
  3. Pour changer de circuit (nouvelle IP), on utilise ControlPort (9051) avec la commande NEWNYM.

Un torrc minimal :

code
SocksPort 9050
ControlPort 9051
CookieAuthentication 1

9.2. Requêtes via TOR (SOCKS5)

java
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")  // on vérifie qu'on passe bien par TOR
        .build();

try (Response resp = client.newCall(req).execute()) {
    System.out.println(resp.body().string()); // {"IsTor":true,...}
}

Important : pour SOCKS5, la résolution DNS doit passer par le proxy (remote DNS), sinon la requête DNS « fuit » depuis votre vraie IP. OkHttp avec Proxy.Type.SOCKS en tient compte ; avec un JDK pur, faites attention aux fuites DNS.

9.3. Changer d'IP avec la commande NEWNYM

java
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 \"\"");   // ou le mot de passe, s'il est défini
        in.readLine();                       // on attend 250 OK
        out.println("SIGNAL NEWNYM");        // nouveau circuit
        in.readLine();
    }
}

Après NEWNYM, mieux vaut respecter une pause (TOR met quelques secondes à construire un nouveau circuit) et ne pas solliciter la commande trop souvent.


10. Multithreading

Le scraping est une tâche I/O-bound (on passe le plus clair du temps à attendre le réseau), donc la parallélisation apporte un gain énorme. L'essentiel — ne pas « faire tomber » le site cible et ne pas se faire bannir.

10.1. ExecutorService — le grand classique

java
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);
        // enregistrer le résultat
    } catch (Exception e) {
        // log + retry/skip
    }
}
pool.shutdown();

10.2. Les threads virtuels (Java 21+) — parfaits pour l'I/O

Les threads virtuels créent des milliers de threads « bon marché » en attente sur le réseau — exactement ce qu'il faut pour le scraping :

java
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (String url : urls) {
        executor.submit(() -> fetchAndParse(url));
    }
} // close() attend la fin de toutes les tâches

10.3. CompletableFuture + HttpClient asynchrone

java
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. Ce qu'il faut absolument prendre en compte

  • Limite de concurrence par domaine. 200 threads sur un seul site = DoS et bannissement immédiat. Limitez, par exemple avec un Semaphore par domaine.
  • Throttling. Mettez des délais/du jitter entre les requêtes (voir la section 12).
  • Sûreté des threads. Mieux vaut ne pas partager un jsoup Document entre les threads ; collections partagées — ConcurrentHashMap, ConcurrentLinkedQueue ; compteurs — AtomicInteger/LongAdder.
  • Backpressure. Ne chargez pas un million de tâches d'un coup dans la file — utilisez une file bornée (ArrayBlockingQueue) et CallerRunsPolicy, pour que le producteur ralentisse.
  • Un seul OkHttpClient/HttpClient pour toute l'application. Ils sont thread-safe et maintiennent un pool de connexions ; en créer à chaque requête est un anti-pattern.

11. Rendu JavaScript

jsoup ne voit que le HTML reçu dans la réponse. Si le site est une SPA (React/Vue/Angular) et que les données sont ajoutées par le JavaScript dans le navigateur, elles ne figureront pas dans le HTML « brut ». Les options :

  1. Trouver l'API interne (voir 3.3) — presque toujours la meilleure voie : plus rapide, plus stable, sans navigateur.
  2. Selenium WebDriver — piloter un vrai navigateur (Chrome/Firefox via WebDriver). Lourd, lent, mais voit tout ce que voit l'utilisateur.
  3. Playwright for Java — l'alternative moderne à Selenium par Microsoft : plus rapide, API plus pratique, gère bien le dynamique et les attentes.
  4. HtmlUnit — un navigateur intégré léger en Java, exécute le JS de façon limitée, mais sans binaires externes.
java
// Selenium (Chrome headless)
ChromeOptions options = new ChromeOptions();
options.addArguments("--headless=new");
WebDriver driver = new ChromeDriver(options);
driver.get("https://spa-site.com");

// attendre l'apparition de l'élément
new WebDriverWait(driver, Duration.ofSeconds(10))
    .until(ExpectedConditions.presenceOfElementLocated(By.cssSelector(".item")));

String renderedHtml = driver.getPageSource();
// on peut ensuite le passer à jsoup pour une analyse pratique :
Document doc = Jsoup.parse(renderedHtml);
driver.quit();

Les moteurs de navigateur résolvent au passage une partie des problèmes anti-bot (empreinte TLS correcte, exécution des JS-challenges), mais au prix des ressources : une seule instance de Chrome consomme des centaines de mégaoctets de RAM, ce qui rend leur mise à l'échelle coûteuse.


12. Protection anti-bot

Plus le scraping est massif, plus le risque de blocage est élevé. « Politesse » de base et camouflage :

  • User-Agent. Utilisez un UA réaliste de vrai navigateur ; pour les tâches massives — faites tourner une liste d'UA. L'UA Java par défaut est un signal d'alerte pour le site.
  • Jeu complet d'en-têtes. Un vrai navigateur envoie Accept, Accept-Language, Accept-Encoding, Referer, Sec-Fetch-*. Imitez-les.
  • Délais et jitter. Une pause aléatoire (par exemple 1–5 s) entre les requêtes plutôt qu'un « tir de mitraillette » régulier.
  • Retries avec backoff exponentiel sur 429/5xx, en respectant Retry-After.
  • Rotation des IP (section 8) — l'outil principal contre les bannissements par IP.
  • Captcha. Quand un captcha apparaît, les options : ralentir, changer d'IP, utiliser des services de résolution (2Captcha/Anti-Captcha) — mais c'est déjà une zone grise, évaluez les risques.
  • Cloudflare et JS-challenges. Souvent, on ne les passe qu'avec un vrai navigateur (section 11) ou des bibliothèques de contournement spécialisées.

Squelette de retries :

java
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 — un retry n'aidera pas
    }
}

13. Stockage des URL et files d'attente

Un crawler, c'est essentiellement le parcours d'un graphe de liens. Il faut deux structures :

  • La file (frontier) — les URL qu'il reste à parcourir.
  • L'ensemble des visitées (visited) — pour ne pas parcourir deux fois la même URL et éviter les boucles.

In-memory (pour les petites tâches)

java
Queue<String> frontier = new ConcurrentLinkedQueue<>();
Set<String> visited = ConcurrentHashMap.newKeySet();

if (visited.add(url)) {       // add renvoie false si déjà présent
    frontier.offer(url);
}

Analyse d'URL

Avant de normaliser ou de filtrer un lien, il faut le décomposer en ses éléments (schéma, hôte, port, chemin, query, fragment). Java propose pour cela la classe standard java.net.URI :

java
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

// résolution d'un lien relatif en absolu :
URI abs = uri.resolve("../other");    // https://Site.com:443/other

jsoup fait la même chose automatiquement pour les liens de la page — link.absUrl("href") (voir la section 3) renvoie déjà l'URL absolue, en s'appuyant sur baseUri. La décomposition en composants via URI sert précisément à la normalisation ultérieure.

Les mêmes principes de décomposition d'URL s'appliquent à d'autres stacks — voir l'article dédié « L'analyse d'URL dans d'autres langages ».

Points importants

  • Normalisation des URL avant la déduplication (en s'appuyant sur Analyse d'URL) : supprimer les ancres #..., trier les paramètres query, harmoniser la casse de l'hôte, trancher entre http/https et le slash final. Sinon site.com/a et site.com/a/ seront considérées comme différentes.
  • Déduplication à grande échelle. Garder des centaines de millions d'URL dans un HashSet est irréaliste en mémoire — on utilise un filtre de Bloom (Guava BloomFilter) ou l'externalisation de l'ensemble vers Redis/une BDD.
  • Persistance. Pour les longs parcours, la file et visited doivent survivre à un redémarrage : Redis (List/Set), Kafka/RabbitMQ comme file de tâches, une BDD relationnelle ou un stockage embarqué.
  • Priorités. Parfois, il faut une PriorityBlockingQueue (d'abord les sections importantes) ou une limitation de la profondeur de parcours.
  • Distribution. À l'échelle de nombreuses machines, la file et la déduplication sont externalisées vers un broker/Redis externe, pour que les workers ne dupliquent pas le travail.

Architecture typique : un producteur extrait des URL de la frontier → un pool de workers télécharge et analyse → les liens extraits sont filtrés, normalisés, vérifiés contre visited et remis dans la frontier ; les données sont écrites dans le stockage.


14. Frameworks prêts à l'emploi

Pour ne pas écrire un crawler de zéro, il existe des solutions prêtes à l'emploi :

  • crawler4j — un crawler multithread simple en Java, démarrage rapide.
  • webmagic — un framework flexible (inspiré de Scrapy) avec des pipelines et un planificateur.
  • Apache Nutch — un crawler lourd, scalable, de niveau industriel (souvent couplé à Hadoop/Solr).
  • StormCrawler — un crawler distribué au-dessus d'Apache Storm pour le traitement en flux.

Pour la plupart des tâches concrètes, la combinaison « OkHttp/HttpClient + jsoup + son propre pool de threads + file Redis » est plus que suffisante, et elle est plus transparente que la « magie » des frameworks.


15. Avantages et inconvénients

Avantages de l'implémentation en Java

  • Performance et multithreading. La JVM supporte très bien une forte concurrence ; les threads virtuels (Java 21+) font passer le scraping I/O-bound à un nouveau niveau.
  • Écosystème mature. jsoup, OkHttp, Apache HttpClient, Selenium/Playwright, Jackson — tout est stable et bien documenté.
  • Fiabilité et sûreté du typage. Le typage strict attrape les erreurs à la compilation ; pratique pour de gros crawlers à longue durée de vie.
  • Intégration avec la stack d'entreprise. Facile à intégrer dans un service Spring, à connecter à Kafka, aux bases de données, au monitoring.
  • Portabilité multiplateforme. Un seul JAR — exécution n'importe où.

Inconvénients

  • Verbosité. Il y a plus de code qu'en Python ; un prototype rapide avec requests + BeautifulSoup s'écrit plus court.
  • Moins d'outils « spécifiques au scraping ». En Python, l'écosystème de scraping (Scrapy, etc.) est plus riche et plus populaire.
  • Les sites JavaScript — un calvaire. Sans navigateur (Selenium/Playwright), impossible de récupérer le dynamique, et les navigateurs sont lourds et gourmands.
  • Course à l'armement anti-bot. Empreinte TLS, analyse comportementale, captchas — tout cela demande une maintenance constante ; le client Java standard se détecte facilement.
  • Encodages. Le cyrillique et windows-1251 demandent de la rigueur (section 4) — une source classique de bugs.
  • Fragilité. Tout scraper casse quand la mise en page du site change ; il faut maintenir les sélecteurs.

Quand Java est un bon choix

Un crawler volumineux, à longue durée de vie, à forte charge ; une équipe déjà sur une stack Java ; un besoin de multithreading intensif et d'intégration avec l'infrastructure. Pour un « extraire un tableau d'une seule page » ponctuel, Python est généralement plus rapide en temps de développement.


Les versions des bibliothèques sont indiquées à jour en 2026 (jsoup 1.22.2). Avant utilisation, vérifiez les versions actuelles sur Maven Central — l'API a pu changer.