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 :
| Codes | Contenu |
|---|---|
| 0x00–0x1F | 32 caractères de contrôle : NUL (0x00), tabulation (0x09), saut de ligne LF (0x0A), retour chariot CR (0x0D), échappement (0x1B)… |
| 0x20 | espace |
| 0x30–0x39 | les chiffres 0 à 9 |
| 0x41–0x5A | A à Z |
| 0x61–0x7A | a à z |
| le reste de 0x21–0x7E | ponctuation et symboles : ! 0x21, @ 0x40, ~ 0x7E… |
| 0x7F | DEL |
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 :
Avaut 0x41 =0100 0001,avaut 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
cestc - '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 :
| Plan | Plage | Contenu |
|---|---|---|
| 0, plan multilingue de base | U+0000–U+FFFF | presque toutes les écritures modernes, les symboles courants |
| 1, plan multilingue complémentaire | U+10000–U+1FFFF | écritures historiques, symboles musicaux et mathématiques, la plupart des emoji |
| 2 et 3 | U+20000–U+3FFFF | idéogrammes CJC (chinois, japonais, coréen) plus rares |
| 15 et 16 | U+F0000–U+10FFFF | usage 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 code | Bits nécessaires | Octet 1 | Octet 2 | Octet 3 | Octet 4 |
|---|---|---|---|---|---|
| U+0000–U+007F | 7 | 0xxxxxxx | |||
| U+0080–U+07FF | 11 | 110xxxxx | 10xxxxxx | ||
| U+0800–U+FFFF | 16 | 1110xxxx | 10xxxxxx | 10xxxxxx | |
| U+10000–U+10FFFF | 21 | 11110xxx | 10xxxxxx | 10xxxxxx | 10xxxxxx |
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 :
À 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.
- int encode(int cp, unsigned char *out) {
- if (cp < 0x80) { /* 0xxxxxxx */
- out[0] = cp;
- return 1;
- }
- if (cp < 0x800) { /* 110xxxxx 10xxxxxx */
- out[0] = 0xC0 | (cp >> 6);
- out[1] = 0x80 | (cp & 0x3F);
- return 2;
- }
- if (cp < 0x10000) { /* 1110xxxx 10xxxxxx 10xxxxxx */
- out[0] = 0xE0 | (cp >> 12);
- out[1] = 0x80 | ((cp >> 6) & 0x3F);
- out[2] = 0x80 | (cp & 0x3F);
- return 3;
- }
- out[0] = 0xF0 | (cp >> 18); /* 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx */
- out[1] = 0x80 | ((cp >> 12) & 0x3F);
- out[2] = 0x80 | ((cp >> 6) & 0x3F);
- out[3] = 0x80 | (cp & 0x3F);
- return 4;
- }
- int main() {
- int text[4] = { 0x41, 0xE9, 0x20AC, 0x1F980 }; /* A é € crab */
- unsigned char buf[4];
- int total = 0;
- for (int i = 0; i < 4; i++) {
- int n = encode(text[i], buf);
- printf("U+%04X ->", text[i]);
- for (int j = 0; j < n; j++)
- printf(" %02x", buf[j]);
- printf("\n");
- total += n;
- }
- return total;
- }
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
/,\nou NUL fonctionne donc tel quel sur de l’UTF-8. - Il est autosynchronisant : les octets de continuation commencent toujours par
10et 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 80sur 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 :
| Texte | Graphèmes | Points de code | Unités UTF-16 | Octets UTF-8 |
|---|---|---|---|---|
| é (décomposé) | 1 | 2 | 2 | 3 |
| 🇫🇷 | 1 | 2 | 4 | 8 |
| 👨👩👧 | 1 | 5 | 8 | 18 |
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).