Le parsing, c'est l'extraction des données utiles depuis des pages web : analyse du balisage HTML, collecte des liens, isolation du contenu textuel. À première vue, la tâche est simple, mais en pratique les pièges surgissent presque toujours : balisage « sale », contenu dynamique, blocages et — point particulièrement sensible sur les sites anciens — les problèmes d'encodage et de caractères accentués.
Dans cet article, nous passons en revue trois scénarios de base (HTML, liens, texte), les problèmes typiques, avec des exemples d'implémentation dans plusieurs langages.
1. Le parsing HTML
Le parsing HTML consiste à construire, à partir d'une chaîne de balisage, un arbre d'éléments (DOM) dans lequel on peut naviguer et sélectionner les nœuds voulus. On travaille le plus souvent via :
- les sélecteurs CSS —
.product .price,div#content > p; - XPath —
//div[@class="product"]//span[@class="price"]; - les expressions régulières — admissibles uniquement pour les cas très simples ; pour du HTML complet, il ne faut pas les utiliser (HTML n'est pas un langage régulier).
L'idée clé : n'analysez pas le HTML avec des regex. Utilisez un parseur spécialisé, capable de réparer un balisage « cassé » exactement comme le fait un navigateur.
2. Le parsing des liens
La collecte des liens est la base de tout crawler. En général, cela consiste à :
- Sélectionner tous les tags
<a>dotés d'un attributhref. - Convertir les liens relatifs en absolus (
/page→https://site.com/page). - Filtrer : écarter
mailto:,tel:,javascript:, les ancres#, les doublons. - Normaliser les URL (casse de l'hôte, slash final, ordre des paramètres de requête).
L'erreur classique du débutant : traiter href comme une adresse absolue prête à l'emploi. Sur les sites réels, les liens sont souvent relatifs, relatifs au protocole (//cdn.site.com/...) ou dépendants du tag <base href>.
3. Le parsing du texte
L'extraction de texte, c'est obtenir le contenu « humain » sans les balises. Points importants :
- supprimer
<script>,<style>, les éléments masqués ; - traiter correctement les espaces, sauts de ligne et espaces insécables (
) ; - décoder les entités HTML (
&→&,é→é,«→«) ; - si possible, séparer le contenu principal de la navigation, de la publicité et du pied de page (le problème dit de « content extraction »).
4. Les problèmes qu'on peut rencontrer
HTML « sale » et invalide. Balises non fermées, imbrication incorrecte, guillemets absents. Un bon parseur pardonne ces fautes, les regex — non.
Contenu dynamique (JavaScript). Si les données arrivent par AJAX/JS, elles sont absentes du HTML initial. Il faut soit un navigateur headless (Selenium, Playwright, Puppeteer), soit interroger directement l'API et les requêtes XHR que fait la page.
Volatilité du balisage. Les sélecteurs cassent à chaque refonte. Mieux vaut choisir des repères stables (id, attributs sémantiques, microdonnées) plutôt que de longues chaînes de classes fragiles.
Protection anti-bots. Captchas, vérification du User-Agent, rate-limiting, blocage par IP, Cloudflare. Il faut des pauses entre les requêtes, une rotation des en-têtes/proxys, le respect de robots.txt et du bon sens.
Encodages et accents. La douleur la plus fréquente sur le web historique — une section entière y est consacrée plus bas.
Performance. Sur de gros volumes comptent l'asynchrone, les pools de connexions, l'analyse en flux (type SAX) plutôt que le chargement de tout le DOM en mémoire.
Aspects juridiques et éthiques. Conditions d'utilisation du site, données personnelles, droits d'auteur, charge imposée au serveur d'autrui.
5. Accents et encodages
La plupart des problèmes de caractères illisibles (été au lieu de été, ou une série de �) se ramènent à une seule cause : les octets ont été lus dans un autre encodage que celui dans lequel ils ont été écrits.
Sur le web francophone, on rencontre trois encodages principaux :
- UTF-8 — le standard moderne, le défaut du web d'aujourd'hui ;
- Windows-1252 (CP1252) — l'encodage hérité, encore vivant sur les vieux sites ;
- ISO-8859-1 / ISO-8859-15 — les variantes « latines » historiques, devenues rares (la seconde ajoute notamment le signe €).
Comment déterminer le bon encodage
L'ordre de priorité, tel que l'applique un navigateur :
- L'en-tête HTTP
Content-Type: text/html; charset=windows-1252. - Le méta-tag dans le HTML :
<meta charset="utf-8">ou<meta http-equiv="Content-Type" content="text/html; charset=windows-1252">. - Le BOM (Byte Order Mark) en début de fichier — pour l'UTF.
- La détection automatique par statistique d'octets (bibliothèques
chardet,charset-normalizer).
Subtilité importante : on ne détermine pas l'encodage d'après une chaîne déjà décodée. L'encodage se détecte sur les octets bruts de la réponse, et c'est seulement ensuite qu'on décode en chaîne.
Erreurs typiques
- Décoder du Windows-1252 comme de l'UTF-8 → erreur ou « bouillie » de caractères.
- Le charset annoncé dans l'en-tête ne correspond pas au réel — le serveur « ment ».
- Encodages mélangés sur une même page.
- Double décodage/encodage (
mojibake). - Oublier de préciser l'encodage à l'écriture du résultat dans un fichier ou une base.
La recette universelle : lire les octets → détecter l'encodage → décoder en Unicode → à l'intérieur du programme, ne travailler qu'en Unicode → encoder en UTF-8 en sortie.
6. Exemples d'implémentation dans différents langages
Dans tous les exemples, la tâche est identique : charger la page, détecter correctement l'encodage, collecter tous les liens et extraire le texte du titre.
Python — requests + BeautifulSoup
Le duo le plus populaire. requests tente lui-même de déterminer l'encodage, et BeautifulSoup digère très bien le HTML « sale ».
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin
url = "https://example.com"
resp = requests.get(url, headers={"User-Agent": "Mozilla/5.0"})
# Détection correcte de l'encodage d'après le contenu (pas seulement l'en-tête)
resp.encoding = resp.apparent_encoding # utilise charset-normalizer/chardet
soup = BeautifulSoup(resp.text, "lxml") # le parseur lxml est rapide et robuste
# Parsing du texte
title = soup.title.get_text(strip=True)
print("Titre :", title)
# Parsing des liens : relatifs -> absolus, avec filtrage
links = set()
for a in soup.select("a[href]"):
href = a["href"].strip()
if href.startswith(("mailto:", "tel:", "javascript:", "#")):
continue
links.add(urljoin(url, href))
print(f"Liens trouvés : {len(links)}")
Si l'encodage doit être déterminé manuellement à partir des octets :
import charset_normalizer
raw = resp.content # les octets bruts
best = charset_normalizer.from_bytes(raw).best()
html = str(best) # déjà une chaîne Unicode correcte
JavaScript / Node.js — axios + cheerio
cheerio, c'est une API façon jQuery côté serveur. Mieux vaut déterminer l'encodage explicitement via iconv-lite, car Node attend de l'UTF-8 par défaut.
const axios = require("axios");
const cheerio = require("cheerio");
const iconv = require("iconv-lite");
const chardet = require("chardet");
(async () => {
const url = "https://example.com";
// On récupère bien les octets
const { data } = await axios.get(url, {
responseType: "arraybuffer",
headers: { "User-Agent": "Mozilla/5.0" },
});
// On détecte l'encodage et on décode
const encoding = chardet.detect(data) || "utf-8";
const html = iconv.decode(Buffer.from(data), encoding);
const $ = cheerio.load(html);
// Le texte
console.log("Titre :", $("title").text().trim());
// Les liens
const links = new Set();
$("a[href]").each((_, el) => {
const href = ($(el).attr("href") || "").trim();
if (/^(mailto:|tel:|javascript:|#)/.test(href)) return;
links.add(new URL(href, url).href); // relatifs -> absolus
});
console.log("Liens trouvés :", links.size);
})();
PHP — DOMDocument
Le DOMDocument intégré sait parser le HTML d'origine. Pour les accents, l'important est de lui fournir le bon encodage — la méthode la plus fiable est montrée ci-dessous.
<?php
$url = "https://example.com";
$html = file_get_contents($url);
// Si la page est en windows-1252, on la convertit en UTF-8 avant le parsing
$encoding = mb_detect_encoding($html, ["UTF-8", "Windows-1252", "ISO-8859-15"], true);
if ($encoding && $encoding !== "UTF-8") {
$html = mb_convert_encoding($html, "UTF-8", $encoding);
}
$dom = new DOMDocument();
libxml_use_internal_errors(true); // on étouffe les erreurs du HTML « sale »
// Astuce pour que DOMDocument ne casse pas l'UTF-8 : encodage explicite
$dom->loadHTML('<?xml encoding="UTF-8">' . $html);
libxml_clear_errors();
$xpath = new DOMXPath($dom);
// Le texte
$titleNode = $xpath->query("//title")->item(0);
echo "Titre : " . trim($titleNode->textContent) . PHP_EOL;
// Les liens
$links = [];
foreach ($xpath->query("//a[@href]") as $a) {
$href = trim($a->getAttribute("href"));
if (preg_match('/^(mailto:|tel:|javascript:|#)/', $href)) continue;
$links[$href] = true; // déduplication par clé
}
echo "Liens trouvés : " . count($links) . PHP_EOL;
Go — net/http + goquery
goquery reprend l'API de jQuery. Pour les encodages, il existe le paquet golang.org/x/net/html/charset, qui lit lui-même le charset dans les en-têtes et les méta-tags.
package main
import (
"fmt"
"net/http"
"net/url"
"github.com/PuerkitoBio/goquery"
"golang.org/x/net/html/charset"
)
func main() {
target := "https://example.com"
resp, err := http.Get(target)
if err != nil {
panic(err)
}
defer resp.Body.Close()
// Convertit automatiquement le flux en UTF-8 selon le charset de l'en-tête/méta
utf8Reader, err := charset.NewReader(resp.Body, resp.Header.Get("Content-Type"))
if err != nil {
panic(err)
}
doc, err := goquery.NewDocumentFromReader(utf8Reader)
if err != nil {
panic(err)
}
// Le texte
fmt.Println("Titre :", doc.Find("title").First().Text())
// Les liens
base, _ := url.Parse(target)
links := map[string]bool{}
doc.Find("a[href]").Each(func(_ int, s *goquery.Selection) {
href, _ := s.Attr("href")
if u, err := base.Parse(href); err == nil {
links[u.String()] = true
}
})
fmt.Println("Liens trouvés :", len(links))
}
Java — Jsoup
Jsoup est l'une des bibliothèques les plus commodes : chargement, analyse, sélecteurs et détection d'encodage dans un seul paquet.
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import java.util.HashSet;
import java.util.Set;
public class Parser {
public static void main(String[] args) throws Exception {
String url = "https://example.com";
// Jsoup détecte lui-même l'encodage via les en-têtes et les méta-tags
Document doc = Jsoup.connect(url)
.userAgent("Mozilla/5.0")
.get();
// Le texte
System.out.println("Titre : " + doc.title());
// Les liens (absUrl les rend absolus tout seul)
Set<String> links = new HashSet<>();
for (Element a : doc.select("a[href]")) {
String href = a.absUrl("href");
if (href.isBlank()) continue;
links.add(href);
}
System.out.println("Liens trouvés : " + links.size());
}
}
7. Recommandations rapides
- Ne parsez pas le HTML avec des regex — utilisez un vrai parseur (lxml, cheerio, goquery, Jsoup, DOMDocument).
- Détectez l'encodage sur les octets, pas sur la chaîne, et ramenez tout à l'UTF-8 le plus tôt possible.
- Convertissez toujours les liens relatifs en absolus (
urljoin,new URL(base),absUrl). - Pour les sites dynamiques, préparez-vous à un navigateur headless ou au travail avec l'API interne.
- Respectez
robots.txt, espacez vos requêtes et ne mettez pas à genoux le serveur d'autrui.