Aller au contenu principal

Concept #027

Architecture

UTF-8

#0277 min de lecture
  • encodage
  • standard
  • fondamentaux
UTF-8 : l'encodage universel du web

Le principe en une phrase

UTF-8 est un encodage de caractères à longueur variable capable de représenter tous les caractères Unicode, tout en restant rétrocompatible avec l'ASCII historique. C'est le standard dominant du web, utilisé par plus de 98 % des sites.

Le coup de génie d'UTF-8, c'est cette rétrocompatibilité. Un texte en anglais pur encodé en ASCII est déjà du UTF-8 valide, sans aucune modification. C'est cette transition douce, sans casser l'existant, qui lui a permis de conquérir le monde.


Le problème historique : la tour de Babel des encodages

Avant Unicode et UTF-8, chaque langue (ou groupe de langues) avait son propre encodage. L'ASCII couvrait l'anglais avec 128 caractères (7 bits). Pour les langues européennes, ISO-8859-1 (Latin-1) étendait à 256 caractères. Le japonais utilisait Shift-JIS, le russe KOI8-R, le chinois GB2312...

Le résultat : ouvrir un fichier avec le mauvais encodage produisait du charabia. Les caractères accentués français apparaissaient comme des symboles incompréhensibles sur un système configuré en japonais. C'était le règne du mojibake (terme japonais pour "texte corrompu").

Texte original : "Développeur français"
Lu en Latin-1 :  "Développeur français"   (correct)
Lu en Shift-JIS : "Développeur français"  (mojibake)

Unicode : le catalogue universel

Unicode est né de l'ambition de cataloguer tous les caractères de toutes les langues humaines dans un seul référentiel. Chaque caractère reçoit un numéro unique appelé code point, noté U+XXXX.

Quelques exemples :

CaractèreCode pointDescription
AU+0041Lettre latine majuscule A
éU+00E9Lettre latine minuscule e accent aigu
U+4E16Caractère chinois "monde"
U+1F600Emoji visage souriant

Unicode définit actuellement plus de 150 000 caractères couvrant 161 écritures. Mais Unicode est un catalogue, pas un encodage. Il dit "le caractère é a le numéro 233" mais ne dit pas comment stocker ce numéro en mémoire. C'est le rôle des encodages comme UTF-8, UTF-16 et UTF-32.


UTF-8 : l'encodage à longueur variable

UTF-8 encode chaque code point Unicode sur 1 à 4 octets, selon sa valeur :

Plage de code pointsOctetsBits utilesPréfixe binaire
U+0000 -- U+007F170xxxxxxx
U+0080 -- U+07FF211110xxxxx 10xxxxxx
U+0800 -- U+FFFF3161110xxxx 10xxxxxx 10xxxxxx
U+10000 -- U+10FFFF42111110xxx 10xxxxxx 10xxxxxx 10xxxxxx

Exemple concret : encoder "Café"

C  → U+0043 → 0x43        → 1 octet  : 01000011
a  → U+0061 → 0x61        → 1 octet  : 01100001
f  → U+0066 → 0x66        → 1 octet  : 01100110
é  → U+00E9 → 0xC3 0xA9   → 2 octets : 11000011 10101001

"Café" occupe donc 5 octets en UTF-8. En ASCII, "Cafe" (sans accent) occupe 4 octets. Le surcoût est minimal pour les langues latines.

La rétrocompatibilité ASCII

Les 128 premiers caractères (U+0000 à U+007F) sont encodés sur un seul octet, dont le bit de poids fort est toujours 0. C'est exactement le même encodage que l'ASCII.

Conséquence : tout fichier ASCII valide est automatiquement un fichier UTF-8 valide. Les outils, protocoles et systèmes qui ne connaissent que l'ASCII continuent de fonctionner sur du UTF-8 anglais. C'est cette compatibilité transparente qui a permis une adoption progressive sans casser l'infrastructure existante.


La propriété de tri des octets

UTF-8 possède une propriété souvent méconnue mais particulièrement utile : trier les octets bruts revient à trier les caractères selon l'ordre Unicode.

C'est possible grâce à la structure des préfixes binaires. Un caractère sur 1 octet commence toujours par 0, un caractère sur 2 octets par 110, sur 3 octets par 1110, sur 4 octets par 11110. Ces préfixes sont ordonnés de telle sorte que l'ordre lexicographique des octets respecte l'ordre des code points.

// En UTF-8, le tri d'octets bruts est cohérent avec l'ordre Unicode
const words = ["banana", "apple", "cherry"];
 
// Tri par octets bruts (Buffer.compare)
words.sort((a, b) =>
  Buffer.from(a, "utf8").compare(Buffer.from(b, "utf8"))
);
// Résultat : ["apple", "banana", "cherry"]

Cette propriété simplifie les index de bases de données, les arbres de recherche et les algorithmes de tri sur des données textuelles encodées en UTF-8 -- pas besoin de décoder les octets en code points pour les comparer.


UTF-8 vs UTF-16 vs UTF-32

UTF-8UTF-16UTF-32
Taille par caractère1 à 4 octets2 ou 4 octets4 octets fixe
ASCII compatibleOuiNonNon
Taille texte anglaisCompacte (1 octet/car.)Gaspille (2 octets/car.)Très lourd (4 octets/car.)
Taille texte CJK3 octets/car.2 octets/car.4 octets/car.
Utilisé parWeb, Linux, protocolesWindows, Java, JavaScriptPeu utilisé

UTF-8 domine le web et les systèmes Unix/Linux. UTF-16 est utilisé en interne par JavaScript (String en JS est du UTF-16) et Windows. UTF-32 est rarement utilisé en stockage mais pratique en mémoire quand on a besoin d'un accès par index en O(1) à chaque caractère.


Les pièges courants en développement

Longueur d'une chaîne vs nombre d'octets

const text = "Café";
 
console.log(text.length);                          // 4 (caractères)
console.log(Buffer.from(text, "utf8").length);     // 5 (octets)
 
const emoji = "Bonjour ";
console.log(emoji.length);                         // 10 en JavaScript !
// JavaScript utilise UTF-16 en interne.
// L'emoji est un surrogate pair → compte pour 2 "unités" en .length

Couper une chaîne UTF-8 au mauvais endroit

Si vous stockez du texte en base de données avec une limite en octets (pas en caractères), vous risquez de couper un caractère multi-octets en plein milieu :

const text = "Café au lait";
const buffer = Buffer.from(text, "utf8");
 
// Couper à 5 octets : "Caf" + premier octet de "é"
const truncated = buffer.subarray(0, 4).toString("utf8");
// Résultat : "Caf" — le "é" incomplet est perdu ou remplacé par un caractère de remplacement
 
// La bonne approche : couper au niveau des caractères
const safe = [...text].slice(0, 4).join("");
// Résultat : "Café"

Le BOM (Byte Order Mark)

Certains éditeurs (notamment sur Windows) ajoutent un BOM (EF BB BF) au début des fichiers UTF-8. Ce marqueur est invisible mais peut casser des parseurs JSON, des scripts shell ou des headers HTTP. Préférez toujours UTF-8 sans BOM.


UTF-8 en pratique

Quelques règles pour éviter les problèmes d'encodage :

  • Déclarez toujours l'encodage explicitement. En HTML : <meta charset="UTF-8">. En HTTP : Content-Type: text/html; charset=utf-8.
  • Configurez vos bases de données en UTF-8. En MySQL, utilisez utf8mb4 (pas utf8 qui ne supporte que 3 octets et ne couvre pas les emoji).
  • Utilisez UTF-8 partout. Fichiers sources, configuration, logs, API. Un seul encodage dans tout le système élimine les problèmes de conversion.
  • Attention aux opérations sur les octets quand vous manipulez du texte. Taille, découpe, recherche -- travaillez au niveau des caractères, pas des octets.

Résumé

UTF-8 est le standard d'encodage du web moderne. Sa rétrocompatibilité avec ASCII, sa compacité pour les textes latins, et sa capacité à représenter l'intégralité d'Unicode en font le choix par défaut pour pratiquement tous les contextes. Et cerise sur le gâteau : trier les octets bruts revient à trier les caractères dans l'ordre Unicode -- une propriété qui simplifie de nombreux algorithmes de traitement de texte.