UTF-8 est une conception brillante
(iamvishnu.com)- UTF-8 est une méthode d’encodage à longueur variable qui permet de représenter des millions de caractères tout en conservant une compatibilité ascendante avec ASCII
- La zone 7 bits identique à ASCII (
U+0000~U+007F) utilise telle quelle 1 octet, ce qui fait qu’un fichier ASCII est aussi un fichier UTF-8 valide - Les autres caractères sont représentés par des séquences de 2 à 4 octets ; le motif de bits de l’octet de tête en définit la longueur, et les octets suivants commencent par
10, ce qui permet de les identifier comme octets de continuation - Grâce à cette conception, UTF-8 peut gérer un jeu de caractères universel tout en restant parfaitement compatible avec les systèmes ASCII existants, ce qui en fait l’encodage de caractères le plus utilisé
- D’autres encodages Unicode comme UTF-16 ou UTF-32 n’offrent pas cette compatibilité ASCII
L’excellence de la conception de UTF-8
- Lorsque j’ai découvert l’encodage UTF-8 pour la première fois, j’ai été très impressionné par sa structure compatible avec l’ASCII existant, tout en réunissant dans un seul système des millions de caractères issus de langues et d’écritures différentes
- Fondamentalement, UTF-8 peut exploiter jusqu’à 32 bits, tandis que ASCII n’en utilise que 7
- Les principes de conception de UTF-8 sont les suivants
- Tout fichier encodé en ASCII est un fichier UTF-8 valide
- Tout fichier UTF-8 ne contenant que des caractères ASCII est un fichier ASCII valide
- L’idée de fusionner un ancien système limité à 128 caractères avec un système couvrant des millions de caractères est particulièrement novatrice
Concepts de base de UTF-8
- UTF-8 est un encodage de caractères à largeur variable (variable-width encoding) conçu pour représenter tous les caractères du jeu de caractères Unicode
- Chaque caractère est encodé sur 1 à 4 octets
- Les 128 premiers caractères (
U+0000~U+007F) sont stockés sur un seul octet, ce qui assure la compatibilité ascendante avec ASCII - Les autres caractères sont encodés sur deux, trois ou quatre octets
- Les bits de tête du premier octet déterminent le nombre total d’octets nécessaires à l’encodage
| Motif sur 1 octet | Nombre d’octets | Motif de la séquence complète d’octets |
|---|---|---|
| 0xxxxxxx | 1 | 0xxxxxxx (ASCII classique) |
| 110xxxxx | 2 | 110xxxxx 10xxxxxx |
| 1110xxxx | 3 | 1110xxxx 10xxxxxx 10xxxxxx |
| 11110xxx | 4 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
- Les 2e, 3e et 4e octets d’une séquence multi-octets commencent toujours par
10, ce qui signale clairement qu’il s’agit d’octets de continuation - En combinant les bits restants de l’octet principal et des octets de continuation, on obtient un point de code
- Un point de code est un identifiant Unicode unique, représenté avec le préfixe "U+" et en hexadécimal
- Exemple : le point de code de "A" est
U+0041
- Le processus d’interprétation d’un caractère à partir d’octets UTF-8 est le suivant
- 1. On lit un octet ; si le début vaut 0, il s’agit d’un caractère sur un seul octet (ASCII), on utilise alors les 7 bits restants pour le caractère, puis on passe à l’octet suivant
- 2. Si ce n’est pas 0
- 110 signifie un caractère sur 2 octets, on lit donc un octet supplémentaire
- 1110 signifie un caractère sur 3 octets, on lit donc les 2 octets suivants
- 11110 signifie un caractère sur 4 octets, on lit donc 3 octets supplémentaires
- 3. On combine, pour les octets déterminés, les bits restants hors bits de tête afin d’obtenir la valeur binaire du point de code
- 4. On recherche ce point de code dans le jeu de caractères Unicode et on l’affiche à l’écran
- 5. Puis on recommence avec l’octet suivant
Exemple : le caractère hindi "अ"
- Représentation UTF-8 :
11100000 10100100 10000101(3 octets) - Premier octet (
11100000) → indique qu’il s’agit d’un caractère sur 3 octets - Combinaison des bits utiles des trois octets →
00001001 00000101= hexadécimal0x0905 - Le point de code
U+0905correspond au caractère dévanagari "अ"
Exemples de fichiers
-
1.
Hey👋 Buddy- Composé de 13 octets au total
- Caractères ASCII (H, e, y, B, u, d, d, y, espace) → 1 octet chacun
- 👋 (U+1F44B) → 4 octets
11110000 10011111 10010001 10001011
- Ce fichier est un fichier UTF-8 valide, mais comme il contient un caractère non ASCII (emoji), il n’est pas compatible ASCII
- Composé de 13 octets au total
-
2.
Hey Buddy- 9 octets au total, tous dans la plage ASCII
- Ce fichier est donc à la fois un fichier ASCII valide et un fichier UTF-8 valide
Comparaison avec d’autres encodages
- Il existe quelques encodages compatibles avec ASCII, mais aucun n’est aussi largement utilisé que UTF-8
- GB18030 (standard chinois), entre autres, offre aussi une compatibilité ASCII, mais n’est pas largement utilisé
- La famille ISO/IEC 8859 repose sur une extension sur un seul octet (jusqu’à 256 caractères), ce qui en limite la portée
- UTF-16/UTF-32 ne sont pas compatibles ASCII
- 'A' (U+0041) : en UTF-16
00 41, en UTF-3200 00 00 41
- 'A' (U+0041) : en UTF-16
Bonus : UTF-8 Playground
- Un outil interactif pour explorer visuellement le processus d’encodage UTF-8
- https://utf8-playground.netlify.app/
1 commentaires
Réactions sur Hacker News
Dans UTF-8, les octets de continuation commencent toujours par
10, donc même si l’on saute à un octet arbitraire, on peut immédiatement voir si l’on est au début d’un caractère ou sur un octet de continuation, ce qui permet de retrouver facilement le début du caractère suivant ou précédent. Avec un encodage comme celui des entiers à longueur variable d’EBML (qui inverse les1/0pour préserver la compatibilité ASCII sur un seul octet), il est beaucoup plus difficile de repérer directement le début d’un caractère à une position arbitraire. Voir RFC8794 section 4.4 pour les détailsOui, c’est un énorme avantage de UTF-8. On peut se déplacer librement en avant et en arrière dans une chaîne UTF-8 sans devoir la relire depuis le début. En Python, pour permettre l’indexation des chaînes par caractère, CPython utilise des wide characters. À une époque on pouvait choisir des caractères sur 2 ou 4 octets, puis cela a été automatisé à l’exécution. Mais cela reste du wide character, pas du UTF-8. Par exemple, un seul emoji peut multiplier par quatre la taille d’une chaîne. J’avais plutôt réfléchi à utiliser UTF-8 en interne, avec un type d’index opaque, sur lequel on pourrait ajouter ou soustraire de petits entiers pour se déplacer dans la chaîne. Ce n’est que lorsqu’on convertit réellement en entier ou qu’on fait un accès direct que l’index du caractère serait calculé. Avec cette approche, les expressions régulières et autres pourraient aussi exploiter cet objet d’index opaque et bien fonctionner sur une représentation UTF-8
Je pense que LEB128/VLQ est meilleur que l’encodage d’entiers à longueur variable d’EBML. On distingue via le bit de poids fort dans l’octet :
0signifie fin de séquence, l’octet suivant démarre une nouvelle séquence ;1signifie qu’il faut continuer à reculer jusqu’à trouver un MSB à0. Il existe aussi des implémentations efficaces optimisées SIMD. La différence entre LEB128 et VLQ n’est qu’une question d’endianness. ASCII serait0xxxxxxx, les caractères étendus1xxxxxxx 0xxxxxxx, puis1xxxxxxx 1xxxxxxx 0xxxxxxx, etc., ce qui permettrait d’encoder jusqu’à0x1FFFFFsur 3 octets, davantage que nécessaire pour Unicode. Ce n’est pas auto-synchronisant, mais c’est plus compact. ASCII resterait sur 1 octet, et des points de code comme les symboles mathématiques ou le japonais en dessous deU+3FFFpourraient tenir sur 2 octets, ce qui aiderait à réduire la taille du codeÀ mon avis, cela n’est vrai qu’à condition que le texte ne soit ni corrompu ni malicieusement altéré. D’innombrables vulnérabilités de sécurité sont apparues lors du parsing ou de l’échappement de séquences UTF-8 invalides. On peut voir par exemple le problème PostgreSQL CVE-2025-1094, ainsi qu’une liste de CVE liées à UTF-8
Ce n’est pas strictement vrai. Avec un UTF-8 invalide, un caractère peut devenir un octet de continuation. Par exemple, une entrée comme
0b01100001 0b10000000 0b01100001donne trois caractères,a�a. Pour savoir si un caractère affiché commence à cet endroit, il faut regarder les 1 à 3 octets précédentsAvec une taille maximale de 4 octets pour un caractère multioctet, il suffit de vérifier jusqu’à 3 octets en arrière pour savoir si la position courante est un octet de continuation. Si aucun octet de début n’apparaît, on sait qu’il s’agit d’un caractère sur un seul octet. Je suppose que cela a été conçu dans une optique de récupération : même si la bibliothèque ne reconnaît pas correctement UTF-8, on peut ignorer les octets invalides en tête et en fin d’un segment tronqué et en extraire malgré tout une chaîne raisonnable
Je trouve vraiment UTF-8 remarquable. Le point clé, c’est la décision qu’ASCII n’utiliserait que 7 bits. Même en 1963, ce choix des 7 bits était un peu particulier. Je me demande si c’était simplement un accident historique. Je me demande si les concepteurs d’ASCII avaient envisagé d’utiliser un bit supplémentaire pour ajouter plus de symboles, ou s’ils pensaient déjà aux code pages et à l’extensibilité
Je ne connais pas la raison exacte, mais autrefois 8 bits n’étaient pas toujours disponibles tels quels. Le schéma 7 bits + 1 bit de parité ou de drapeau était courant (c’est aussi pour cela que l’e-mail encode encore 8 bits en 7 bits via le quoted-printable). Quand une transmission transporte bien les 8 bits sans altération, on dit qu’elle est 8-bit clean. Dans ce contexte, UTF-8 exploite finalement très bien ce 8e bit laissé libre par ASCII. Voir aussi l’explication sur 8-bit clean
Je ne suis pas expert, mais j’ai lu il y a longtemps l’histoire d’ASCII. ASCII vient des codes de téléscripteur, eux-mêmes issus des codes télégraphiques. Le code Morse étant de longueur variable, il était pénible à implémenter mécaniquement. On a donc créé le code Baudot sur 5 bits. L’idée était de simplifier les machines avec un code à longueur fixe, et aussi de réduire la fatigue des opérateurs. C’est à cause de Baudot qu’on parle encore aujourd’hui de débit en bauds. Plus tard, l’usage de machines à écrire pour saisir sur bande perforée a apporté davantage de souplesse, et des caractères spéciaux comme Carriage Return et Line Feed ont été ajoutés. Les débuts de l’informatique ont adopté la carte perforée comme entrée, et IBM a développé un nouveau système sur 8 bits pour traiter les cartes plus rapidement, qui a servi de base à ASCII. En fin de compte, les codes binaires ont été étendus au fil des progrès techniques. ASCII est aussi un produit transitoire, antérieur à la convention du byte sur 8 bits
En pratique, le bit restant était réutilisé pour la parité
Les extensions 8 bits d’ASCII (du type ISO 8859-x) ont été très largement utilisées pendant des décennies, et elles sont encore employées dans les code pages standard de Windows. Même si ASCII avait été défini dès le départ sur 8 bits, les caractères essentiels auraient probablement occupé les 128 premiers codes, ce qui l’aurait aussi rendu adapté à UTF-8. Si l’on parle d’accident historique, ce n’est pas tant qu’ASCII soit sur 7 bits, mais plutôt que l’évolution de l’informatique ait surtout eu lieu dans le monde anglophone, et que l’anglais soit largement exprimable sur 7 bits
Le 7 bits n’a rien de particulièrement étrange en soi. Baudot était sur 5 bits, ce qui s’est révélé insuffisant, d’où l’apparition de codes sur 6 bits puis d’ASCII sur 7 bits. IBM a standardisé le byte sur 8 bits (code EBCDIC) avec le System/360, mais chez les autres constructeurs la taille du byte n’était pas fixe. Les 7 bits paraissent bizarres aujourd’hui, mais à l’époque il n’y avait pas nécessairement d’ajustement propre entre les caractères et les mots machine
Je suis d’accord pour dire que UTF-8 est mieux conçu qu’on ne pourrait l’espérer. En revanche, Unicode a un problème de périmètre qui s’élargit trop. On peut se demander ce qui devrait vraiment être inclus dans Unicode. Intuitivement, on pourrait dire : « tous les caractères imprimables distincts que l’humanité utilise pour communiquer ». Mais en pratique, ce n’est pas si simple.
La distinction n’est pas nette. Certains points de code existent sous forme de caractères combinants
Ce n’est pas concret. Un même caractère peut s’écrire de plusieurs façons. Des caractères visuellement identiques peuvent avoir des points de code différents et des significations différentes
Ce n’est pas uniquement de l’imprimable. Il existe des caractères de contrôle. Ils ont été inclus pour la compatibilité ASCII, mais Unicode a aussi ses propres caractères de contrôle Il ne semble pas encore y avoir de points Unicode animés. Au moins, ce qui est imprimable peut encore être mis sur papier. Mais je ne sais pas si cette invariance restera vraie à l’avenir. Au passage, parmi les encodages UTF que l’auteur ne mentionne pas, il y a aussi UTF-7. C’est proche de UTF-8, mais conçu à une époque où l’on supposait que l’usage du dernier bit n’était pas sûr sur certains réseaux des années 80. Il m’est déjà arrivé de recevoir un e-mail encodé en UTF-7. Je ne sais toujours pas comment il a été envoyé
UTF-7 a surtout été conçu pour des environnements de transport non 8-bit clean comme l’e-mail. Aujourd’hui c’est obsolète, et il ne sait même pas encoder les plans supplémentaires (sauf via les surrogate pairs de UTF-16). Il existe aussi UTF-9, mais c’est une parodie introduite dans une RFC du 1er avril (pour des environnements 36 bits comme le PDP-10)
Il y a quelque chose qui m’a toujours intrigué : un point de code Unicode peut théoriquement être encodé avec une séquence d’octets inutilement longue. UTF-8 l’interdit et n’autorise que la séquence la plus courte. Par exemple,
00000001et11000000 10000001pourraient représenter la même chose. Dans ce cas, ne pourrait-on pas concevoir le schéma autrement pour qu’il n’existe tout simplement aucun encodage illégal ? Par exemple, en faisant commencer la plage des séquences sur 2 octets à la toute dernière valeur valide, de sorte que11000000 10000001vaille128+1, et que0-127restent codés sur 1 octet. On n’aurait alors plus de codes illégaux, et les chaînes seraient parfois un peu plus courtes dans certains cas limites. Je me demande si cela n’a pas été retenu à cause du coût du matériel à l’époque. (Mise à jour : la vraie séquence de bits devait être10000001, corrigé)U+0080est-ilc2 80et nonc0 80, c’est-à-dire la première valeur juste après7f? Je pense que la réponse est la suivante a) Si l’on autorise les overlong encodings, cela ouvre des failles de sécurité quand certains composants ne vérifient que les formes courtes b) L’encodage/décodage UTF-8 standard peut se faire uniquement avec des opérations de masquage (bitmask) et de décalage (bitshift). Le schéma proposé nécessiterait en plus des soustractions Il y a eu une discussion à ce sujet dans des échanges par e-mail en 1992, et FSS-UTF incluait des constantes additives (voir plus bas)Le point clé est de préserver l’auto-synchronisation des motifs d’octets. Si l’on ne conserve pas des octets de continuation commençant comme
11000000 10000001, on perd la propriété consistant à retrouver systématiquement les frontières des points de code dans un flux UTF-8 tronqué. Si l’on ajoute en plus des opérations d’addition ou de soustraction, les décodeurs deviennent moins rapides. Aujourd’hui, tout se fait avec de simples opérations sur les bitsComme l’a dit quectophoton, les octets de continuation doivent toujours commencer par
10pour qu’un parseur puisse retrouver les frontières de points de code depuis n’importe quelle position. À l’époque de la conception de UTF-8, au début des années 90, on tenait compte du fait que de nombreux environnements de transmission n’étaient pas fiablesAvec le schéma proposé, les calculs d’encodage et de décodage deviennent plus complexes et plus lents. Aujourd’hui cela se résume à quelques décalages de bits, mais à l’époque, sur les machines lentes des années 90, c’était un point important
Si vous voulez en savoir plus sur la conception de UTF-8, voir le one-pager de Russ Cox et le récapitulatif historique de Rob Pike
UTF-8 est excellent, et j’aimerais vraiment qu’il soit utilisé partout (je te regarde, JavaScript). Mais son seul vrai défaut est que la manière d’interpréter des séquences d’octets invalides n’est pas clairement standardisée. Une conception qui « spécifierait obligatoirement une interprétation pour toute séquence d’octets » aurait été encore plus parfaite. Je pense qu’une approche comme celle de la spécification HTML5 peut fonctionner avec succès
J’ai une relation compliquée avec la backward compatibility. Je n’aime pas la confusion qu’elle apporte, mais j’apprécie aussi les démarches qui acceptent de casser des choses pour avancer. En même temps, des exemples comme UTF-8 ou EAN, qui préservent la compatibilité tout en étant intelligemment conçus, me plaisent beaucoup. Franchement, UTF-8 semble n’avoir presque rien sacrifié pour rester compatible
S’il fallait vraiment changer quelque chose, j’aurais peut-être remplacé une partie des caractères de contrôle par des caractères plus courants pour gagner encore un peu d’espace (si l’on accepte aussi de casser la compatibilité Unicode). Mais comme format d’encodage multioctet, pris isolément, je pense qu’il est quasiment optimal
J’aime beaucoup le lien vers le playground UTF-8 (utf8-playground.netlify.app). Ce serait bien si l’UI permettait aussi de saisir directement des points de code (pour l’instant, c’était seulement possible via l’URL). (Mise à jour : c’est déjà possible, la PR a été mergée)
Si vous voulez creuser davantage le sujet et que vous aimez le format Advent of Code, il y a plusieurs énigmes sur les encodages de texte sur i18n-puzzles. C’est très utile pour vraiment intérioriser le fonctionnement de UTF-8, UTF-16, etc.
Merci pour ce bon article. Moi aussi je recommande UTF-8, mais seulement avec un BOM. Sinon, une application ne peut pas savoir qu’il s’agit de UTF-8, ni même qu’il faut enregistrer en UTF-8. Par exemple, sous Windows, si l’on crée un nouveau document texte et qu’il ne contient qu’un BOM quand il est vide, n’importe quelle application sait ensuite automatiquement qu’elle doit l’éditer et l’enregistrer en UTF-8. Sans BOM, même si l’application essaie de détecter automatiquement l’encodage, ce n’est jamais totalement fiable, et la confusion augmente dès qu’on ajoute des accents ou d’autres caractères spéciaux (l’éditeur peut mal deviner la langue, ou Notepad peut avoir changé son encodage par défaut après une mise à jour). Donc oui pour UTF-8, mais avec un BOM défini par défaut au niveau de l’OS et des applications