Réparer un texte au mauvais encodage
Remettre d’aplomb des valeurs sorties d’un import où « Crédit Agricole » a remplacé « Crédit Agricole ».
Pas besoin d’IA
GratuitInstantanéVos données ne sortent pas
Le verdict
N0, parce que la réparation est une identité sur les octets : on la rejoue dans l’autre sens et on retombe sur l’entrée, ce qu’aucun niveau au-dessus ne permet. Les deux extraits ne s’accordent pas partout, et le décompte est publié dans leur docstring : sur les quatre chaînes courtes où ils se séparent le plus visiblement — `ÃŽle-de-France`, `ÃŽlot`, `ÃŽles Canaries`, `Å’uvre` —, c’est l’extrait sans bibliothèque qui répare et `ftfy` qui renonce. Deux choix de la fiche méritent d’être copiés. Le premier est d’appeler la fonction étroite plutôt que la large — `fix_text` redresse aussi les guillemets, et transforme « L’été à Nice », déjà correct, en une version à apostrophe droite. Le second est de rendre un drapeau au lieu de réécrire en place : le point de rupture montre pourquoi, et une réparation que personne n’a notée ne se distingue pas d’une donnée qui a toujours été ainsi.
Aller-retour Windows-1252 vers UTF-8, avec un drapeau sur ce qui a changé
Pas besoin d’IA
Règle et algorithme classiqueN0GratuitInstantané
Python
"""
Réparer un texte décodé avec le mauvais jeu de caractères.
Niveau N0. « Crédit Agricole » n'est pas du texte abîmé : c'est
« Crédit Agricole » dont les octets UTF-8 ont été lus comme du
Windows-1252. L'opération est exactement inversible — réécrire les
caractères en octets Windows-1252, relire ces octets en UTF-8 — et c'est une
identité sur les octets, pas un jugement.
`ftfy` le fait en Python, et depuis dix ans. Il connaît aussi les variantes :
deux tours du même accident, un Windows-1252 qui aurait dû être du
cyrillique, un UTF-8 lu en UTF-16. Il n'en existe pas de portage en
JavaScript, si bien que l'extrait de ce langage écrit lui-même la
transformation principale, en une trentaine de lignes.
Les deux s'accordent sur trente-neuf des quarante-sept chaînes de cette
fiche, et ils se séparent dans les deux sens.
`ftfy` pèse la chaîne entière avant de réparer, et renonce quand ce qui en
sortirait est une suite courte ouvrant sur une majuscule accentuée :
`fix_encoding` laisse « ÃŽle-de-France » exactement tel quel, et répare le
même mot à l'intérieur de « Région ÃŽle-de-France ». « ÃŽles Canaries »,
« ÃŽlot » et « Å’uvre » échouent de la même façon — c'est la forme d'une
cellule de tableur et d'une colonne `region`, c'est-à-dire ce dont cette
fiche parle. L'extrait JavaScript n'a pas cette heuristique et répare les
quatre.
`ftfy` lit aussi un caractère de remplacement déjà présent comme un octet :
« Ã » + U+FFFD + « ambe » ressort en U+FFFD + « ambe », et le « Ã » a
disparu. L'extrait JavaScript laisse cette chaîne tranquille.
Dans l'autre sens, `ftfy` porte une règle que cet extrait n'a pas — un « Ã »
suivi d'une espace ordinaire est lu comme le « à » dont l'espace insécable a
été perdue en route, ce qui répare un cas réel de plus et réécrit une phrase
correcte de plus — et il défait n'importe quelle profondeur d'accidents
empilés, là où l'extrait JavaScript s'arrête après quatre tours.
Deux choses que ce fichier fait exprès. Il appelle `fix_encoding` et non `fix_text` : le second redresse aussi les guillemets, ce qui transforme « L'été à Nice », déjà correct, en une version à apostrophe droite — une réécriture silencieuse de la typographie française. Et il rend
`changed` au lieu de réparer en place, parce qu'une réparation que personne
n'a notée ne se distingue pas d'une donnée qui a toujours été ainsi.
"""
from __future__ import annotations
import ftfy
# The character a decoder writes when it gave up: the byte is gone, and no
# round trip brings it back.
REPLACEMENT = "�"
def repair_encoding(text) -> dict:
"""
Undo a wrong decoding, and say whether anything was undone.
`changed` is the flag a caller logs: the repair is a rewrite of somebody's
data, and it is wrong often enough — see this entry's breaking point — that
it must not happen in silence.
`lossy` says the text that comes back carries replacement characters.
Those are bytes a decoder threw away, usually before this function ever
saw the string, and nothing here restores them. When the input already
carries one, `ftfy` reads it as a byte and can drop the character in front
of it — see the header — so a `lossy` text is one to import again, not one
to repair.
"""
if not isinstance(text, str):
return _report(None, False, False, f"text is expected, not {type(text).__name__}")
repaired = ftfy.fix_encoding(text)
return _report(repaired, repaired != text, REPLACEMENT in repaired, None)
def _report(text, changed: bool, lossy: bool, reason: str | None) -> dict:
return {"text": text, "changed": changed, "lossy": lossy, "reason": reason}JavaScript
/**
* Réparer un texte décodé avec le mauvais jeu de caractères.
*
* Niveau N0. « Crédit Agricole » n'est pas du texte abîmé : c'est
* « Crédit Agricole » dont les octets UTF-8 ont été lus comme du
* Windows-1252. L'opération est exactement inversible — réécrire les
* caractères en octets Windows-1252, relire ces octets en UTF-8 — et c'est une
* identité sur les octets, pas un jugement.
*
* `ftfy` le fait en Python, et depuis dix ans. Il connaît aussi les variantes :
* deux tours du même accident, un Windows-1252 qui aurait dû être du
* cyrillique, un UTF-8 lu en UTF-16. Il n'en existe pas de portage en
* JavaScript, si bien que ce fichier écrit lui-même la transformation
* principale, en une trentaine de lignes.
*
* Les deux s'accordent sur trente-neuf des quarante-sept chaînes de cette
* fiche, et ils se séparent dans les deux sens.
*
* Ce fichier répare quatre chaînes courtes que `ftfy` refuse :
* « ÃŽle-de-France », « ÃŽles Canaries », « ÃŽlot » et « Å’uvre ». `ftfy` pèse
* la chaîne entière et renonce quand ce qui en sortirait est une suite courte
* ouvrant sur une majuscule accentuée — il répare le même mot à l'intérieur de
* « Région ÃŽle-de-France ». En travaillant segment par segment, ce fichier
* n'a pas cette heuristique. Il laisse aussi tranquille un texte qui porte déjà
* un caractère de remplacement, là où `ftfy` lit ce caractère comme un octet et
* supprime celui qui le précède.
*
* Dans l'autre sens, `ftfy` porte une règle que ce fichier n'a pas — un « Ã »
* suivi d'une espace ordinaire est lu comme le « à » dont l'espace insécable a
* été perdue en route, ce qui répare un cas réel de plus et réécrit une phrase
* correcte de plus — et il défait n'importe quelle profondeur d'accidents
* empilés, là où ce fichier s'arrête après ROUNDS tours.
*
* Deux choses que ce fichier fait exprès. Il ne répare que l'encodage et laisse les guillemets tranquilles : le `fix_text` plus large de `ftfy` les redresse, ce qui transforme « L'été à Nice », déjà correct, en une version à apostrophe droite — une réécriture silencieuse de la typographie française. Et il rend
* `changed` au lieu de réparer en place, parce qu'une réparation que personne
* n'a notée ne se distingue pas d'une donnée qui a toujours été ainsi.
*/
// Windows-1252, positions 0x80 to 0x9F, where it differs from Latin-1. The
// five code points that are unassigned there keep their Latin-1 value, which
// is what `ftfy` calls a sloppy Windows-1252 and what real files contain.
const WINDOWS_1252 =
'€\u0081‚ƒ„…†‡ˆ‰Š‹Œ\u008d'
+ 'Ž\u008f\u0090‘’“”•–—˜™š›'
+ 'œ\u009džŸ';
// The character a decoder writes when it gave up: the byte is gone, and no
// round trip brings it back.
export const REPLACEMENT = '�';
// How many times the same accident may have been applied. Two is common — a
// file repaired once and re-imported — and four is already generous.
export const ROUNDS = 4;
const strict = new TextDecoder('utf-8', { fatal: true });
/** How many bytes a UTF-8 sequence starting with this byte should have. */
const sequenceLength = (byte) => (byte >= 0xf0 ? 4 : byte >= 0xe0 ? 3 : byte >= 0xc2 ? 2 : 0);
/** The byte this character would have been in Windows-1252, or null. */
function byteOf(character) {
const special = WINDOWS_1252.indexOf(character);
if (special >= 0) return 0x80 + special;
const code = character.codePointAt(0);
return code <= 0xff ? code : null;
}
/**
* Undo a wrong decoding, and say whether anything was undone.
*
* `changed` is the flag a caller logs: the repair is a rewrite of somebody's
* data, and it is wrong often enough — see this entry's breaking point — that
* it must not happen in silence.
*
* `lossy` says the text that comes back carries replacement characters. Those
* are bytes a decoder threw away before this function ever saw the string, and
* nothing here restores them: a `lossy` text is one to import again, not one to
* repair.
*
* @param {string} text
*/
export function repairEncoding(text) {
if (typeof text !== 'string') {
return report(null, false, false, `text is expected, not ${kindOf(text)}`);
}
let repaired = text;
for (let round = 0; round < ROUNDS; round += 1) {
const next = onePass(repaired);
if (next === repaired) break;
repaired = next;
}
return report(repaired, repaired !== text, repaired.includes(REPLACEMENT), null);
}
/**
* One round: every run of characters whose Windows-1252 bytes form a valid
* multi-byte UTF-8 sequence is decoded; everything else is left exactly as it
* is. Working run by run rather than on the whole string is what lets a text
* where only some fields are broken come back with the rest untouched.
*/
function onePass(text) {
const characters = [...text];
const bytes = characters.map(byteOf);
const pieces = [];
let i = 0;
while (i < characters.length) {
const length = bytes[i] === null ? 0 : sequenceLength(bytes[i]);
const candidate = length > 0 && i + length <= characters.length
&& bytes.slice(i + 1, i + length).every((b) => b !== null && b >= 0x80 && b <= 0xbf);
if (candidate) {
try {
pieces.push(strict.decode(Uint8Array.from(bytes.slice(i, i + length))));
i += length;
continue;
} catch {
// Overlong, surrogate or out of range: not a sequence, keep the character.
}
}
pieces.push(characters[i]);
i += 1;
}
return pieces.join('');
}
const kindOf = (value) => (value === null ? 'null' : typeof value);
const report = (text, changed, lossy, reason) => ({ text, changed, lossy, reason });Essayer
interactifLe code ci-dessus tourne dans votre navigateur. Écrivez dans le champ : le résultat se refait sous vos yeux.
Collez une valeur sortie d’un import. Rien ne part sur le réseau : la réparation est une opération sur les octets, faite ici même.
Ou partez d’un exemple
La saisie libre et les autres exemples ont besoin de JavaScript. L’exemple affiché ci-dessous, lui, a été calculé à la construction du site.
3 passages attrapés
Ce que le code a repéré
Crédit Agricole, 12 rue des Frères-Lumière
Ce qu’il renvoie
Crédit Agricole, 12 rue des Frères-Lumière
Ce qu’il en conclut
Réparé
Les octets ont été réécrits en Windows-1252 puis relus en UTF-8.
Là, ça casse
Ce que ça implique
- Vos données ne sortent pas
- Résultat toujours identique
- Se teste unitairement, cas par cas
- Dépend d’une bibliothèque
- Empreinte négligeable
Ce que cela ajoute à votre périmètre réglementaire
- Aucun périmètre spécifique ajouté : le texte est réécrit dans votre processus et n’est transmis à personne
- La réparation modifie une donnée enregistrée : le drapeau rendu est ce qui permet d’en garder la trace
Point de rupture
Le réparateur ne sait pas que le texte parle de l’encodage : « on voit « é » au lieu de « é » » ressort en « on voit « é » au lieu de « é » », et l’exemple cité a disparu. Témoin dans le même test : vingt chaînes déjà correctes, dont « São Paulo » et « Ångström », ressortent inchangées.
Quand passer au niveau suivant
Il n’y a pas de niveau au-dessus. Le jour où le texte porte le caractère de remplacement — « Cr�dit » —, l’octet a été jeté avant que ce code le voie : aucun niveau de cette fiche ne le fait revenir, et la réponse est de réimporter la source.
Les quatre options
- RecommandéN0Aller-retour Windows-1252 vers UTF-8, avec un drapeau sur ce qui a changéGratuit · Instantané
- N1Niveau sans objet ici
- N2Niveau sans objet ici
- N3Niveau sans objet ici
Les autres options
N1Niveau sans objet ici
Niveau sans objet ici
Modèle classique légerN1
Il n’y a rien à apprendre : la transformation est une table de 256 octets et une norme, appliquée dans un sens ou dans l’autre, et elle est exactement inversible. Un classifieur entraîné sur des paires abîmé-correct approcherait une bijection, et le seul endroit où il pourrait faire mieux — décider si une chaîne donnée doit être réparée — est précisément celui où une erreur écrit une donnée fausse dans votre base.
N2Niveau sans objet ici
Niveau sans objet ici
Petit modèle spécialisé auto-hébergéN2
Même argument, avec un modèle à charger. Et une objection propre à ce besoin : la réparation s’applique à une colonne entière, souvent des millions de lignes, dans un traitement par lots où le coût par ligne est la seule chose qui compte. Un aller-retour d’octets se mesure en microsecondes ; une inférence par ligne, non.
N3Niveau sans objet ici
Niveau sans objet ici
API de LLM généralisteN3
C’est le niveau que cette fiche existe pour refuser. Un modèle à qui l’on demande de réparer un texte rend un texte plausible, et il n’existe aucun moyen de distinguer, dans sa sortie, ce qu’il a décodé de ce qu’il a deviné — sur un nom propre, une référence ou une adresse, la différence est la donnée elle-même. L’aller-retour d’octets, lui, se vérifie : on le rejoue dans l’autre sens et on retrouve l’entrée.
Pour aller plus loin
- ftfy — la bibliothèque Python, sa documentation et ce qu’elle sait défaire
- ftfy — l’explication de la méthode, par son autrice
- Unicode — le caractère de remplacement U+FFFD et sa raison d’être
- WHATWG Encoding — la table windows-1252 telle que l’appliquent les navigateurs
Source des chiffres
- ftfy sur PyPI (consulté) : version 6.3.1, publiée le 26 octobre 2024, licence Apache 2.0
- ftfy, documentation de fix_encoding et fix_text (consultée) : la seconde applique aussi uncurl_quotes, ce que le test de cette fiche reproduit sur « L’été à Nice »
- WHATWG Encoding, index windows-1252 (consulté) : les trente-deux positions 0x80–0x9F qui distinguent windows-1252 de latin-1, recopiées dans l’extrait JavaScript
- Recherche d’un équivalent de ftfy en JavaScript (registre npm, consulté) : aucun paquet de réparation d’encodage de ce genre ; les résultats voisins détectent l’encodage (chardet) ou le convertissent (encoding-japanese), ce qui est un autre travail
Métadonnées
Information générale, tenue à jour au mieux. Ce n’est pas un avis juridique. Faites valider votre situation par un professionnel.