Aller au contenu

ISO-8859-1 et UTF-8⚓︎

Au delà de l'ASCII

Python n'est pas limité aux caractères ASCII.

>>> encodage("éèàêï")
[233, 232, 224, 234, 239]
>>> decodage([192, 198, 181, 182, 215])
'ÀÆμ¶×'

Les codes obtenus sont ceux de la table ISO-8859-1, aussi appelée latin-1. Pour des raisons de place en mémoire, Python va prendre la représentation la plus petite pour chaque caractère : ASCII ou latin-1 pour les premiers caractères, puis UTF-8 pour les autres.

Les caractères peuvent également être donnés directement à partir de leur code unicode :

>>> "\u0043\u0069\u0065\u006C\u0020\u00c9\u0074\u006F\u0069\u006C\u00E9"
'Ciel Étoilé'

Lorsque le code unicode n'utilise que 4 chiffres en hexadécimal, il peut s'écrire "\uHHHH", où les 4 H sont les chiffres en hexadécimal. Il faut toujours mettre 4 chiffres, même si les premiers sont 0.

Par contre s'il y a plus de chiffres, il faut utiliser "\UhhhhHHHH" en mettant 4 chiffres en hexadécimal. Néanmoins, ces symboles ne sont pas forcément pris en compte par la police de caractères utilisée par Python.

Il est aussi possible d'utiliser la description du caractère pour l'obtenir :

>>> print("\N{Latin Small Letter E with Acute}")
é

Nous verrons plus tard comment trouver ces descriptions.

Exercice 3

Découvrez le message codé en unicode ci-dessous :

###(Dés-)Active le code après la ligne # Tests (insensible à la casse)
(Ctrl+I)
Tronquer ou non le feedback dans les terminaux (sortie standard & stacktrace / relancer le code pour appliquer)
Évaluations restantes : /∞

Vous pouvez chercher d'autres symboles du même genre sur des sites présentant les tables unicodes, comme ce site. On y trouve notamment la table complète de tous les symboles unicode.

Les autres encodages

Python peut convertir n'importe quel texte dans n'importe quel encodage, à condition que les caractères utilisés existent dans cet encodage.

>>> "é".encode("utf8")
b'\xc3\xa9'
>>> "é".encode("latin1")
b'\xe9'
>>> "é".encode()
b'\xc3\xa9'

Les objets que l'on obtient ne sont pas des textes, mais des suites d'octets, identifiées par le b avant les apostrophes. On remarque que l'encodage de base proposé par Python est l'UTF-8, qui garantit que tout peut être encodé. On remarque également qu'en latin-1, le caractère n'est codé que sur un octet, contre 2 pour l'UTF-8. C'est pourquoi en interne, Python utilise le latin-1 lorsque c'est possible, pour économiser de l'espace mémoire. Voici les versions en binaire de ces octets.

>>> ' '.join([f"{c:08b}" for c in 'é'.encode('utf8')])
'11000011 10101001'
>>> ' '.join([f"{c:08b}" for c in 'é'.encode('latin1')])
'11101001'

Le format 08b pour les f-strings indique qu'il faut afficher le résultat en binaire, sur 8 bits.

En UTF-8, le caractère é correspondant à U+00E9 en unicode. Son écriture binaire est bien de la forme 110xxxxx 10xxxxxx.

Décoder une suite d'octets

À l'inverse, il est possible de décoder une suite d'octets, et c'est là que les choses peuvent se compliquer.

>>> b'\xc3\xa9'.decode("utf8")
'é'
>>> b'\xc3\xa9'.decode("latin1")
'é'
>>> b'\xe9'.decode("latin1")
'é'
>>> b'\xe9'.decode("utf8")
Traceback (most recent call last):
  File "<pyshell>", line 1, in <module>
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 0: unexpected end of data

Lorsqu'on decode une suite d'octets avec le bon encodage, on retrouve le texte voulu. Par contre, si on utilise un autre encodage, on obtient soit des caractères qui ne sont pas les bons, soit une erreur parce qu'une suite d'octets n'existe pas dans l'encodage choisi. Ainsi, les octets C3 et A9 correspondent à deux symboles différents en latin-1. De même, l'octet E9, correspond au début d'une suite de 3 octets de la forme 1110xxxx 10xxxxxx 10xxxxxx.

Exercice 4

En sachant que \(10000000_2=128_{10}\) et \(10111111_2=191_{10}\), trouver deux nombres n1 et n2 entre 128 et 191 tels que "é"+chr(n1)+chr(n2) donne une chaîne de 3 caractères existants (pas de \xHH) et que ("é"+chr(n1)+chr(n2)).encode("latin1").decode("utf8") donne également un symbole valide.

Un exemple qui ne marche pas
>>> n1 = 130
>>> n2 = 140
>>> "é" + chr(n1) + chr(n2)
'é\x82\x8c'
>>> ("é"+chr(n1)+chr(n2)).encode("latin1").decode("utf8")
'邌'

###(Dés-)Active le code après la ligne # Tests (insensible à la casse)
(Ctrl+I)
Tronquer ou non le feedback dans les terminaux (sortie standard & stacktrace / relancer le code pour appliquer)
Évaluations restantes : /∞

Solution

On peut prendre n1 et n2 entre 161 et 191, à l'exception de 173.

Quelques exemples
>>> n1 = 162
>>> n2 = 165
>>> "é" + chr(n1) + chr(n2)
'颥'
>>> ("é"+chr(n1)+chr(n2)).encode("latin1").decode("utf8")
'颥'
>>> n1 = 171
>>> n2 = 187
>>> "é" + chr(n1) + chr(n2)
'é«»'
>>> ("é"+chr(n1)+chr(n2)).encode("latin1").decode("utf8")
'髻'
>>> n1 = 189
>>> n2 = 188
>>> "é" + chr(n1) + chr(n2)
'é½¼'
>>> ("é"+chr(n1)+chr(n2)).encode("latin1").decode("utf8")
'齼'