Aller au contenu
OneKitly

Nettoyer un texte en désordre : l'ordre des opérations qui compte vraiment

Publié le 08/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

Le nettoyage de texte est un pipeline dont les étapes ne commutent pas : les mêmes opérations dans un autre ordre donnent un autre texte, et les dégâts sont silencieux. Exécuté dans Node, décoder les entités HTML avant de retirer les balises transforme le texte échappé &lt;b&gt; en un vrai élément <b> que le nettoyeur supprime ensuite : « écrire <b> pour afficher une balise » ressort en « écrire pour afficher une balise ». Retirer les balises d'abord, puis décoder exactement une fois, reproduit ce qu'affiche un navigateur. Écraser les blancs avec /\s+/ avant d'avoir décidé du traitement des retours à la ligne aplatit trois paragraphes en une seule chaîne de 80 caractères, et aucune étape ultérieure ne reconstruit les frontières : n'écrase que les blancs horizontaux, avec /[^\S\n]+/, après les jointures. Dédupliquer les lignes avant de couper leurs espaces de fin ne trouve rien, car « alpha » et « alpha » suivi d'une espace sont deux chaînes distinctes : cinq lignes restent cinq ; coupe d'abord et les mêmes cinq tombent à trois. Trois caractères survivent alors à tout passage naïf : U+00A0 (reconnu par \s, retiré par trim), U+200B (reconnu par aucun des deux) et U+FEFF, que \s reconnaît mais que la propriété Unicode White_Space refuse. Corrige l'ordre, puis les invisibles, et mesure seulement ensuite.

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

Les étapes de nettoyage ne commutent pas

Un nettoyeur de texte ressemble à un menu : supprimer les espaces en trop, supprimer les retours à la ligne, supprimer les lignes en double, retirer les balises HTML. Chaque entrée a l'air autonome, alors on les coche dans l'ordre où l'interface les affiche. Elles ne sont pas autonomes. Chaque étape réécrit l'entrée que verra la suivante, et plusieurs paires donnent un résultat différent selon celle qui passe en premier. Les échecs sont discrets : tu obtiens toujours du texte, il a toujours l'air plausible, et ce que tu as perdu est justement ce que tu ne regardais pas.

Trois paires causent l'essentiel des dégâts réels : les balises contre les entités, les blancs contre les retours à la ligne, et la déduplication contre la coupe des espaces. Chacune est démontrée ci-dessous sur une chaîne réelle, exécutée plutôt que raisonnée. Les neuf étapes ordonnées ne sont que ce qui découle de ces trois contraintes quand on les respecte toutes en même temps.

Les balises avant les entités, et un seul décodage

Prenons un fragment HTML : <p>Terms &amp;amp; conditions: write &lt;b&gt; to show a bold tag.</p>. Un navigateur l'affiche ainsi : Terms &amp; conditions: write <b> to show a bold tag. Les séquences échappées sont du texte — l'auteur voulait une esperluette littérale et une balise de gras littérale, visible.

Retire d'abord les balises et décode une fois : tu obtiens exactement cette sortie du navigateur. Décode d'abord puis retire : le &lt;b&gt; est déjà devenu un vrai élément <b> quand le nettoyeur passe, et le nettoyeur le supprime. Le résultat est « write to show a bold tag », avec une double espace là où se trouvait le sujet de la phrase. Retire d'abord mais décode deux fois et tu obtiens l'échec inverse : « write <b> to show a bold tag » contient désormais une vraie balise que plus rien n'échappe, et c'est ainsi qu'un extrait en texte brut redevient du balisage dès que quelqu'un le colle dans une page.

La règle derrière ces trois résultats est courte : une entité est du texte échappé, et le décodage promeut du texte en balisage. Tout ce qui traite le balisage de façon particulière doit donc passer avant la promotion. Les navigateurs évitent la question en tokenisant une seule fois, en un seul passage, ce qui explique qu'un vrai analyseur n'ait jamais à trancher. Un pipeline de regex, si, et la décision est : retirer, puis décoder, exactement un décodage.

Les blancs après les retours à la ligne, jamais à travers

Prends un document de trois paragraphes, dont certains coupés sur plusieurs lignes, avec une ou trois lignes vides parasites entre eux. Applique d'abord /\s+/ remplacé par une espace unique, parce que « supprimer les espaces en trop » était la première case, et tu obtiens une chaîne de 80 caractères sans aucune frontière de paragraphe. Rien en aval ne peut les rétablir : les retours à la ligne qui portaient la structure étaient des blancs, et tu as demandé d'écraser les blancs.

Passe le même document dans le bon ordre — normaliser les fins de ligne, couper les blancs de chaque ligne, joindre les lignes coupées, n'écraser que les séries horizontales avec /[^\S\n]+/, puis réduire trois retours ou plus à une ligne vide — et il ressort à 58 caractères sur deux paragraphes, structure intacte. La différence n'est pas qu'une regex serait meilleure que l'autre. C'est que /\s+/ inclut \n et \r, et qu'une classe comme [^\S\n] ne les inclut délibérément pas.

Il y a un second ordonnancement, plus subtil, à l'intérieur du premier. Prends « Paragraph one text. », une ligne contenant une seule espace, puis « Paragraph two text. ». Découpe sur /\n\n/ et tu trouves un paragraphe, pas deux, parce que la ligne de séparation n'est pas vide : elle contient une espace. Coupe les blancs de chaque ligne d'abord et le même découpage en trouve deux. Voilà pourquoi la coupe des lignes précède toute décision sur les paragraphes, alors que l'écrasement des séries la suit : les deux opérations sur les blancs se placent de part et d'autre de l'étape des retours à la ligne.

Dédupliquer en dernier, parce que l'égalité est une propriété d'aval

La déduplication de lignes compare des chaînes entières. Donne-lui les cinq lignes « alpha » suivie d'une espace, « beta », « alpha », « beta » suivie d'une tabulation et « gamma » : elle ne retire rien. Cinq lignes en entrée, cinq en sortie, parce que « alpha » et « alpha » plus une espace sont simplement deux chaînes différentes, comme « beta » et « beta » plus une tabulation. Coupe d'abord les blancs de fin et la même déduplication ramène les cinq à trois. Les doublons étaient toujours là ; ils étaient invisibles exactement comme une espace de fin est invisible.

La même chose se produit sur l'exemple travaillé à la fin de cet article. Placée en deuxième position, juste après le retrait des balises, la déduplication retire zéro ligne. La même déduplication placée en huitième en retire une, parce qu'entre-temps l'espace sans chasse a disparu et la double espace a été écrasée : les deux lignes qui étaient depuis toujours la même phrase sont enfin devenues la même chaîne. La déduplication ne trouve pas les doublons ; c'est la normalisation qui les crée, et la déduplication qui les ramasse ensuite.

Une décision reste la vôtre : la casse. La déduplication compare à l'identique, donc « Total » et « total » sont deux lignes. Le repli de casse est un choix distinct et destructeur, qui mérite sa propre étape et doit rester visible dans l'interface plutôt qu'être enfoui dans le déduplicateur — le même argument que pour les conventions de nommage, où la transformation n'est sûre que si l'on sait de quelle convention on part.

Les caractères qui survivent à tout nettoyage naïf

Trois points de code produisent l'essentiel des résidus. U+00A0, l'espace insécable, arrive des traitements de texte, des pages web et de la typographie française et espagnole, où elle se place avant un deux-points ou après un guillemet ouvrant. U+200B, l'espace sans chasse, arrive des éditeurs de CMS et du texte enrichi collé, comme indication invisible de coupure. U+FEFF, l'espace sans chasse insécable, est ce que donne le décodage d'une marque d'ordre des octets UTF-8 ; on la trouve tout au début des fichiers produits par des exports de tableur et par des outils Windows, et parfois au milieu après une concaténation naïve.

Ce qui compte pour un pipeline de nettoyage, c'est lequel de ces caractères tes outils voient, et la réponse n'est pas intuitive. Exécuté dans Node 22 : /\s/ reconnaît U+00A0 et U+FEFF mais pas U+200B. L'échappement de propriété Unicode /\p{White_Space}/u reconnaît U+00A0 mais pas U+FEFF. Les deux divergent, dans les deux sens : U+0085, le contrôle NEL, est reconnu par la propriété Unicode et pas par /\s/. La raison est que la grammaire ECMAScript définit sa propre production WhiteSpace, qui ajoute la marque d'ordre des octets pour des raisons historiques, tandis que la base de caractères Unicode attribue White_Space selon ses propres critères et ne la donne pas à U+FEFF.

La normalisation ne remplace rien. Appliquer NFKC ramène U+00A0 à un U+0020 ordinaire, ce qui est réellement utile, mais laisse U+200B exactement où il était : la chaîne fait toujours un caractère après coup. Le pipeline a donc besoin d'une étape de suppression explicite pour les caractères de format — U+200B, U+200C, U+200D, U+FEFF, U+00AD — et d'une étape de conversion distincte pour les espaces exotiques. Aucune ne peut être déléguée à une regex de blancs, car pour une regex de blancs la moitié d'entre eux ne sont pas des blancs.

Un exemple en désordre, neuf étapes, avant et après

L'exemple fait 126 caractères et contient, volontairement, tous les problèmes évoqués ci-dessus : deux espaces en tête, des fins de ligne CRLF, un h2 et trois éléments p, une esperluette doublement encodée, une espace sans chasse collée à la fin d'une phrase, trois retours à la ligne consécutifs, une phrase répétée à l'identique, une espace interne doublée, un paragraphe coupé sur deux lignes avec la continuation indentée, et une marque d'ordre des octets suivie de deux espaces tout à la fin.

Passé dans les neuf étapes en ordre, il tombe à 58 caractères : une ligne de titre « Q3 &amp; Q4 report », une ligne « Revenue rose 12%. », une ligne vide, puis « Costs fell » et « slightly. » comme deux lignes d'un même paragraphe. Les longueurs intermédiaires sont 122 après normalisation des fins de ligne, 92 après le retrait des balises, 88 après l'unique décodage d'entité, 86 une fois l'espace sans chasse et la marque d'ordre des octets supprimées, 80 après la coupe de chaque ligne, 77 après l'écrasement horizontal, 76 après la réduction du triple retour, et 58 après déduplication.

Deux de ces nombres résument tout l'article. La chute de 76 à 58 correspond à la ligne en double, et elle n'existe que parce que les étapes 4 et 8 sont passées avant ; déplace la déduplication en deuxième position et cette chute vaut zéro. L'étape 86 vers 80 est la coupe ligne par ligne, et c'est elle qui permet ensuite au découpage en paragraphes de voir une vraie ligne de séparation vide. Tout le reste est de la comptabilité.

Avant de livrer le texte nettoyé

Conserve l'original. Chaque étape de ce pipeline est destructrice par conception, et aucune n'est réversible : tu ne peux pas retrouver quelles espaces étaient insécables, quel retour à la ligne était une coupure et lequel un paragraphe, ni laquelle des deux lignes identiques tu voulais garder. Un texte nettoyé est un artefact dérivé, et un artefact dérivé ne doit jamais être l'unique copie.

Passe ensuite le pipeline deux fois sur sa propre sortie. Un nettoyage correctement ordonné est idempotent : le second passage ne doit rien changer. S'il change quelque chose, une étape n'est pas stable par répétition, et en pratique c'est presque toujours le décodage d'entités — la seule opération de la liste qui puisse se créer du travail supplémentaire. Un contrôle d'idempotence coûte une ligne de code et attrape la classe de bugs qui n'apparaît que lorsqu'un document repasse dans l'outil six mois plus tard, parce que quelqu'un l'a réimporté.

Enfin, ne compte qu'à la fin. Toute longueur, tout nombre de mots ou toute mesure de lisibilité prise en milieu de pipeline mesure une chaîne qui n'existe plus, et une longueur dépend en particulier de ce que tu acceptes d'appeler un caractère — une question à trancher séparément avant de faire confiance au moindre chiffre affiché par un compteur.

Six caractères invisibles, et ce que les outils de blancs de JavaScript en voient. Exécuté dans Node 22 : U+FEFF et U+0085 divergent en sens inverse, parce que la production WhiteSpace d'ECMAScript et la propriété Unicode White_Space ne sont pas la même liste.
CaractèrePoint de code/\s/ le reconnaît/\p{White_Space}/u le reconnaît.trim() le retire
Espace insécableU+00A0OuiOuiOui
Espace insécable étroiteU+202FOuiOuiOui
Espace sans chasseU+200BNonNonNon
Espace sans chasse insécable, la marque d'ordre des octetsU+FEFFOuiNonOui
Ligne suivanteU+0085NonOuiNon
Trait d'union conditionnelU+00ADNonNonNon
Supprimer les espaces en tropRéduis les espaces répétés et rogne chaque ligne pour nettoyer un texte.Essayer l'outil

Questions fréquentes

Si je lance le nettoyeur deux fois, l'ordre compte-t-il encore ?
Oui, et le lancer deux fois peut empirer les choses. La répétition ne recrée pas l'information détruite par une étape antérieure : des frontières de paragraphes écrasées le restent, quel que soit le nombre de passages. Pendant ce temps, le décodage d'entités n'est pas idempotent : un second passage décode &amp;amp; une seconde fois, transformant une esperluette littérale voulue dans le texte en une esperluette structurelle. Le bon test n'est pas de lancer deux fois pour un meilleur résultat, mais de lancer deux fois pour vérifier que le second passage ne change rien.
Pourquoi .trim() retire-t-il la marque d'ordre des octets mais pas l'espace sans chasse ?
Parce que trim est défini par rapport à la production WhiteSpace d'ECMAScript, et non à la propriété Unicode White_Space, et que cette production liste explicitement U+FEFF pour des raisons historiques remontant à l'époque où la marque d'ordre des octets se trouvait couramment en tête de flux. U+200B n'a jamais figuré sur cette liste : Unicode le classe comme caractère de format dans la catégorie générale Cf, au motif qu'il marque une possibilité de coupure de ligne et non une espace entre mots. Donc trim retire l'un et pas l'autre, et ni /\s/ ni trim ne t'aideront jamais avec U+200B. Supprime-le explicitement.
Y a-t-il une bonne raison d'utiliser /\s+/ sur un document entier ?
Oui, dans exactement une situation : quand tu as décidé que la sortie tient sur une seule ligne et que la structure est sans importance — une clé de recherche, une empreinte de comparaison, une valeur destinée à une cellule CSV d'une seule ligne. Là, tout aplatir en espaces simples est précisément le but. Partout où la sortie sera lue par un humain, /\s+/ est la mauvaise classe, car elle traite le saut de ligne qui sépare deux paragraphes et les deux espaces après un point comme la même chose. Utilise-la délibérément pour des clés, jamais par défaut pour de la prose.
Comment voir qu'un caractère invisible est présent ?
Compare la longueur attendue avec la longueur obtenue, puis affiche les points de code. Deux chaînes qui s'affichent à l'identique à l'écran peuvent avoir des longueurs différentes — c'est tout l'indice. Une fois la longueur surprenante, liste chaque caractère avec son point de code en hexadécimal et le coupable saute aux yeux : un U+00A0 là où tu supposais un U+0020, ou un U+200B collé à la fin d'une phrase. Le faire une fois sur un échantillon de chaque source importée te dira quels producteurs de ta chaîne émettent quels caractères, et tu pourras alors supprimer exactement ceux-là.
La déduplication doit-elle préserver l'ordre d'origine des lignes ?
Presque toujours oui, et elle doit garder la première occurrence plutôt que la dernière. Trier pour repérer les doublons est une habitude héritée des pipelines en ligne de commande, et cela réordonne en silence un document dont l'ordre portait du sens — une liste d'étapes, un journal de modifications, une transcription. Garder la première occurrence correspond aussi à la lecture humaine : la ligne antérieure est en général celle qui a du contexte autour d'elle. Si un outil propose de trier pendant qu'il déduplique, considère cela comme deux opérations distinctes et ne demande que celle que tu veux.

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.GuideRetirer le HTML proprement : ce qu'un suppresseur de balises peut et ne peut pas faireRetirer 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.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.ExplicationTrouver les doublons d'une liste sans tableurDeux lignes qui paraissent identiques ne le sont souvent pas. La casse, une espace finale, une espace insécable et deux encodages différents de la même lettre accentuée ont été passés dans le détecteur de doublons : il n'en a signalé aucun dans trois cas sur quatre.GuideNettoyer une liste collée depuis un tableur ou un PDFUn collage transporte des caractères invisibles : espaces insécables, traits d'union conditionnels, espaces de largeur nulle, tabulations et CRLF. Quatre outils de nettoyage ont été passés sur chacun, et ils utilisent trois définitions différentes de l'espace.ExplicationDétecter la langue d'un texte, et pourquoi les textes courts échouentMesuré, pas affirmé : 90 phrases courtes réelles dans six langues, aucune refusée et 68 justes — 76 %, tombant à 64 % en dessous de seize lettres. Quatre des réponses fausses sont revenues avec 100 % de confiance.

Outils similaires

Sources

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