Skip to content

Fondamentaux · Chapitre F.5

Caractères, ASCII et Unicode

Comment le texte devient des nombres : l’ASCII et ses astuces, les pages de code 8 bits et le mojibake, les points de code et les plans d’Unicode, l’algorithme UTF-8 bit par bit (avec un encodeur en direct), les paires de substitution UTF-16, la normalisation, et pourquoi un emoji peut être un caractère, cinq points de code et 18 octets.

La mémoire contient des nombres, donc le texte doit lui aussi être stocké sous forme de nombres. Un code de caractères attribue un nombre à chaque caractère : 65 pour A, 233 pour é, 128 512 pour 😀. Tous ceux qui échangent du texte doivent s’accorder sur le même code, c’est pourquoi les codes de caractères sont des normes. Il faut distinguer trois idées :

  • le caractère, une unité abstraite comme « lettre minuscule latine e accent aigu » ;
  • son point de code (code point), le nombre que la norme lui attribue ;
  • son encodage, les octets qui stockent ce nombre en mémoire ou dans un fichier.

Le glyphe affiché à l’écran (la forme dessinée par une police) est une quatrième chose, qui ne fait pas du tout partie du code de caractères.

L’ASCII

L’ASCII, American Standard Code for Information Interchange, a été publié pour la première fois en 1963. Il utilise 7 bits, donc 128 codes :

CodesContenu
0x00–0x1F32 caractères de contrôle : NUL (0x00), tabulation (0x09), saut de ligne LF (0x0A), retour chariot CR (0x0D), échappement (0x1B)…
0x20espace
0x30–0x39les chiffres 0 à 9
0x41–0x5AA à Z
0x61–0x7Aa à z
le reste de 0x21–0x7Eponctuation et symboles : ! 0x21, @ 0x40, ~ 0x7E…
0x7FDEL

La disposition a été pensée avec soin, et les programmes en tirent encore parti :

  • Majuscules et minuscules diffèrent d’un seul bit, 0x20 : A vaut 0x41 = 0100 0001, a vaut 0x61 = 0110 0001. Mettre à 1 ou à 0 le bit 5 change la casse d’une lettre ASCII.
  • Les chiffres se suivent à partir de 0x30, donc la valeur d’un caractère chiffre c est c - '0' : '7' - '0' vaut 7.
  • Les lettres sont dans l’ordre alphabétique, si bien que, pour un texte anglais simple, trier par code trie alphabétiquement, toutes les majuscules passant avant toutes les minuscules.

La plupart des caractères de contrôle étaient destinés aux téléscripteurs et aux liaisons de données, et servent rarement aujourd’hui, mais quelques-uns sont partout. Le C marque la fin d’une chaîne par NUL, l’octet 0. Unix termine les lignes par LF ; Windows utilise les deux octets CR LF, un héritage des téléscripteurs, qui avaient besoin d’un code pour ramener le chariot et d’un autre pour faire avancer le papier. Les réglages de fin de ligne de Git existent à cause de cette différence.

Huit bits et pages de code

Les ordinateurs stockent un caractère dans un octet, l’ASCII laisse donc 128 codes inutilisés, de 0x80 à 0xFF. Constructeurs et normes les ont remplis différemment, chaque jeu de 256 caractères formant une page de code :

  • l’ISO 8859-1, ou Latin-1, a ajouté les lettres accentuées des langues d’Europe occidentale : é vaut 0xE9, ü vaut 0xFC ;
  • Windows-1252, la variante de Latin-1 de Microsoft, met aussi des caractères imprimables en 0x80–0x9F, là où Latin-1 a des codes de contrôle : € vaut 0x80 ;
  • d’autres parties d’ISO 8859 couvraient les langues d’Europe centrale, le cyrillique, le grec, l’arabe, l’hébreu, etc., en réutilisant toutes les mêmes 128 codes.

Deux normes se confondent facilement ici. L’ISO 646 est la version internationale de l’ASCII, sur 7 bits, dont les variantes nationales remplaçaient quelques symboles comme # ou [ par des lettres locales ; elle n’ajoutait aucun caractère. Le Latin-1 sur 8 bits, c’est l’ISO 8859-1. La page de code de l’IBM PC d’origine, la 437, avait bien des visages souriants, mais aux codes 1 et 2, parmi les caractères de contrôle ; sa moitié haute, les codes 128 à 255, contenait des lettres accentuées, des lettres grecques et des caractères de dessin de cadres.

L’approche par pages de code a un défaut fatal : les octets ne disent pas dans quelle page de code ils sont. Décodez un texte avec la mauvaise, et vous obtenez du mojibake : des caractères brouillés. L’exemple classique aujourd’hui est un texte UTF-8 lu comme du Latin-1 ou du Windows-1252 : les deux octets de é, C3 A9, s’affichent é. Si vous avez déjà vu « café » sur une page web, c’est ce qui s’est passé.

Unicode : un nombre par caractère

Unicode, publié pour la première fois en 1991, attribue à chaque caractère de chaque système d’écriture un point de code unique, noté U+ suivi d’au moins quatre chiffres hexadécimaux : A est U+0041, é est U+00E9, € est U+20AC, l’emoji crabe 🦀 est U+1F980. Les 256 premiers points de code sont ceux de Latin-1, donc les 128 premiers sont l’ASCII.

Unicode a d’abord été conçu comme un code sur 16 bits, avec 65 536 points de code et sans caractères multioctets. Cette conception est dépassée depuis Unicode 2.0 en 1996. Les points de code vont désormais de U+0000 à U+10FFFF, un espace de 21 bits de 1 114 112 valeurs, divisé en 17 plans de 65 536 :

PlanPlageContenu
0, plan multilingue de baseU+0000–U+FFFFpresque toutes les écritures modernes, les symboles courants
1, plan multilingue complémentaireU+10000–U+1FFFFécritures historiques, symboles musicaux et mathématiques, la plupart des emoji
2 et 3U+20000–U+3FFFFidéogrammes CJC (chinois, japonais, coréen) plus rares
15 et 16U+F0000–U+10FFFFusage privé

Unicode 16.0, publié en 2024 et fourni avec le module unicodedata de Python 3.14, définit 154 998 caractères ; en comptant les points de code à usage privé et les demi-codets de substitution réservés, 294 579 points de code sont attribués. La majeure partie de l’espace est encore vide.

Un point de code n’est qu’un nombre ; encore faut-il le stocker sous forme d’octets. Unicode définit trois encodages : UTF-8, UTF-16 et UTF-32.

UTF-8

UTF-8 stocke chaque point de code sur 1 à 4 octets. Le nombre de bits à 1 en tête du premier octet indique combien d’octets le caractère occupe ; chaque octet suivant commence par 10. Les bits restants, notés x, contiennent le point de code :

Points de codeBits nécessairesOctet 1Octet 2Octet 3Octet 4
U+0000–U+007F70xxxxxxx
U+0080–U+07FF11110xxxxx10xxxxxx
U+0800–U+FFFF161110xxxx10xxxxxx10xxxxxx
U+10000–U+10FFFF2111110xxx10xxxxxx10xxxxxx10xxxxxx

L’encodage de é, U+00E9, pas à pas :

U+00E9 = 000 1110 1001             11 bits needed → 2-byte form
split:   00011 | 101001            5 bits, then 6 bits
fill:    110 00011  10 101001
bytes:   1100 0011  1010 1001   = C3 A9

Et le crabe, U+1F980, qui demande 17 bits, donc la forme à 4 octets :

U+1F980 = 0 0001 1111 1001 1000 0000        (21 bits)
split:    000 | 011111 | 100110 | 000000
bytes:    11110000 10011111 10100110 10000000 = F0 9F A6 80

Le programme ci-dessous fait la même chose en C, avec des décalages et des masques, et affiche les octets de quatre caractères :

Live · Un encodeur UTF-8

À essayer : Appuyez sur Step pour exécuter une instruction, Run pour animer ou Continue pour aller au bout ; les boutons L2 à L7 changent de niveau, vers le bas ou le haut.

C source · click a line number for a breakpoint
  1. int encode(int cp, unsigned char *out) {
  2. if (cp < 0x80) { /* 0xxxxxxx */
  3. out[0] = cp;
  4. return 1;
  5. }
  6. if (cp < 0x800) { /* 110xxxxx 10xxxxxx */
  7. out[0] = 0xC0 | (cp >> 6);
  8. out[1] = 0x80 | (cp & 0x3F);
  9. return 2;
  10. }
  11. if (cp < 0x10000) { /* 1110xxxx 10xxxxxx 10xxxxxx */
  12. out[0] = 0xE0 | (cp >> 12);
  13. out[1] = 0x80 | ((cp >> 6) & 0x3F);
  14. out[2] = 0x80 | (cp & 0x3F);
  15. return 3;
  16. }
  17. out[0] = 0xF0 | (cp >> 18); /* 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx */
  18. out[1] = 0x80 | ((cp >> 12) & 0x3F);
  19. out[2] = 0x80 | ((cp >> 6) & 0x3F);
  20. out[3] = 0x80 | (cp & 0x3F);
  21. return 4;
  22. }
  23. int main() {
  24. int text[4] = { 0x41, 0xE9, 0x20AC, 0x1F980 }; /* A é € crab */
  25. unsigned char buf[4];
  26. int total = 0;
  27. for (int i = 0; i < 4; i++) {
  28. int n = encode(text[i], buf);
  29. printf("U+%04X ->", text[i]);
  30. for (int j = 0; j < n; j++)
  31. printf(" %02x", buf[j]);
  32. printf("\n");
  33. total += n;
  34. }
  35. return total;
  36. }
step 0
Loading emulator…
Your program as you wrote it: the current line, its variables by name, and its output.

Il affiche 41, c3 a9, e2 82 ac et f0 9f a6 80, et renvoie 10, le nombre total d’octets : quatre caractères, dix octets.

Cette conception a plusieurs propriétés utiles :

  • Un texte ASCII est de l’UTF-8 valide, octet pour octet, et aucun octet d’un caractère multioctet n’est inférieur à 0x80. Un programme qui ne cherche que /, \n ou NUL fonctionne donc tel quel sur de l’UTF-8.
  • Il est autosynchronisant : les octets de continuation commencent toujours par 10 et les premiers octets jamais, donc depuis n’importe quelle position on retrouve le début du caractère suivant.
  • Trier des chaînes UTF-8 octet par octet les trie par point de code.
  • Chaque caractère a exactement un encodage valide. La forme « trop longue » C0 80 sur 2 octets pour NUL, par exemple, est invalide, et les décodeurs doivent la rejeter. L’accepter a causé des failles de sécurité, quand un filtre cherchait un octet que le décodeur produisait ensuite à partir d’une autre séquence.

UTF-8 a d’abord été conçu avec des formes allant jusqu’à 6 octets, qui pouvaient encoder des valeurs sur 31 bits, environ deux milliards de caractères ; depuis 2003 (RFC 3629), UTF-8 est limité à 4 octets et à U+10FFFF, pour correspondre à l’étendue d’UTF-16. UTF-8 est aujourd’hui l’encodage de l’écrasante majorité des pages web, l’encodage habituel du code source et des noms de fichiers sous Linux et macOS, et celui qu’exige JSON pour les données échangées entre systèmes.

UTF-16 et les paires de substitution

UTF-16 stocke chaque point de code sur une ou deux unités de 16 bits. Le plan multilingue de base prend une unité, égale au point de code. Pour les points de code au-delà de U+FFFF, on soustrait 0x10000, et les 20 bits restants sont coupés en deux moitiés de 10 bits, chacune ajoutée à une plage réservée : la moitié haute à 0xD800, la moitié basse à 0xDC00. Le résultat est une paire de substitution (surrogate pair) :

U+1F980 − 0x10000 = 0xF980 = 0000111110 0110000000   (20 bits)
high: 0xD800 + 0000111110 = 0xD83E
low:  0xDC00 + 0110000000 = 0xDD80

C’est pourquoi les points de code U+D800 à U+DFFF sont réservés et jamais attribués à des caractères. UTF-16 était le successeur naturel de l’Unicode 16 bits d’origine, et les systèmes conçus à cette époque l’utilisent en interne : Windows, Java et JavaScript. En JavaScript, "🦀".length vaut 2, parce que length compte des unités de 16 bits. UTF-32 stocke chaque point de code sur 4 octets : simple, mais gaspilleur, et rarement utilisé pour le stockage.

Un caractère, deux orthographes : la normalisation

Unicode peut souvent représenter le même texte de plusieurs façons. é existe comme point de code unique, U+00E9, mais peut aussi s’écrire e suivi de U+0301, ACCENT AIGU COMBINANT, qui s’attache à la lettre précédente. Les deux s’affichent à l’identique. En Python :

>>> import unicodedata as u
>>> nfc, nfd = u.normalize("NFC", "é"), u.normalize("NFD", "é")
>>> len(nfc), len(nfd), nfc == nfd
(1, 2, False)

Deux chaînes d’apparence identique sont différentes à la comparaison, et une recherche de l’une rate l’autre. La solution est la normalisation : convertir les deux dans la même forme avant de comparer. NFC compose les caractères chaque fois que possible ; NFD les décompose. La plupart des textes sont en NFC, mais les systèmes de fichiers de macOS ont longtemps stocké les noms décomposés, source classique de bogues « fichier introuvable » quand des noms passent d’un système à l’autre.

Ce que l’utilisateur appelle un caractère : les graphèmes

Certaines choses qu’un lecteur voit comme un seul caractère sont plusieurs points de code. L’emoji famille 👨‍👩‍👧 est fait de trois personnes reliées par deux liants sans chasse invisibles (zero-width joiner, U+200D). Un drapeau comme 🇫🇷 est fait de deux lettres « indicateurs régionaux », F et R. Unicode appelle graphème (grapheme cluster) ce que l’utilisateur perçoit comme un caractère, et la question « quelle est la longueur de cette chaîne ? » a quatre réponses différentes :

TexteGraphèmesPoints de codeUnités UTF-16Octets UTF-8
é (décomposé)1223
🇫🇷1248
👨‍👩‍👧15818

Ces comptes viennent de Python (len compte les points de code) et de Node.js (length compte les unités UTF-16 ; Intl.Segmenter compte les graphèmes). Conséquences pratiques : tronquer une chaîne à n points de code peut couper un emoji en deux, l’inverser point de code par point de code casse les accents et les drapeaux, et la « longueur maximale » d’un champ de texte ne veut pas dire la même chose selon les langages. Quand c’est le compte visible qui importe, utilisez un segmenteur de graphèmes.

Au niveau du CPU, rien de tout cela n’existe : les instructions ne voient que des octets, comme le note le chapitre sur les types de données.

À retenir

  • Un code de caractères associe des nombres (points de code) aux caractères ; un encodage transforme les points de code en octets.
  • L’ASCII a 128 codes sur 7 bits ; la casse diffère par le bit 5 (0x20) et les chiffres commencent à 0x30. Les pages de code 8 bits comme Latin-1 ont réutilisé 0x80–0xFF chacune à sa façon, d’où le mojibake quand on les confond.
  • Unicode n’est plus sur 16 bits : les points de code vont jusqu’à U+10FFFF (17 plans). Unicode 16.0 définit 154 998 caractères.
  • UTF-8 utilise 1 à 4 octets selon les motifs 0xxxxxxx, 110xxxxx 10xxxxxx… é = C3 A9, 🦀 = F0 9F A6 80. Il est compatible avec l’ASCII et autosynchronisant ; ses formes d’origine à 5 et 6 octets ont été supprimées en 2003.
  • UTF-16 utilise des paires de substitution au-delà de U+FFFF (🦀 = D83E DD80) ; c’est le format interne de Windows, Java et JavaScript.
  • Un même texte peut avoir des points de code différents (normalisation : NFC contre NFD), et un caractère visible peut compter de nombreux points de code (graphèmes : 👨‍👩‍👧 fait 5 points de code et 18 octets).

Fondamentaux

  1. F.1Niveaux d’abstraction et brève histoire des ordinateurs
  2. F.2Binaire et hexadécimal
  3. F.3Complément à deux et entiers signés
  4. F.4Virgule flottante (IEEE 754)
  5. F.5Caractères, ASCII et Unicode
  6. F.6Boutisme (endianness)
  7. F.7Parité, codes de Hamming et correction d’erreurs
  8. F.8Unités : kilo, kibi et compagnie