Parsing HTML, liens et texte : problèmes, encodages et exemples multi-langages

Extraire le texte et les liens du HTML : encodages, balisage cassé, URL relatives et exemples de solutions dans les langages populaires.

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

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

  1. Sélectionner tous les tags <a> dotés d'un attribut href.
  2. Convertir les liens relatifs en absolus (/pagehttps://site.com/page).
  3. Filtrer : écarter mailto:, tel:, javascript:, les ancres #, les doublons.
  4. 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 (&nbsp;) ;
  • décoder les entités HTML (&amp;&, &#233;é, &laquo;«) ;
  • 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 :

  1. L'en-tête HTTP Content-Type: text/html; charset=windows-1252.
  2. Le méta-tag dans le HTML : <meta charset="utf-8"> ou <meta http-equiv="Content-Type" content="text/html; charset=windows-1252">.
  3. Le BOM (Byte Order Mark) en début de fichier — pour l'UTF.
  4. 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 ».

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

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

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

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

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