Aller au contenu
OneKitly

Retirer le HTML proprement : ce qu'un suppresseur de balises peut et ne peut pas faire

Publié le 14/07/2026 · 16 min de lecture · Outils texte & langage

Daniel Okonkwo

Daniel OkonkwoDéveloppeur front-end et rédacteur Tech chez OneKitly

Performance web · Formats de fichiers

Vérifié à partir de 5 sources

Voir le profil
En bref

Retirer des balises et assainir du HTML sont deux métiers différents. Un suppresseur de balises produit du texte brut pour l'affichage, le stockage ou le comptage ; un assainisseur produit du HTML sûr à insérer dans une page. Confondre les deux, c'est ainsi que s'écrivent les failles d'injection, et une regex ne fera jamais le second métier parce qu'elle ne modélise pas l'analyseur : donne au motif qui reconnaît un chevron ouvrant, une suite de caractères qui ne sont pas un chevron fermant, puis un chevron fermant le fragment <a title="fast > cheap" href="/x">our guide</a>, et il s'arrête au premier chevron fermant rencontré, produisant cheap" href="/x">our guide. Partout où la regex et le tokeniseur du navigateur divergent, c'est la regex qui a tort, et ces divergences ne peuvent pas être énumérées — d'où l'échec des listes noires comme classe, et les vraies défenses : l'échappement contextuel au point d'insertion, ou un assainisseur à liste blanche fondé sur un analyseur. Comme outil de mise en forme, en revanche, un suppresseur a du vrai travail. Sur un fragment de 390 caractères, le motif naïf renvoie 183 caractères contenant le corps de la feuille de style, une ligne de JavaScript abîmée, la queue d'un commentaire, des entités non décodées et deux éléments de liste fusionnés en StandardExpress. Supprime le contenu des éléments à texte brut, transforme les fins de bloc en lignes vides et br en un saut de ligne, décode les entités une fois : 118 caractères propres.

Retirer des balises et assainir du HTML sont deux métiers différents. Un fragment réel passé dans une regex naïve puis dans un nettoyeur conscient de la mise en forme, avec le contenu des script et style, les coupures de blocs, les commentaires, les CDATA et l'ordre des entités montrés en sortie.

Deux métiers qui n'en paraissent qu'un

Retirer les balises, c'est produire du texte brut : quelque chose à afficher dans un résultat de recherche, dont compter les mots, à mettre dans un courriel en texte simple, à donner à un résumeur ou à stocker dans une colonne qui ne sera jamais rendue comme du balisage. Assainir, c'est produire du HTML : du balisage qui sera inséré dans une page vivante et doit donc y être sûr. Les deux se ressemblent parce que tous deux prennent du HTML et renvoient quelque chose de plus court. Ce n'est pas le même métier, le critère de réussite n'est pas le même, et un seul des deux est un contrôle de sécurité.

Ce guide traite du premier métier, fait correctement. La question de sécurité est traitée dans la dernière section, brièvement et sans ambiguïté, parce que la réponse honnête est courte : un suppresseur de balises n'est pas une frontière de sécurité, et aucun ajout de motifs ne le transforme en frontière. Tout ce qui se trouve entre les deux est le travail de mise en forme qu'un nettoyeur doit réellement accomplir et que la plupart des implémentations ratent — supprimer le contenu des script et style, transformer la structure en sauts de ligne, traiter les commentaires et les CDATA, et décoder les entités au bon endroit.

Ce qu'une regex de balises naïve fait à un fragment réel

Le fragment de test fait 390 caractères de balisage de page ordinaire : un conteneur article, une feuille de style en ligne, un titre avec une esperluette encodée, un paragraphe coupé par un br et contenant une entité de tiret demi-cadratin, un commentaire HTML dont le texte contient un chevron fermant, une liste de deux éléments, un lien dont l'attribut title contient un chevron fermant encodé, et un script en ligne. Rien d'exotique, rien d'hostile — c'est à quoi ressemble un export de CMS.

Exécute le one-liner standard — reconnaître un chevron ouvrant, une suite quelconque de caractères qui ne sont pas un chevron fermant, puis un chevron fermant, en global — et 183 caractères reviennent. Parmi eux : le texte littéral .lead{font-weight:700}, c'est-à-dire le corps de la feuille de style devenu de la prose. La chaîne var ok = a d;, c'est-à-dire le script amputé de son milieu parce que la regex a lu les opérateurs de comparaison comme une balise. Le fragment 0 --> , c'est-à-dire la queue du commentaire, qui a survécu parce que la regex s'est arrêtée au chevron fermant qu'il contenait. Deux entités non décodées. Et les mots Standard et Express fusionnés en StandardExpress, avec une phrase qui se termine par Monday.Delivery parce que le br n'a rien apporté.

Le même fragment passé dans un nettoyeur qui sait ce que signifient les éléments renvoie 118 caractères, en cinq blocs : le titre, le paragraphe sur deux lignes avec la coupure du br respectée, les deux éléments de liste sur leurs propres lignes, et la phrase finale — avec l'esperluette et le tiret demi-cadratin décodés, et aucune trace de la feuille de style, du script ni du commentaire. L'écart entre 183 et 118 caractères n'est pas de la compression. C'est le retrait de choses qui n'ont jamais été du texte.

Le contenu des script et style n'est pas du balisage

Le principal défaut de mise en forme des nettoyeurs naïfs est qu'ils retirent les balises ouvrante et fermante des script et style et laissent tout ce qu'il y a entre. Ce n'est pas un petit souci cosmétique : les règles CSS d'une page ou le code d'un script en ligne atterrissent au milieu de ce qui devait être de la prose lisible, et ils seront indexés, comptés, résumés et montrés à quelqu'un. Dans le fragment de test, le bloc de déclarations CSS apparaît en deuxième ligne de sortie, juste au-dessus du titre.

La norme HTML les appelle des éléments à texte brut : à l'intérieur de script et style, l'analyseur cesse de chercher des balises et lit jusqu'à la balise de fin correspondante. C'est pourquoi la regex naïve abîme aussi ce qu'elle laisse fuir. Dans le script de test, l'expression contenant un chevron ouvrant et un chevron fermant est lue par la regex comme une balise et supprimée : le JavaScript qui fuit n'est même pas le JavaScript d'origine, c'est une version raccourcie qui ressemble par hasard à une phrase. Deux modes d'échec dans une seule construction.

Le correctif est une règle placée avant toutes les autres : reconnaître la balise ouvrante, son contenu et sa balise fermante comme une seule unité pour script, style, template et noscript, et supprimer l'ensemble. Place-la en tête du pipeline, car une fois les balises parties tu ne peux plus savoir quel texte se trouvait à l'intérieur.

Les blocs ont besoin de coupures, et br et p ne coupent pas pareil

Supprimer une balise supprime le blanc qu'elle impliquait. Deux paragraphes qui se suivent sans blanc entre eux dans la source deviennent OneTwo. Deux éléments de liste deviennent AB. Un titre suivi d'un paragraphe devient TB. C'est le défaut qui rend le texte nettoyé illisible et les comptages de mots faux, et il est invisible en test dès que le HTML source est mis en forme avec des sauts de ligne entre les balises — ce qui explique précisément qu'il parte en production.

La correspondance qui produit une sortie lisible est courte. La balise fermante d'un élément de bloc — p, div, li, tr, h1 à h6, blockquote, section, article et les autres — devient une ligne vide. Un br devient exactement un saut de ligne, parce qu'un br est une coupure à l'intérieur d'un bloc, pas un nouveau bloc. Un hr devient une ligne vide. Toute autre balise devient rien. Ensuite, réduis les séries de trois sauts de ligne ou plus à une seule ligne vide, car un bloc imbriqué émet deux balises fermantes et donc deux lignes vides.

Une réserve honnête : cette correspondance place une ligne vide entre les éléments de liste, parce que li est un bloc. Cela se lit mal pour une liste serrée, et la version plus soignée traite li comme un simple saut de ligne et ne réserve la ligne vide qu'au ul ou ol englobant. Vouloir cela relève de la préférence de mise en forme et non de la justesse, mais c'est une préférence qu'il faut exprimer explicitement — un nettoyeur qui n'a jamais réfléchi aux listes les collera bout à bout, et ça, ce n'est pas une préférence, c'est un bug.

Commentaires, CDATA et entités : trois grammaires particulières

Un commentaire HTML n'est pas une balise et ne suit pas la grammaire des balises : il court d'une ouverture de quatre caractères à une fermeture de trois, et absolument n'importe quoi peut se trouver entre les deux, y compris des chevrons fermants. Une regex de balises qui semble retirer les commentaires s'en tire uniquement parce que la plupart des commentaires ne contiennent pas de chevron fermant. Mets-en un — une note du type garder si le stock est supérieur à zéro — et la regex s'arrête là, laissant le reste du texte du commentaire dans ta sortie. Dans le fragment de test, le résidu visible est la chaîne 0 suivie du terminateur de commentaire.

Les sections CDATA sont un cas voisin qui survit dans la nature à l'intérieur du SVG et des exports de type XHTML. Leur contenu n'est par définition pas du balisage, et leur terminateur n'est pas non plus un chevron fermant. Passe une section CDATA contenant du texte qui ressemble à du balisage dans la regex naïve et tu obtiens le texte intérieur privé de ses balises intérieures, avec la séquence de crochets fermants qui traîne — un résultat faux de trois manières à la fois. Reconnais et supprime les sections CDATA comme une unité, avant la passe générale sur les balises, exactement comme pour les commentaires.

Les entités viennent en dernier, et une seule fois. Décode après la disparition des balises, jamais avant : un chevron ouvrant échappé dans la source est du texte que l'auteur voulait visible, et le décoder d'abord le promeut en balise que le nettoyeur supprime ensuite. Sur un fragment disant d'utiliser un élément em pour l'emphase, avec le nom d'élément échappé, retirer puis décoder préserve le nom d'élément visible, tandis que décoder puis retirer le supprime et laisse un trou. Décode deux fois et tu fabriques du balisage vivant à partir de quelque chose d'échappé volontairement, ce qui est un bug de mise en forme en route pour devenir pire.

Où se trouve réellement la frontière de sécurité

Un suppresseur de balises est un outil de mise en forme. Ce n'est pas une frontière de sécurité, et on ne l'y transforme pas en ajoutant des motifs. La raison est structurelle et déjà visible dans le tableau ci-dessus : le motif naïf et le tokeniseur du navigateur divergent sur la fin d'une balise dès qu'une valeur d'attribut contient un chevron fermant, et la version consciente de la mise en forme diverge au même endroit. Tout filtre bâti sur des motifs est une liste noire des formes auxquelles son auteur a pensé, et une liste noire ne vaut que l'imagination de son auteur, alors que l'analyseur est une spécification fixe, publiée, indépendante de l'adversaire. On ne peut pas énumérer les façons dont deux grammaires diffèrent.

Les bonnes défenses sont au nombre de deux, et aucune n'est un retrait de balises. La première et la plus importante est l'échappement contextuel en sortie : échappe la valeur au point où tu l'insères, avec la règle d'échappement de ce contexte, car les règles diffèrent pour le texte HTML, les valeurs d'attributs, les URL, les scripts et les styles. Un moteur de gabarits qui échappe par défaut le fait pour toi, et les bugs surviennent là où quelqu'un s'en est exempté. La seconde, nécessaire seulement lorsque les utilisateurs sont réellement autorisés à soumettre du balisage qui doit s'afficher comme du balisage, est un assainisseur fondé sur un analyseur : il analyse l'entrée en arbre et la reconstruit à partir d'une liste blanche d'éléments et d'attributs — le modèle de la spécification HTML Sanitizer et des bibliothèques d'assainissement établies. Liste blanche, analyseur, pas regex.

Il y a une simplification nette qui mérite d'être énoncée. Si le texte nettoyé est du texte brut et le reste — affiché dans un nœud de texte, écrit dans une colonne texte, compté, envoyé en courriel texte — alors la sortie du nettoyeur n'a aucun rôle de sécurité, parce que ce texte n'est plus jamais analysé comme du balisage. Le danger n'apparaît que lorsque quelqu'un reprend le texte nettoyé et le réinsère dans du HTML. À cet instant, le contrôle pertinent est l'échappement à ce point d'insertion, et rien de ce que le nettoyeur a fait avant. Garder ces deux moments distincts dans sa tête, c'est l'essentiel de ce que cette section cherche à enseigner.

Huit constructions HTML passées dans une regex de balises naïve puis dans un nettoyeur conscient de la mise en forme. Chaque colonne de sortie est la chaîne littérale renvoyée par le code dans Node 22.
EntréeLa regex naïve renvoieLe nettoyeur conscient de la mise en forme renvoie
<p>One</p><p>Two</p>OneTwoOne, ligne vide, Two
<p>One<br>Two</p>OneTwoOne, simple saut de ligne, Two
<li>A</li><li>B</li>ABA et B sur des lignes distinctes
<h2>T</h2><p>B</p>TBT, ligne vide, B
Un élément style contenant .lead{font-weight:700}le texte CSS .lead{font-weight:700} en sortie visiblerien — l'élément et son contenu sont supprimés ensemble
Un élément script contenant var ok = a < b && c > d;var ok = a d; — le milieu avalé comme s'il s'agissait d'une baliserien
<p>A<!-- keep if stock > 0 -->B</p>A 0 -->BAB
<a title="fast > cheap" href="/x">our guide</a>cheap" href="/x">our guidefaux aussi — aucune regex ne corrige cela, seul un analyseur le fait
Supprimer les balises HTMLSupprime toutes les balises HTML d'un extrait et garde uniquement le texte.Essayer l'outil

Questions fréquentes

Puis-je simplement utiliser le navigateur et lire textContent ?
Analyser avec l'analyseur de la plateforme est le bon réflexe, et cela règle gratuitement les problèmes d'attributs et de commentaires. Mais note ce que donnent les deux propriétés. textContent renvoie le texte concaténé de tous les descendants sans aucune mise en forme, y compris le contenu des script et style, et sans saut de ligne pour les blocs — il reproduit donc deux des défauts de cet article. innerText approxime le texte rendu, insère bien des sauts de ligne et ignore les éléments masqués, mais il dépend de la mise en page et donc du CSS. Dans un navigateur, analyse l'entrée en document inerte, retire explicitement les nœuds script, style et template, puis parcours l'arbre en émettant tes propres coupures. Sur un serveur sans DOM, une vraie bibliothèque d'analyse HTML te donne le même arbre.
Faut-il tronquer avant ou après avoir retiré les balises ?
Après, toujours. Tronquer du HTML coupe une balise en deux ou laisse un élément non fermé, et le fragment obtenu n'est ni du balisage valide ni du texte sensé — un extrait qui s'arrête au milieu d'une valeur d'attribut est une source classique de mises en page cassées lorsqu'il est inséré ailleurs plus tard. Retire d'abord, puis tronque le texte brut à une frontière de mot, puis ajoute les points de suspension. Le même argument vaut pour le comptage : un nombre de caractères ou de mots pris sur le balisage compte les noms de balises et les valeurs d'attributs comme du contenu, il est donc faux dans la proportion de balisage de la source, ce qui, pour un export de CMS typique, représente une grande part.
Si le texte nettoyé retourne dans une page, dois-je décoder les entités ?
Décode-le une fois pour le stockage, puis laisse la couche de sortie le réencoder au moment de l'insérer. Cela semble du travail en plus et c'est en fait le seul agencement qui reste correct : ta valeur stockée est le texte réel, et chaque consommateur l'échappe pour son propre contexte — texte HTML, attribut, URL, cellule CSV, courriel en texte simple. Si à l'inverse tu stockes la forme encodée et saute l'échappement en sortie parce qu'elle « est déjà échappée », le premier consommateur qui n'est pas du HTML reçoit des séquences d'esperluettes littérales, et le premier qui échappe quand même double l'encodage. Une valeur décodée canonique en stockage, un échappement à chaque frontière.
Peut-on rendre un nettoyeur à regex sûr en ajoutant des motifs ?
Non, et la raison mérite d'être intégrée car elle se généralise. Une liste de motifs encode les formes que son auteur a anticipées ; l'analyseur accepte toutes les formes définies par la spécification, y compris le comportement de récupération sur entrée malformée, que personne n'écrit sous forme de règle. La sécurité ne suit que si le modèle du langage qu'a le filtre est au moins aussi complet que celui du consommateur, et une liste de motifs est par construction moins complète. C'est tout l'argument des listes blanches contre les listes noires et des analyseurs contre les motifs, et c'est pourquoi cet article te donne un nettoyeur pour la mise en forme et t'oriente vers un assainisseur pour la sécurité, plutôt que de vendre un seul outil pour les deux.
Que doit faire le nettoyeur des images, des liens et des tableaux ?
Décide délibérément et documente la décision, car les trois portent une information qui vit hors du texte. Une image apporte son texte alternatif, qui est la description accessible et souvent la seule phrase à conserver ; supprimer l'élément en silence la perd. Un lien apporte son texte d'ancrage par défaut, et savoir si la destination doit être ajoutée entre crochets dépend de si ta sortie sera un jour lue sans moyen de suivre les liens. Un tableau exige un séparateur de cellules et un séparateur de lignes, sinon chaque ligne devient une suite illisible de valeurs concaténées — une tabulation entre cellules et un saut de ligne entre rangées, c'est le minimum habituel. Rien de tout cela n'est décidé pour toi par les noms de balises ; c'est un choix éditorial sur ce que le texte brut doit préserver.

Articles qui pourraient t'intéresser

Tous les guides
GuideRetirer le Markdown : ce que le texte brut perd, et ce qu'une regex se trompe à faireUn lien devient un texte dont la destination a disparu, une liste imbriquée perd sa hiérarchie, un tableau devient une file de mots. Puis la moitié technique : le markdown n'a pas de spécification unique, et un nettoyeur à base de regex abîme un nom de fichier, un signe de multiplication et l'intérieur d'un bloc de code — le tout confronté à un vrai analyseur.TutorielNettoyer un texte en désordre : l'ordre des opérations qui compte vraimentRetirer les balises avant de décoder les entités, couper les espaces avant de dédupliquer, écraser les blancs en dernier. Trois ordres exécutés dans Node, un pipeline de neuf étapes dans le bon sens, et les caractères invisibles — U+00A0, U+200B, U+FEFF — qui survivent à tout nettoyage naïf.GuideConstruire une URL avec paramètres qui survit à un copier-collerTrois encodages, une différence visible : %20 ou +. Le mode formulaire du générateur reproduit URLSearchParams octet pour octet sur dix-sept valeurs — mais donne-lui une URL de base avec un fragment et tous les paramètres atterrissent dans le hash, invisibles du serveur.ExplicationCasse de phrase et casse de titre : les règles changent selon la langueLa casse de titre anglaise a trois seuils différents selon le guide de style. Le français, l'espagnol, le portugais et l'italien n'en ont aucun. L'allemand capitalise chaque nom. L'outil n'en sait rien — voici exactement ce qu'il fait.GuideLes limites de caractères qui font vraiment mal : unités de code, points de code et graphèmesUn caractère, c'est trois choses à la fois. Un emoji avec teinte de peau vaut 1 graphème, 2 points de code et 4 unités UTF-16. Tous les comptages de ce guide ont été mesurés dans Node, avec en prime pourquoi un SMS tombe de 160 à 70 et pourquoi VARCHAR(255) ne fait pas 255 de quoi que ce soit de précis.ExplicationCompter les mots est ambigu, et chaque outil répond autrementUn compte de mots est une définition, pas une mesure. Nous avons compté le même paragraphe de quatre façons et obtenu 25, 28, 33 et 38 — puis compté 50 000 caractères de prose ordinaire et obtenu un accord à 4,5 % près. L'écart tient entièrement aux composés, aux chiffres et aux URL.

Outils similaires

Sources

Tu as repéré une erreur dans cet article ?

Retirer le HTML proprement : ce qu'un suppresseur de balises peut et ne peut pas faire — OneKitly