Extraire des titres, des liens ou des blocs de contenu depuis des pages web est un travail courant, et il produit invariablement le même symptôme quand il est mal fait : des accents transformés en suites de caractères illisibles. Le parsing HTML en Python n'est pas difficile en soi, les bibliothèques disponibles sont excellentes ; ce qui est difficile, c'est la chaîne de décisions qui va de la réception d'une suite d'octets à l'écriture d'un fichier de résultats. À chaque étape, un jeu de caractères est supposé, et une seule supposition fausse suffit à abîmer tout le corpus. Le pire est que le problème ne se voit pas toujours immédiatement : un jeu de données peut sembler correct sur les cent premières lignes et être irrémédiablement dégradé sur les pages issues d'un vieux site.

Comprendre où l'encodage se décide

Le point de départ est une confusion de vocabulaire qu'il faut lever. Une page web n'est pas du texte, c'est une suite d'octets accompagnée d'indications sur la manière de les interpréter. Ces indications peuvent être contradictoires, absentes ou fausses, et c'est au programme de trancher. Ce raisonnement rejoint les précautions que nous décrivons ailleurs dans la rubrique création web à propos des imports de contenus.

Trois sources d'information, par ordre d'autorité

Le jeu de caractères peut être annoncé à trois endroits : dans l'en tête de la réponse du serveur, dans une balise du document, et par une marque d'ordre des octets en tête de fichier. En théorie, l'en tête du serveur prime. En pratique, il est très souvent générique et faux, notamment sur les serveurs qui déclarent un jeu de caractères par défaut pour toutes leurs pages, quel que soit le contenu réel. L'indication interne au document est fréquemment plus fiable, parce qu'elle a été posée par celui qui a produit la page, mais elle peut aussi n'être qu'un vestige laissé après une migration.

Le piège de la déclaration par défaut

Un serveur qui annonce un jeu de caractères occidental hérité pour une page écrite en UTF-8 produit des caractères abîmés à coup sûr si le programme lui fait confiance. Le cas inverse existe aussi. La bibliothèque de requêtes la plus répandue en Python suit la spécification à la lettre et retombe sur ce jeu hérité quand l'en tête ne précise rien, ce qui est le comportement correct au sens de la norme et le mauvais comportement en pratique. Utiliser le texte décodé automatiquement par cette bibliothèque, sans vérification, est la première cause de corpus abîmés.

Travailler sur les octets, pas sur le texte

La règle qui règle la majorité des problèmes tient en une phrase : récupérer le contenu brut, sous forme d'octets, et laisser l'analyseur HTML décider du décodage. Les bibliothèques d'analyse modernes savent lire les déclarations internes au document, appliquer les règles de repli prévues par la spécification et, en dernier recours, tenter une détection statistique. Elles font ce travail bien mieux qu'un code écrit à la main, à la seule condition qu'on leur transmette les octets d'origine et non une chaîne déjà décodée, éventuellement mal décodée.

La détection statistique et ses limites

Quand aucune déclaration n'est disponible, il reste la détection par analyse des séquences d'octets, qui donne de bons résultats sur un texte assez long et des résultats hasardeux sur un fragment court. Sur un corpus important, il est utile de journaliser le jeu de caractères retenu pour chaque page ainsi que la méthode qui l'a déterminé, afin de pouvoir revenir sur les cas douteux. Une page dont l'encodage a été deviné mérite un contrôle visuel, alors qu'une page dont le document déclare son jeu de caractères n'en demande aucun.

Le double encodage, qui ne se répare pas toujours

Le cas le plus pénible n'est pas la page mal déclarée mais le texte qui a déjà subi deux décodages successifs erronés avant d'arriver. Les caractères accentués apparaissent alors sous forme de paires ou de triplets caractéristiques, et un œil habitué les reconnaît immédiatement. Tant qu'une seule opération fautive a eu lieu, l'opération inverse restaure le texte d'origine sans perte, ce qui se scripte facilement. Après deux opérations fautives enchaînées, ou après un passage par un jeu de caractères qui ne contient pas tous les caractères d'origine, une partie de l'information est définitivement perdue et remplacée par des caractères de substitution. C'est la raison pour laquelle il vaut mieux détecter le problème pendant la collecte que le traiter après coup sur le corpus final.

Même texte affiché avec un encodage correct et avec un encodage erroné

Choisir son analyseur

Python offre plusieurs analyseurs HTML, dont les différences ne portent pas sur la syntaxe d'utilisation mais sur la tolérance aux documents mal formés et sur la vitesse. Le choix a des conséquences directes sur ce qui sera extrait, et il doit être fait en connaissance de cause plutôt que par habitude. C'est le même type d'arbitrage que celui décrit dans notre article sur la manière d'aspirer un site web efficacement.

L'analyseur intégré à la bibliothèque standard

Il a l'avantage de ne rien exiger d'autre que Python lui même, ce qui compte sur un environnement contraint. Il est plus lent et surtout moins tolérant : sur un document dont les balises ne sont pas correctement fermées, cas très courant sur les pages anciennes, il peut produire une structure qui ne correspond pas à ce qu'un navigateur afficherait. Pour une extraction ponctuelle sur des pages bien formées, il suffit ; pour un corpus hétérogène, il expose à des manques silencieux.

Les analyseurs tolérants et conformes

Les analyseurs qui suivent l'algorithme d'analyse défini par la spécification HTML reconstituent l'arbre exactement comme le ferait un navigateur, y compris sur des documents très abîmés. C'est le comportement souhaitable dès qu'on traite des pages produites par des systèmes variés, parce que le résultat de l'extraction correspond alors à ce que voit un utilisateur. Leur contrepartie est une vitesse moindre, ce qui devient perceptible au delà de quelques milliers de documents.

Les analyseurs rapides fondés sur une bibliothèque compilée

Pour les gros volumes, un analyseur reposant sur une bibliothèque écrite en langage compilé change l'ordre de grandeur du temps de traitement, souvent d'un facteur dix. Il accepte des sélecteurs riches et convient parfaitement aux traitements de masse. Son défaut est une tolérance intermédiaire : il redresse beaucoup de choses mais pas exactement comme un navigateur, et une page très abîmée peut donner un arbre légèrement différent. Sur un corpus homogène, cette différence est sans conséquence, sur un corpus ancien elle peut faire perdre des éléments.

Comparer sur son propre corpus

La bonne méthode consiste à ne pas choisir dans l'abstrait mais à prendre trente pages représentatives, à extraire la même information avec deux analyseurs, et à comparer les résultats. Les écarts se comptent en général sur une ou deux pages, et leur examen apprend plus sur le corpus que n'importe quelle documentation. Ce test prend vingt minutes et évite de découvrir, après avoir traité dix mille pages, que quatre cents d'entre elles ont été mal analysées.

Situation Ce qu'il faut faire Ce qu'il ne faut pas faire
Récupération d'une page distante Passer les octets bruts à l'analyseur Utiliser le texte décodé automatiquement
En tête serveur et document en désaccord Privilégier la déclaration du document Faire confiance à l'en tête sans vérifier
Aucune déclaration disponible Détecter, journaliser, contrôler Supposer UTF-8 par principe
Corpus de pages anciennes Analyseur conforme au navigateur Analyseur strict de la bibliothèque standard
Traitement de masse Analyseur compilé, après comparaison Optimiser avant d'avoir vérifié la justesse
Écriture des résultats UTF-8 déclaré explicitement Laisser l'encodage par défaut du système

Le cas des pages construites par script

Un analyseur HTML lit ce que le serveur a envoyé, rien de plus. Sur une page dont le contenu principal est injecté par du code exécuté dans le navigateur, l'extraction ne rapportera qu'une coquille, sans qu'aucune erreur ne soit levée : le document est valide, il est simplement vide de ce qu'on cherchait. Le symptôme est reconnaissable, un taux d'extraction proche de zéro sur un domaine entier alors que la page s'affiche parfaitement dans un navigateur. La réponse consiste soit à trouver l'adresse de l'interface de programmation que la page interroge, ce qui est souvent le chemin le plus simple et le plus rapide, soit à passer par un navigateur automatisé, nettement plus lent et plus fragile. Regarder les requêtes réseau de la page avant d'écrire le moindre code fait gagner beaucoup de temps sur ce point précis.

Extraire sans perdre de contenu

Une fois l'arbre construit, l'extraction paraît triviale, et c'est là que se logent les erreurs les plus discrètes, celles qui ne provoquent aucune exception et produisent simplement des données incomplètes. Trois précautions suffisent à en éviter l'essentiel, et elles reposent toutes sur la même idée : ne jamais supposer qu'une page est conforme à ce qu'on attend. C'est la même prudence que celle appliquée aux contrôles visuels décrits dans notre article sur la manière d'automatiser des captures d'écran pour valider une refonte.

Ne pas confondre le texte et le balisage

Récupérer le texte d'un élément et récupérer son contenu HTML sont deux opérations différentes, et le choix dépend de l'usage. Pour indexer ou compter des mots, le texte suffit. Pour reprendre un contenu et le republier, il faut le balisage, ce qui impose ensuite un nettoyage sélectif des attributs et des balises indésirables. Extraire le texte puis tenter de reconstituer la mise en forme est une impasse : l'information est déjà perdue au moment de l'extraction.

Gérer les entités et les espaces insécables

Les analyseurs convertissent les entités HTML en caractères, ce qui est le comportement attendu, mais produit des caractères invisibles qui posent problème plus tard : l'espace insécable notamment, qui n'est pas un espace ordinaire et fait échouer toutes les comparaisons de chaînes naïves. La normalisation doit donc être explicite, en remplaçant ces caractères par leur équivalent ordinaire quand le traitement l'exige, et en la conservant quand la restitution typographique compte. Décider une fois, et l'appliquer partout, évite des heures de recherche sur des égalités qui échouent sans raison apparente.

Prévoir l'absence de l'élément cherché

Un sélecteur qui ne trouve rien renvoie une valeur vide ou nulle selon la bibliothèque, et enchaîner directement une opération sur ce résultat produit une erreur au premier document atypique. Sur un corpus de plusieurs milliers de pages, l'atypique est certain. Le code doit donc vérifier systématiquement la présence avant l'accès, et surtout compter les absences : un extracteur qui échoue silencieusement sur douze pour cent du corpus est bien plus dangereux qu'un extracteur qui s'arrête, parce que rien ne le signale. Ce comptage est aussi ce qui permet de décider quand s'arrêter : un extracteur qui couvre quatre vingt dix huit pour cent du corpus est en général suffisant, et poursuivre les deux pour cent restants coûte souvent plus cher que de les traiter à la main.

Journaliser ce qui n'a pas été extrait

La contrepartie de la robustesse est la traçabilité. Chaque page dont l'extraction n'a pas abouti doit laisser une ligne dans un journal, avec son adresse et la raison. Ce fichier se relit en fin de traitement et fait apparaître les familles de pages non prises en compte, qui appellent souvent une simple variante de sélecteur. Sans lui, le corpus final semble complet et ne l'est pas, ce qui fausse toutes les conclusions qu'on en tirera.

Où se produit la corruption d'encodage dans une chaîne de traitement Python
Décodage automatique à la récupération
41 %
Écriture de fichier sans encodage déclaré
27 %
Déclaration serveur erronée suivie sans contrôle
16 %
Concaténation de sources hétérogènes
10 %
Analyseur inadapté au document
6 %

Répartition des causes constatées sur des traitements d'extraction de contenu. Les deux premiers postes se corrigent chacun par une seule ligne de code.

Écrire les résultats sans les abîmer

La dernière étape est celle qui produit le plus de dégâts visibles, parce qu'elle est souvent traitée à la légère. Le texte correctement extrait peut être détruit à l'écriture, et le fichier obtenu ne porte alors plus aucune trace de l'erreur d'origine.

Déclarer l'encodage à chaque ouverture de fichier

Python utilise, en l'absence de précision, l'encodage par défaut de l'environnement, qui varie selon le système et la configuration. Un script qui fonctionne parfaitement sur une machine produit donc des fichiers illisibles sur une autre. La discipline consiste à préciser explicitement le jeu de caractères à chaque ouverture de fichier en écriture comme en lecture, sans exception et même pour un fichier temporaire. C'est une habitude à prendre une fois, qui supprime définitivement toute une catégorie de problèmes.

Le cas particulier du format CSV et des tableurs

Un fichier CSV encodé en UTF-8 s'ouvre correctement dans la plupart des outils modernes et incorrectement dans certains tableurs, qui supposent l'encodage hérité du système. La solution consiste à écrire une marque d'ordre des octets en tête de fichier, que ces tableurs reconnaissent comme un signal d'UTF-8, sans que cela gêne les autres outils. C'est un détail qui évite le retour classique du client expliquant que le fichier livré contient des caractères bizarres, alors que le fichier est parfaitement correct.

Le format JSON et la lisibilité

Le sérialiseur JSON de Python échappe par défaut tous les caractères non latins, ce qui produit un fichier valide mais illisible pour un humain. Désactiver cet échappement et écrire en UTF-8 donne un fichier tout aussi valide et directement lisible, ce qui change beaucoup lors des relectures et des comparaisons. Le même réglage améliore aussi nettement les différentiels entre deux versions d'un fichier de résultats, puisqu'une modification portant sur un mot accentué se lit alors comme un changement d'un mot et non comme une ligne entière remaniée. La contrepartie est nulle, la norme JSON prévoyant explicitement l'UTF-8 comme encodage par défaut.

Contrôler le résultat par un test simple

Le contrôle final ne demande aucun outil : il suffit de chercher dans le fichier produit les suites de caractères typiques d'un double encodage, celles qui apparaissent quand un texte UTF-8 a été interprété comme du texte hérité. Leur présence signale l'erreur immédiatement et permet de la corriger avant que le corpus ne soit exploité. Un second contrôle, tout aussi rapide, consiste à compter les caractères de substitution présents dans le résultat : leur nombre doit être nul, et toute occurrence signale une perte définitive d'information survenue en amont. Ajouter ce test à la fin du script coûte trois lignes et transforme une classe entière de bogues silencieux en échec immédiat et explicite.

Respecter le site interrogé

Un dernier point n'est pas technique mais conditionne la faisabilité du travail dans la durée. Un script d'extraction se comporte, du point de vue du serveur interrogé, comme un visiteur très rapide et très régulier, et il est trivial à repérer. Annoncer une identité explicite plutôt que de se faire passer pour un navigateur, respecter les indications du fichier robots.txt, espacer les requêtes d'au moins une seconde et mettre en cache localement toute page déjà récupérée sont des pratiques élémentaires. La mise en cache locale sert d'abord l'auteur du script : il relancera son extraction dix fois pendant la mise au point de ses sélecteurs, et retélécharger le corpus à chaque essai fait perdre des heures en plus de solliciter inutilement un serveur qui n'a rien demandé. Un cache sur disque, indexé par adresse, règle ce point en quelques lignes et se réutilise ensuite sur tous les projets d'extraction ultérieurs.