python encodage texte

Identification

Infoforall

31 - Encodage des textes


Prérequis : Cette activité s'effectue normalement après avoir effectué celle de la partie Donnée sur l'intérêt de l'hexadécimal. Vous devez donc normalement comprendre le principe de l'ASCII et savoir exprimer un nombre en base 10 (décimal) ou en base 16 (hexadécimal).

Après cette activité, vous comprendrez les blagues de ce style :

Martine, couverture UTF-8
Martine écrit en UTF-8

Logiciel nécessaire pour l'activité : Python 3 : Thonny, IDLE ...

Evaluation ✎ : questions 04-05-06-09-10-11-13-14-18-19-20

Documents de cours : open document ou pdf

1 - ASCII

Nous avons vu que les ordinateurs stockent uniquement des bits associés en octets :

  • un nombre entier est représenté par un ou plusieurs octets
  • un nombre à virgule est représenté par une suite d'octets
  • une image est représentée par une suite d'octets,
  • un texte est représenté par une suite d'octets.

On a donc besoin d'une table de conversion lorsqu'on veut écrire un texte ou qu'on veut le lire . Sans la technique de conversion, impossible de savoir les nombres qu'il faut écrire à la place du texte initial ou comment décoder les nombres qu'on est en train de lire !

La transformation caractère en nombre est ce qu'on nomme l'encodage quelque soit la base utilisée pour exprimer le nombre associé.

A 100 0001  2

A 65 10

A 41 16

La transformation nombre vers caractère est ce qu'on nomme le décodage.

 100 0001  2A

 65 10A

 41 16A

Initialement, la mémoire des ordinateurs n'était pas très grande et les communications n'étaient pas très fiables.

Principe de la table ASCII

La première table normalisée partagée à grande échelle est la table ASCII pour American Standard Code for Information Interchange.

La table ASCII est devenue le standard incontournable des 128 premiers caractères.

Elle est publiée pour la première fois en 1963 et est le résultat d'un groupe de travail.

La table ASCII attribue des valeurs codifiées

  • à 32 caractères dits caractères de contrôle : passage à la ligne, début de fichier, fin de fichier...
  • aux lettres minuscules (a-z)
  • aux lettres majuscules (A-F)
  • aux chiffres décimaux (0-9)
  • aux caractères de ponctuation : . , ; ...
  • à quelques symboles : + - * / ...

Avant cela, chaque matériel avait sa propre table d'encodage/décodage... Changer de support impliquait donc de changer les codes à inscrire !

Taille de l'encodage

La taille choisie pour l'encodage des caractères était de 7 bits. On pouvait donc encoder 27, soit 128 caractères au total. Pas un de plus. Cela représentait une place mémoire non négligeable à l'époque : on était à quelques centaines de kilo-octets (ko) à l'époque. De quoi encoder un petit livre uniquement.

Exemple : un petit bout de la table ASCII

Caractère encodé En binaire (7 bits) En décimal En hexadécimal
A  100 0001  65 41
B  100 0010  66 42
C  100 0011  67 43

Sous cette forme de tableau avec une ligne par caractère, cela va donner 128 lignes. Cette représentation prendrait beaucoup de place.

On préfère représenter la correspondance Code/Caractère via un tableau à deux entrées.

Quelques alias du nom ASCII :

  • iso-ir-6
  • ANSI_X3.4-1968
  • ANSI_X3.4-1986
  • ISO_646.irv:1991
  • ISO646-US
  • US-ASCII
  • us
  • IBM367
  • cp367
  • csASCII

Les premières questions vous permettront de remettre vos connaissances sur les bases 2 et 16 à jour.

01° Montrer que

  • 41 16 correspond bien à 65 10.
  • 0A 16 correspond bien à 10 10.
  • 20 16 correspond bien à 32 10.

Montrer ensuite que  100 0001 2< correspond bien à 65 10.

...CORRECTION...

Calculons la valeur du nombre en base 10.

41 16

= (4 * 16 + 1) 10

= (64 + 1) 10

= 65 10

On peut faire la même chose avec le binaire.

 100 0001  2

= (1 * 64 + 1 * 1) 10

= 65 10

02° En décomposant  0100 0001  en deux quartets, montrer que ce nombre s'écrit bien 41 16.

En utilisant la méthode des quartets, retrouver l'écriture binaire de 0A 16 et 20 16.

...CORRECTION...

 100 0001  peut s'écrire  0100  et  0001 

On voit immédiatement que le premier quartet correspond à 4 et le deuxième à 1.

Bit de parité

Les communications n'étant pas très fiables à l'époque, le 8e bit servait de bit de contrôle lors des communications : un exemple possible à l'émission,

  • si le code du caractère comporte un nombre pair de bits à 1, on met le 8e à 0 : on a au total un nombre pair de bits à 1,
  • si le code du caractère comporte un nombre impair de bits à 1, on met le 8e à 1 : on a au total un nombre pair de bits à 1,

Voici ce qu'on devrait envoyer pour émettre un A, un B ou un C.

Caractère encodé En binaire (7 bits) Nombre de bits à 1
A  0100 0001  2 donc pair
B  0100 0010  2 donc pair
C  1100 0011  4 donc pair

L'ordinateur récepteur pouvait alors faire la même chose sur les 7 bits. En comparant à la valeur du 8e bit, on pouvait savoir si une erreur de transmission avait à priori eu lieu.

Exemples de réception

Je reçois  0111 0010  : il y a un nombre pair de 1, j'accepte cette valeur.

Je reçois  1111 0010  : il y a un nombre impair de 1, je refuse cette valeur.

03° Expliquer si les codes de caractères ci-dessous, reçus par un ordinateur sont possiblement erronés.

 1100 0111 

 1100 1111 

...CORRECTION...

Le premier est invalide : il y 5 bits à 1. Une erreur de transmission est détéctée.

Le deuxième comporte 6 bits à 1. Un nombre pair. Soit la communication est bonne, soit il y a eu plusieurs erreurs en même temps !

Table ASCII (en version hexadécimale)

Voici la table ASCII complète, en version hexadécimale. Vous pouvez visualiser la valeur décimale d'un caractère en stabilisant la souris au dessus du caractère.

_0 _1 _2 _3 _4 _5 _6 _7 _8 _9 _A _B _C _D _E _F
0_ NUL SOH STX ETX EOT ENQ ACK BEL BS HT LF VT FF CR SO SI _0
1_ DLE DC1 DC2 DC3 DC4 NAK SYN ETB CAN EM SUB ESC FS GS RS US 1_
2_ ! " # $ % & ' ( ) * + , - . / 2_
3_ 0 1 2 3 4 5 6 7 8 9 : ; < = > ? 3_
4_ @ A B C D E F G H I J K L M N O 4_
5_ P Q R S T U V W X Y Z [ \ ] ^ _ 5_
6_ ` a b c d e f g h i j k l m n o 6_
7_ p q r s t u v w x y z { | } ~ DEL 7_
_0_1_2_3_4_5_6_7_8_9_A_B_C_D_E_F

✎ 04° Utiliser le tableau pour trouver le nom du caractère qui correspond à la valeur 0A 16 : le nom apparaît au survol de la case.

Avec quelle association de caractères d'échappement parvient-on à le symboliser en Python ?

  •  \t  ?
  •  \n ?
  •  \f ?

2 - Encodage avec Python

Voyons maintenant comment Python parvient à gérer ces histoires d'encodage.

Méthode des strings permettant d'encoder : encode

On peut créer une suite d'octets en Python en utilisant la méthode encode. Cette méthode attend au moins un string contenant le nom de l'encodage à utiliser en argument.

La méthode renvoie un objet de type bytes qui veut dire usuellement octets.

>>> octets = "ABCDE".encode('ascii') >>> type(octets) <class 'bytes'>

Voyons comment récuper les valeurs (0 à 255 en décimal) de deux façons différentes.

Méthode 1 - Utilisation d'une boucle FOR

Remarque : il faut appuyer deux fois sur ENTREE pour lancer la boucle FOR depuis la console.

Lecture directe de la valeur

>>> for octet in octets : print(octet) 65 66 67 68 69

Lecture par utilisation de l'indice

>>> for i in range(len(octets)) : print(octets[i]) 65 66 67 68 69

Méthode 2 - Utilisation de la fonction list

>>> tableau = list(octets) >>> tableau [65, 66, 67, 68, 69]

Affichage des bytes par Python

Python affiche les bytes comme des strings précédés d'un b minuscule.

>>> print(octets) b'ABCDE'

Si la valeur de l'octet correspond à un caractère ASCII imprimable, l'interpréteur va alors afficher le caractère plutôt que la valeur.

✎ 05° Utiliser les instructions ci-dessous dans votre Console. Répondre ensuite aux questions :

>>> texte = "AB\nCD" >>> print(texte) AB CD >>> octets = texte.encode('ascii') >>> octets b'AB\nCD' >>> len(octets) 5 >>> list(octets) [65, 66, 10, 67, 68]

Questions :

  • Que représente \n ?
  • Par quel octet est-il encodé ?
  • Pourquoi le string 'AB\nCD' est-il encodé par 5 octets alors qu'on voit clairement 6 caractères ?

✎ 06° Utiliser Python (ou à la main, mais ça va être long) pour trouver la façon dont on encode la chaîne de caractères suivante qui ne comporte aucun accent :"Bonjour a tous, l'ASCII permet d'encoder beaucoup de caracteres mais pas tous : il s'agit d'un encodage cree par les americains et donc beaucoup de caracteres des langages europeens ou asiatiques ne sont absolument pas presents dans cette table. C'est pour cela que je vous demande de ne pas mettre d'accents dans les noms de variables en Python, meme si nous verrons que le langage Python sait les gerer. Mais ce n'est pas le cas de tous les langages de programmation."

Question supplémentaire : combien d'octets sont nécessaires en mémoire pour stocker ce texte en ASCII ? Pensez à utiliser la fonction native len : elle fonctionne aussi sur les objets bytes.

Question : peut-on tout écrire avec l'ASCII ?

Essayons maintenant de faire la même chose mais avec un texte réel : un texte avec des caractères étranges pour un lecteur américain.

>>> a = "Bonjour à tous, l'ASCII permet d'encoder beaucoup de caractères mais pas tous : il s'agit d'un encodage créé par les américains et donc beaucoup de caractères des langages européens ou asiatiques ne sont absolument pas présents dans cette table. C'est pour cela que je vous demande de ne pas mettre d'accents dans les noms de variables en Python, même si nous verrons que le langage Python sait les gérer. Mais ce n'est pas le cas de tous les langages de programmation."

Aucun problème pour créer le string : Python sait très bien gérer les accents dans les chaînes de caractères.

Passons à la suite : la création des octets-bytes encodant en ASCII ce texte de façon à pouvoir l'enregistrer dans un fichier-texte :

>>> b = a.encode('ascii') UnicodeEncodeError: 'ascii' codec can't encode character '\xe0' in position 8: ordinal not in range(128)

Et voilà : un beau message d'erreur. Visiblement le caractère d'index 8 pose problème.

>>> a[8] 'à'

Pourquoi ce caractère pose-t-il problème ?: simplement car il n'est pas dans la table ASCII. On aura donc du mal à trouver la valeur de l'octet à lui faire correspondre !

Avec la rapide augmentation des mémoires, l'encodage des caractères est passé de 7 bits à 8 bits.

Un bit en plus veut alors dire qu'on double le nombre de caractères disponibles : on est passé

  • de 27 (128, de 0 à 127) valeurs disponibles
  • à 28 (256, de 0 à 255) valeurs disponibles.

Soit 128 nouvelles valeurs permettant d'encoder 128 nouveaux glyphes.

Une révolution : deux fois plus de caractères !

3 - Les tables ASCII étendues (sur un octet)

Les tables cp sont les tables d'encodage "code page" créés par IBM sur les premiers ordinateurs personnels.

Pendant très lontemps, l'une des plus connue a été la page de code 437 cp-437 utilisé par IBM pour le système DOS et ces consoles.

La première partie de ces tables n'est pas reproduite car ce sont les mêmes codes que l'ASCII : le but est d'être compatible avec les caractères 0-127 d'ASCII.

On ne représente donc ici que les 128 caractères rajoutés dont les valeurs sont entre 128 (en hexa 8016) et 255 (en hexa FF16).

_0_1_2_3_4_5_6_7_8_9_A_B_C_D_E_F
0x8_ÇüéâäàåçêëèïîìÄÅ0x8_
0x9_ÉæÆôöòûùÿÖÜ¢£¥ƒ0x9_
0xA_áíóúñѪº¿¬½¼¡«»0xA_
0xB_0xB_
0xC_0xC_
0xD_0xD_
0xE_αßΓπΣσµτΦΘΩδφε0xE_
0xF_±÷°·²¤¤¤0xF_
_0_1_2_3_4_5_6_7_8_9_A_B_C_D_E_F

Vous pourriez vous demander pourquoi ils ont placé autant de caractères graphiques ? Simplement car à l'époque les affichages étaient encore très basiques. Pas d'environnement graphique. Ces caractères permettaient de réaliser de jolis tableaux.

Ces tables ont permis de donner encore plus de créativité aux gens qui réalisaient (et réalisent encore) une forme d'art : l'ASCII-Art.

_________________________________________________ | _ _ ____ ____ ___ ___ | | __ _ _ __| |_ / \ / ___| / ___|_ _|_ _| | | / _` | '__| __| / _ \ \___ \| | | | | | | | | (_| | | | |_ / ___ \ ___) | |___ | | | | | | \__,_|_| \__|/_/ \_\____/ \____|___|___| | |_______________________________________________|
)) \||/ (( | @@___oo)) /\ /\ / (__,,,,| ) /^\) ^\/ _) ) /^\/ _) ) _ / / _) /\ )/\/ || | )_) < > |(,,) )__) || / \)___)\ | \____( )___) )___ \______(_______;;; __;;;
\|/(_)_(_)\|/ @~ (o.o) ~@ /___( * )___\ NA NA / `U' \ NANA ( . ) NA !!! `>---<' _\ /_
o _====| o ====|_====| _====| | ====| I-I-I-I-I _ _ _ _ _ _ _ _ _ _ _ _ ====| | | \ ` ' / I-I-I-I-I-I I-I-I-I-I-I | | | |. | \ ` '_/ \ ` '_/ | / \ | | /^\| |__ [],| [*] __| ^ / ^ \ ^ | [*]| _|______|_ |__ ,| / \ / `\ / \ | ===| <=-=-==-=-=> ___| ___ |__ / /=_=_=_=\ \ |, _| \__ _'_/ I_I__I_I__I_I (====(_________)___) |____| ___ |. _ | \-\--|-|--/-/ | | [*I__| I_I__I____I_I_I | _ | |[] '| | | __ . | \-\--|-|--/-/ |` '| |. ' | __| ___ |__ ___ |__ |---------| | [] | / \ [] .|_| |_| |_| |_| |_| |_| [] [] | | ` | (===) .|-=-=-=-=-=-=-=-=-=-=-| / \ |[] | | []|` [] | . . . . . . . . . |- (===) |` __| | []| ` |/////////\\\\\\\\\\ |__. |[] | /| | <===> ' ||||| |||| | [] <===>-I-I-I-I-I-I-I-I-| ` I-| \I/ 1-- ||||| Maze's |||| | . ' \i/ . | | _| | . _||||| FunHouse |||| | | ----' |` .| ../|',v,.,,,.|||||/_________\||||,/|.,,.Y,,..|\__

Pour pouvoir afficher correctement ces dessins sur d'autres supports, il faut impérativement utiliser des polices de caractères à chasse fixe, c'est à dire que chaque caractère doit avoir exactement la même largeur.

Sinon, l'image est déformée.

Les logiciels permettant d'éditer du code sont habituellement à chasse fixe.

07° Pour voir les encodages disponibles sur votre machine, vous pouvez lancer le code suivant.

1 2 3 4 5 6 7 8 9 10 11 12
import encodings def lister_les_encodages(): """Fonction qui renvoie un tableau contenant les encodages disponibles""" les_encodages = [e for e in sorted(set(encodings.aliases.aliases.values()))] return les_encodages if __name__ == '__main__': print('Liste des encodages disponibles') for element in lister_les_encodages(): print(f'- {element}')

Ce code utilise set, un ensemble. C'est une structure différente de list. Notamment, un ensemble est conçu pour ne pas contenir plusieurs fois des éléments identiques. Vous pouvez lancer le code en remplaçant set par list : vous allez voir qu'on obtient alors plusieurs fois les mêmes noms.

Vous devriez en trouver un beau paquet. Nous allons voir pourquoi il y en a autant...

A titre d'exemple, voici la liste obtenue avec sur mon système (en blanc : ceux dont nous allons parler aujourd'hui) :

Liste des encodages disponibles - ascii - base64_codec - big5 - big5hkscs - bz2_codec - cp037 - cp1026 - cp1125 - cp1140 - cp1250 - cp1251 - cp1252 - cp1253 - cp1254 - cp1255 - cp1256 - cp1257 - cp1258 - cp273 - cp424 - cp437 - cp500 - cp775 - cp850 - cp852 - cp855 - cp857 - cp858 - cp860 - cp861 - cp862 - cp863 - cp864 - cp865 - cp866 - cp869 - cp932 - cp949 - cp950 - euc_jis_2004 - euc_jisx0213 - euc_jp - euc_kr - gb18030 - gb2312 - gbk - hex_codec - hp_roman8 - hz - iso2022_jp - iso2022_jp_1 - iso2022_jp_2 - iso2022_jp_2004 - iso2022_jp_3 - iso2022_jp_ext - iso2022_kr - iso8859_10 - iso8859_11 - iso8859_13 - iso8859_14 - iso8859_15 - iso8859_16 - iso8859_2 - iso8859_3 - iso8859_4 - iso8859_5 - iso8859_6 - iso8859_7 - iso8859_8 - iso8859_9 - johab - koi8_r - kz1048 - latin_1 - mac_cyrillic - mac_greek - mac_iceland - mac_latin2 - mac_roman - mac_turkish - mbcs - ptcp154 - quopri_codec - rot_13 - shift_jis - shift_jis_2004 - shift_jisx0213 - tactis - tis_620 - utf_16 - utf_16_be - utf_16_le - utf_32 - utf_32_be - utf_32_le - utf_7 - utf_8 - uu_codec - zlib_codec

08° Lancer maintenant le code suivant qui vous donnera la façon d'encoder les caractères accentués en utilisant la table cp437.

1 2 3 4 5 6 7 8 9 10 11 12 13
texte = "Test avec é, des à et des è" octets = texte.encode('cp437') print('\nLe string de base') print(texte) print("\nLa suite d'octets") print(octets) print("\nLa suite d'octets sous forme de liste") for octet in octets: print(octet, end=' ')

La présence de  \x  veut dire qu'il s'agit d'un code hexadécimal d'un caractère non-ASCII : les valeurs sont supérieures ou égales à 128 en base 10 ou 80 en base 16.

Questions

  • En comparant les trois affichages, trouver l'encodage en hexadécimal et décimal des é avec cp437.
  • Idem pour le à
  • Idem pour le è

...CORRECTION...

Si votre code ne parvient pas à se lancer pour une raison ou une autre, voici le résultat de l'affichage :

Le string de base Test avec é, des à et des è La suite d'octets b'Test avec \x82, des \x85 et des \x8a' La suite d'octets sous forme de liste 84 101 115 116 32 97 118 101 99 32 130 44 32 100 101 115 32 133 32 101 116 32 100 101 115 32 138

Voici la première ligne de la table cp437 avec les caractères voulus en rouge : on voit bien qu'avec cette table é est encodé par 8216 que Python écrit 0x82.

_0_1_2_3_4_5_6_7_8_9_A_B_C_D_E_F
0x8_ÇüéâäàåçêëèïîìÄÅ0x8_

La façon étrange d'afficher les objets bytes a une raison d'être : on voit rapidement les caractères qui ne sont pas des caractères ASCII (les caractères de 0 à 127).

La suite d'octets b'Test avec \x82, des \x85 et des \x8a'

Si on résume dans une table :

Caractère encodé En binaire (8 bits) En décimal En hexadécimal
é  1000 0010  130 82
à  1000 0101  133 85
é  1000 1010  138 8a

Pendant longtemps, il y a donc eu une multitude de tables :

  • des tables pour ceux qui voulaient plutôt des caractères graphiques,
  • des tables pour les pays d'Europe de l'Ouest,
  • des tables pour les pays d'Europe du Nord,
  • des tables pour le cyrillique,
  • des tables pour les caractères asiatiques spécifiques,
  • et même des tables par constructeurs
  • ou par systèmes d'exploitation !

Bref, il n'était pas toujours facile de parvenir à décoder correctement un texte si on ne connaissait pas l'encodage qui avait été utilisé.

Table latin-1 ou iso8859_1

L'une des autres tables très utilisée en Europe est la table iso8859_1, de son petit nom latin-1.

Elle comporte tous les caractères usuels pour écrire un texte dans l'une des langues de l'Europe de l'Ouest et reste entièrement compatible avec l'ASCII sur les caractères de valeurs inférieures à 128.

Je ne fournis ici que la moitié supérieure de la table, la partie inférieure (0-127) est entièrement composée des caractères ASCII.

_0_1_2_3_4_5_6_7_8_9_A_B_C_D_E_F
0x8_ PAD HOP BPH NBH IND NEL SSA ESA HTS HTJ LTSPLD PLU RI SS2SS30x8_
0x9_ DCS PU1 PU2 STS CCH MW SPA EPA SOS SGCI SCI CSI ST OSC PM APC0x9_
0xA_NBSP ¡¢£¤¥¦§¨©ª«¬SHY®¯0xA_
0xB_°±²³´µ·¸¹º»¼½¾¿0xB_
0xC_ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏ0xC_
0xD_ÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞß0xD_
0xE_àáâãäåæçèéêëìíîï0xE_
0xF_ðñòóôõö÷øùúûüýþÿ0xF_
_0_1_2_3_4_5_6_7_8_9_A_B_C_D_E_F

Remarquez bien qu'elle n'inclut pas de caractères graphiques : les applications graphiques sont apparues. Plus besoin de réaliser de tableaux ou de dessins sur la console en utilisant des caractères, on utilisait déjà à cette époque de vraies images par exemple.

Voici les alias courants de cette table :

  • ISO_8859-1:1987
  • ISO_8859-1
  • ISO-8859-1
  • iso-ir-100
  • csISOLatin1
  • latin1
  • l1
  • IBM819
  • CP819

✎ 09° Trois questions.

  • 1 - En vous inspirant des codes précédents, fournir les octets permettant d'encoder ce texte en latin-1 puis en cp437.
  • Le texte : "Perceval ne sait vraiment pas écrire en latin !".

  • 2 - Quel est le seul caractère dont l'encodage est différent sur les deux tables ici ?
  • 3 - Peut-on décoder à coup sur des octets (bytes) si on ne connait pas la table qui a servi à les créer ?

Je n'ai présenté ici qu'une toute petite partie des tables d'encodage qui étaient (et sont encore) disponibles. Plus d'exemple dans la fiche présentée en fin d'activité.

Sachez néanmoins que la table latin-1 a connu une mise à jour importante pour l'Europe : la table latin-9 (également nommée iso8859_15) inclut en effet un caractère qui n'existait pas au moment de la création de la première : le signe de l'Euro, la monnaie.

Latin-9, une sorte de mise à jour de latin-1

En rose, les quelques associations valeurs-caractères qui ont changé par rapport à latin-1.

_0_1_2_3_4_5_6_7_8_9_A_B_C_D_E_F
0x8_¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤ 0x8_
0x9_¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤ 0x9_
0xA_¤¤¤¡¢£¥Š§š©ª«¬¤¤¤®¯0xA_
0xB_°±²³Žµ·ž¹º»ŒœŸ¿0xB_
0xC_ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏ0xC_
0xD_ÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞß0xD_
0xE_àáâãäåæçèéêëìíîï0xE_
0xF_ðñòóôõö÷øùúûüýþÿ0xF_
_0_1_2_3_4_5_6_7_8_9_A_B_C_D_E_F

Un dernier exemple pour la route, l'iso-8859-11 ou Thai. Les 128 premières valeurs sont celles de l'ASCII, c'est à partir de 128 que ça diffère. Il y a même 8 valeurs non utilisées.

...iso-8859-11...

_0_1_2_3_4_5_6_7_8_9_A_B_C_D_E_F
0x8_¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤0x8_
0x9_¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤0x9_
0xA_¤¤¤0xA_
0xB_0xB_
0xC_0xC_
0xD_฿0xD_
0xE_0xE_
0xF_0xF_
_0_1_2_3_4_5_6_7_8_9_A_B_C_D_E_F
Tables d'encodage sur un octet

Il existe une multitude de tables d'encodages sur un octet permettant d'écrire dans toutes les langues.

Pourquoi "encodage sur un octet" ? Tout simplement car toutes ces tables contiennent 256 associations valeurs-caractères qui permettent d'encoder un texte en prenant exactement 1 octet par caractère.

La table la plus utilisée en Europe a été latin-1, remplacée par latin-9 pour introduire notamment l'euro.

Exemples

  • l'espace est un caractère qui possède une valeur (32 10 ou 20 16) et prend donc un octet en mémoire à ces tables.
  • le A est un caractère qui possède une valeur (65 10 ou 41 16) et prend donc un octet en mémoire à ces tables.
  • le passage à la ligne (ou LF pour Line Feed) est un caractère qui possède une valeur (10 10 ou 0A 16) et prend donc un octet en mémoire à ces tables. On le représente parfois sous la forme d'une flèche ou plus généralemente par \n

Remarque sur le protocole HTTP : de façon à ne pas créer des problèmes de compréhension sur les requêtes HTTP et les répondes HTTP, on transmet uniquement des caractères ASCII. Cela évite les problèmes de décodage erroné. Exemple ici avec une requête GET envoyé par Firefox à un serveur :

GET /act/archi/communication-client-serveur/ HTTP/1.1
Host: www.infoforall.fr

User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:76.0) Gecko/20100101 Firefox/76.0

On voit bien que ce message ne contient que de l'ASCII. De cete façon, quelque soit la table 1 octet utilisée, il n'y a pas possibilité de se tromper puisqu'elles sont toutes compatibles ASCII sur leur première partie.

✎ 10° Estimer le nombre nécessaire d'octets pour encoder ce texte en latin-1. Pensez bien à tous les caractères, même les caractères "cachés".

Bon, alors combien d'octets pour stocker ça ?

Ce nombre change-t-il si on utilise une autre table "1 octet" ?

Avec ce principe des tables "un octet" de 256 caractères, on peut donc écrire

  • un texte en anglais,
  • un texte en français (en prenant une table contenant les caractères voulus),
  • un texte en chinois (en prenant une table contenant les bons caractères),
  • un texte en russe (en prenant une table contenant les bons caractères)
  • ...

Deux problèmes néanmoins qui vont justifier l'invention d'UNICODE :

  1. Si on ne connait pas la table utiliser pour encoder, difficile de parvenir à décoder vu le nombre de tables disponibles...
  2. Si on veut écrire un texte dans plusieurs langues sur un même support (comme un site Web), ben, c'est pas possible...

4 - Décodage avec Python

Nous avons vu comment encoder avec la méthode encode. Il est temps de parvenir à savoir décoder avec la méthode ... decode.

Méthode des bytes : decode

On peut créer un string en utilisant sur un objet bytes la méthode decode. Cette méthode attend au moins un paramètre de type string contenant le nom de la table d'encodage à utiliser.

La méthode renvoie un objet de type str.

Un exemple avec un texte qu'on avait encodé en latin9, également nommé iso8859_15 sur ma machine. Voir la question 07 pour voir le nom qu'elle peut porter sur la votre.

>>> texte = b"Cet article co\xFBte\x2020\xA3.".decode('latin9') >>> texte 'Cet article coûte 20£.'
>>> texte = b"Cet article co\xFBte\x2020\xA3.".decode('iso8859_15') >>> texte 'Cet article coûte 20£.'

Pourquoi un tel résultat ? Il suffit d'aller voir la valeur des caractères en hexadécimal pour ceux précédé de \x.

  • \xFB pour û.
  • \x20 pour   (espace).
  • \xA3 pour £.

✎ 11° Que va contenir texte dans l'exemple ci-dessous ?

Attention, il y a une différence par rapport à l'exemple du dessus.

>>> texte = b"Cet article co\xFBte\x2020\xA4.".decode('iso8859_15') >>> texte ? votre réponse ?

Là où c'était vraiment pénible, c'était donc lorsqu'on ne connaissait pas l'encode d'un texte. On devait faire au hasard, en utilisant l'encodage le plus probable.

Voici ainsi un exemple où on dispose de la suite d'octets 160, 161, 162, 164... qu'on décode avec différentes tables :

Création d'un contenu bytes (octets)

>>> t = [160, 161, 162, 163, 164, 165] >>> octets = bytes(t) >>> octets b'\xa0\xa1\xa2\xa3\xa4\xa5'

Décodage de ces octets en utilisant différentes tables...

>>> print(octets.decode('latin-1'))  ¡¢£¤¥ >>> print(octets.decode('latin9'))  ¡¢£€¥ >>> print(octets.decode('thai'))  กขฃคฅ >>> print(octets.decode('iso8859_10'))  ĄĒĢĪĨ >>> print(octets.decode('iso8859_11'))  กขฃคฅ >>> print(octets.decode('iso8859_13'))  ”¢£¤„ >>> print(octets.decode('iso8859_14'))  Ḃḃ£Ċċ >>> print(octets.decode('iso8859_15'))  ¡¢£€¥ >>> print(octets.decode('iso8859_16'))  ĄąŁ€„

Vous devriez voir que pour un même contenu en octets, le texte obtenu est bien différents selon le choix de la table utilisée pour décoder. Il fallait donc trouver des moyens clairs et précis d'indiquer la table qui avait été utilisée pour encoder le texte. Malgré ça, le risque d'erreur était encore très grand.

L'apparition de la communication globale via Internet, a amplifié les risques d'erreur de decodage : on pouvait créer un fichier-texte encodé en latin-3. Ensuite, ce fichier pouvait être lu en Turquie par quelqu'un ayant un encodage de type mac-turkish. Bref, les risques d'avoir les effets suivants devenaient non négligeables :

  • des affichages bizarres (au mieux),
  • des programmes qui plantent ou
  • des bases de données anéanties (le pire qui soit)

D'où l'invention d'un nouveau système plus flexible que des tables de 256 caractères : UNICODE.

5 - UNICODE

UNICODE
Logo UNICODE
Logo UNICODE - Image dans le domaine public

Principe

UNICODE n'est pas une table d'encodage à proprement parler : UNICODE n'impose rien au sujet de la façon de représenter le nombre UNICODE en mémoire.

Il s'agit juste d'une immense table associant un numéro à un glyphe (un caractère).

Exemples

A est associé à 6510 (compatible ASCII)

a est associé à 9710 (compatible ASCII)

ᎈ est associé à 50010

⟘ est associé à 1020010

😀 est associé à 12851210

Contenu

UNICODE regroupe tous les caractères et assimilés qu'utilise l'humanité, ou presque : les caractères des langues connues, les caractères scientifiques, des éléments graphiques mais également des icones comme les emote-icons.

Au moment où j'ai écrit cette partie, nous en étions à Unicode 13.0, version qui contient 143 859 caractères différents.

Les caractères UNICODE sont classés en bloc thématique contenant 65536 caractères. Il existe donc beaucoup de valeurs non utilisées puisque les blocs ne sont pas totalement plein loin de là : les 143 859 glyphes de la version 13 sont associés à des numéros compris entre 0 et 16 842 751.

Le but est de garantir une durée de vie énorme au système en pouvant rajouter un nombre très important de valeurs.

Plus de renseignements et une visualisation de tous les caractères, sur le site d'UNICODE.

La page correspondante de Wikipedia est très bien réalisée également WIKIPEDIA.

Voici quelques minces extraits des caractères présents dans UNICODE. Pour visualiser le numéro en décimal, il suffit de rester stationnaire au dessus du caractère.

...0 à 255 : identique aux valeurs latin-1...

Numéros 0-255
_0_1_2_3_4_5_6_7_8_9_A_B_C_D_E_F
0x0_¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤
0x1_¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤
0x2_ !"#$%&'()*+,-./
0x3_0123456789:;<=>?
0x4_@ABCDEFGHIJKLMNO
0x5_PQRSTUVWXYZ[\]^_
0x6_`abcdefghijklmno
0x7_pqrstuvwxyz{|}~¤¤¤
0x8_¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤
0x9_¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤
0xA_¤¤¤¡¢£¤¥¦§¨©ª«¬¤¤¤®¯
0xB_°±²³´µ·¸¹º»¼½¾¿
0xC_ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏ
0xD_ÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞß
0xE_àáâãäåæçèéêëìíîï
0xF_ðñòóôõö÷øùúûüýþÿ

...Numéros 256-511...

Numéros 256-511
_0_1_2_3_4_5_6_7_8_9_A_B_C_D_E_F
0x10_ĀāĂ㥹ĆćĈĉĊċČčĎď
0x11_ĐđĒēĔĕĖėĘęĚěĜĝĞğ
0x12_ĠġĢģĤĥĦħĨĩĪīĬĭĮį
0x13_İıIJijĴĵĶķĸĹĺĻļĽľĿ
0x14_ŀŁłŃńŅņŇňʼnŊŋŌōŎŏ
0x15_ŐőŒœŔŕŖŗŘřŚśŜŝŞş
0x16_ŠšŢţŤťŦŧŨũŪūŬŭŮů
0x17_ŰűŲųŴŵŶŷŸŹźŻżŽžſ
0x18_ƀƁƂƃƄƅƆƇƈƉƊƋƌƍƎƏ
0x19_ƐƑƒƓƔƕƖƗƘƙƚƛƜƝƞƟ
0x1A_ƠơƢƣƤƥƦƧƨƩƪƫƬƭƮƯ
0x1B_ưƱƲƳƴƵƶƷƸƹƺƻƼƽƾƿ
0x1C_ǀǁǂǃDŽDždžLJLjljNJNjnjǍǎǏ
0x1D_ǐǑǒǓǔǕǖǗǘǙǚǛǜǝǞǟ
0x1E_ǠǡǢǣǤǥǦǧǨǩǪǫǬǭǮǯ
0x1F_ǰDZDzdzǴǵǶǷǸǹǺǻǼǽǾǿ

...Numéros 512-767...

Numéros 512-767
_0_1_2_3_4_5_6_7_8_9_A_B_C_D_E_F
0x20_ȀȁȂȃȄȅȆȇȈȉȊȋȌȍȎȏ
0x21_ȐȑȒȓȔȕȖȗȘșȚțȜȝȞȟ
0x22_ȠȡȢȣȤȥȦȧȨȩȪȫȬȭȮȯ
0x23_ȰȱȲȳȴȵȶȷȸȹȺȻȼȽȾȿ
0x24_ɀɁɂɃɄɅɆɇɈɉɊɋɌɍɎɏ
0x25_ɐɑɒɓɔɕɖɗɘəɚɛɜɝɞɟ
0x26_ɠɡɢɣɤɥɦɧɨɩɪɫɬɭɮɯ
0x27_ɰɱɲɳɴɵɶɷɸɹɺɻɼɽɾɿ
0x28_ʀʁʂʃʄʅʆʇʈʉʊʋʌʍʎʏ
0x29_ʐʑʒʓʔʕʖʗʘʙʚʛʜʝʞʟ
0x2A_ʠʡʢʣʤʥʦʧʨʩʪʫʬʭʮʯ
0x2B_ʰʱʲʳʴʵʶʷʸʹʺʻʼʽʾʿ
0x2C_ˀˁ˂˃˄˅ˆˇˈˉˊˋˌˍˎˏ
0x2D_ːˑ˒˓˔˕˖˗˘˙˚˛˜˝˞˟
0x2E_ˠˡˢˣˤ˥˦˧˨˩˪˫ˬ˭ˮ˯
0x2F_˰˱˲˳˴˵˶˷˸˹˺˻˼˽˾˿

...Numéros 8704-8959...

Numéros 8704-8959
_0_1_2_3_4_5_6_7_8_9_A_B_C_D_E_F
0x220_
0x221_
0x222_
0x223_
0x224_
0x225_
0x226_
0x227_
0x228_
0x229_
0x22A_
0x22B_
0x22C_
0x22D_
0x22E_
0x22F_

...Numéros 63744-63999...

Numéros 63744-63999
_0_1_2_3_4_5_6_7_8_9_A_B_C_D_E_F
0xF90_
0xF91_
0xF92_
0xF93_錄
0xF94_
0xF95_
0xF96_
0xF97_勵
0xF98_
0xF99_
0xF9A_
0xF9B_樂
0xF9C_
0xF9D_
0xF9E_
0xF9F_刺

Et on encode comment ?

L'encodage en octets n'est pas géré par UNICODE : c'est une simple correspondance numéro - caractère. Et c'est sa force : même si on change la façon dont les numéros sont mémorisés sous forme d'octets, on gardera la possibilité de lire les anciens contenus.

Actuellement, les encodages nommés UTF-8, UTF-16, UTF-32 se chargent de l'aspect transformation en octets justement.

Retro-compatibilité

Pour maintenir une retro-compatibilité, les valeurs UNICODE des valeurs 0 à 127 correspondent exactement aux valeurs d'encodage de la table ASCII.

Pour maintenir une certaine retro-compatibilité, les valeurs UNICODE des valeurs 128 à 255 correspondent exactement aux valeurs d'encodage de la table latin-1.

Les caractères présents dans les autres tables sont associés à un numéro supérieur à 255.

Justement, Python utilise directement à l'interne ce code pour gérer ces caractères. C'est pour cela que vous pourriez utiliser des noms de variables ou des fonctions en utlisant les caractères de n'importe quelle langue.

UNICODE et PYTHON : fonctions natives ord et chr

La fonction native chr renvoie le caractère UNICODE correspondant au nombre décimal décimal fourni en argument.

>>> chr(65) 'A' >>> chr(10000) '✐' >>> chr(20000) '丠'

La fonction native ord fait l'inverse : elle renvoie la valeur UNICODE correspondant au caractère fourni en argument.

>>> ord('A') 65 >>> ord('✐') 10000 >>> ord('丠') 20000

Et en hexadécimal ?

C'est pareil, il suffit de dire à Python que vous fournissez un nombre en base 16 :

>>> hex(65) '0x41' >>> chr(0x41) 'A'
UNICODE et HTML

On peut afficher très facilement de l'UNICODE dans HTML.

Il suffit de taper &#xFFFFF;FFFFF correspond au numéro en hexadécimal que vous aurez trouvé sur une page décrivant ces caractères.

On obtient 🧙 en tapant &#x1F9D9;.

On obtient 🧚 en tapant &#x1F9DA;.

On obtient 🐲 en tapant &#x1F432;.

Si vous voulez fournir le numéro en décimal, c'est pareil, mais on ne met pas le x qui veut dire hexadécimal :

On obtient 🧙 en tapant &#129497;.

Pourquoi ? Simplement car 1F9D616 = 12949710.

Si vous voulez en voir plus, Wiipédia a des pages assez claires et bien faites sur le sujet. exemple

12° Créer un programme qui permet d'afficher les caractères UNICODE de l'invervalle [1000;1100].

...CORRECTION...

J'espère que vous avez trouvé tout seul...

Une correction pour la forme.

1 2
for valeur in range(1000,1101): print(chr(valeur))

Et le résultat attendu :

Ϩ ϩ Ϫ ϫ Ϭ ϭ Ϯ ϯ ϰ ϱ ϲ ϳ ϴ ϵ ϶ Ϸ ϸ Ϲ Ϻ ϻ ϼ Ͻ Ͼ Ͽ Ѐ Ё Ђ Ѓ Є Ѕ І Ї Ј Љ Њ Ћ Ќ Ѝ Ў Џ А Б В Г Д Е Ж З И Й К Л М Н О П Р С Т У Ф Х Ц Ч Ш Щ Ъ Ы Ь Э Ю Я а б в г д е ж з и й к л м н о п р с т у ф х ц ч ш щ ъ ы ь

Mais on aurait pu demander de 9800 à 9900 :

♈ ♉ ♊ ♋ ♌ ♍ ♎ ♏ ♐ ♑ ♒ ♓ ♔ ♕ ♖ ♗ ♘ ♙ ♚ ♛ ♜ ♝ ♞ ♟ ♠ ♡ ♢ ♣ ♤ ♥ ♦ ♧ ♨ ♩ ♪ ♫ ♬ ♭ ♮ ♯ ♰ ♱ ♲ ♳ ♴ ♵ ♶ ♷ ♸ ♹ ♺ ♻ ♼ ♽ ♾ ♿ ⚀ ⚁ ⚂ ⚃ ⚄ ⚅ ⚆ ⚇ ⚈ ⚉ ⚊ ⚋ ⚌ ⚍ ⚎ ⚏ ⚐ ⚑ ⚒ ⚓ ⚔ ⚕ ⚖ ⚗ ⚘ ⚙ ⚚ ⚛ ⚜ ⚝ ⚞ ⚟ ⚠ ⚡ ⚢ ⚣ ⚤ ⚥ ⚦ ⚧ ⚨ ⚩ ⚪ ⚫

Attention par contre, selon les supports, les glyphes colorés sont parfois indisponibles.

C'est bien gentil tout ça mais ça n'explique pas concrétement comment encoder les caractères.

✎ 13° Fournir le calcul à faire pour montrer qu'avec 3 octets (24 bits donc), on ne peut pas encoder la valeur extrème actuelle d'UNICODE 16 842 751 10.

✎ 14° Fournir le calcul à faire pour montrer qu'avec 4 octets (32 bits donc), on peut encoder plus de 4 milliards de caractères.

En nous arrivons au problème majeur de l'UNICODE : comment parvenir à encoder correctement tous ces caractères.

Imaginons qu'on décide d'utiliser 4 octets pour chaque caractère. Cela permet d'aller jusqu'à 4 milliards de caractères, nous avons donc plus de marge qu'avec les tables d'un octet...

Cet encodage brut se nomme UTF-32.

D'où vient ce nom ? De Universal Character Set Transformation Format 32 bits. Ca tombe bien 32 bits, ça donne 4 octets.

Le problème d'UTF-32 vient de la taille des fichiers : ils seraient globalement 4 fois plus lourd à stocker qu'avec les encodages 1 octet, puisqu'il faudrait 4 octets par caractère.

15° Expliquer en quelques mots pourquoi on obtient bien à peu près quatre fois plus en utilisant l'encodage UTF-32 plutôt que l'encodade latin-1.

1 2 3 4 5 6 7
texte = "Le texte tout basique à encoder" octets_v1 = texte.encode('latin-1') octets_v2 = texte.encode('utf-32') print(len(octets_v1)) print(len(octets_v2))
31 128
Pourquoi y-a-t-il quelques octets en plus ?

Il s'agit d'octets de valeurs connus qui permettent de reconnaitre qu'il s'agit d'un texte encodé en UTF. On leur donne le nom de BOM.

Le BOM est composé de 2 octets en UTF-16 et de 4 octets en UTF-32.

Si on reprend l'exemple de la question précédente : nous avons 31 caractères à encoder en UTF-32 dont 31 * 4 = 124. Si on rajoute les 4 octets du BOM, on retombe bien sur 128.

Voir la FAQ en bas de page et en fin de séance si ça vous intéresse.

Un fichier-texte de 100 000 caractères encodé en latin-1 prendrait 100 ko de place mémoire sans compression.

Le même fichier encodé avec UTF-32 occuperait 400 ko (+ 4 petits octets de BOM)

Et c'est qu'intervient le fameux UTF-8.

6 - UTF-8

UTF-8

Idée générale

L'idée générale derrière UTF-8 est de pouvoir encoder les numéros UNICODE en prenant moins que 4 octets par caractère dans la majorité des cas.

  • Les caractères courantes (ASCII) sont encodés en utilisant uniquement 1 octet.
  • >>> list("A".encode('utf-8')) [65]
  • Les caractères assez courants sont encodés en utilisant 2 octets
  • >>> list("é".encode('utf-8')) [195, 169]
  • Les caractères rares sont encodés en utilisant 3 octets
  • >>> list("✎".encode('utf-8')) [226, 156, 142]
  • Les derniers caractères en utilisant 4 octets s'il le faut vraiment

UTF-8 veut dire donc que certains caractères prennent 8 bits, soit 1 octet. Pour les autres, c'est plus.

Ce sont les caractères courants dans la plupart des langages occidentaux (ceux de la table ASCII donc) qui sont encodés sur 1 octet. D'où la présence d'autres encodages. UTF-8 est un standard, mais pas LE standard universel.

Perceval et l'UTF-8
Mauvais décodage !

UTF-8 : le standard actuel

Il est aujourd'hui l'encodage par défaut des systèmes UNIX compatibles (Linux, MacOS) et plus de 95% des pages Web sont encodées en UTF-8.

Nous n'allons pas rentrer dans le détail de la norme ici.

16° Quelle place mémoire prendrait le texte suivant en étant encodé en ASCII (via latin-1), en UTF-8 puis en UTF-32 ?

>>> texte = "ABCDE"

...CORRECTION...

Il ne s'agit que de caractères ASCII.

Il y a 5 caractères.

En ASCII (via latin-1) : 5 octets.

En UTF-8 : 5 octets.

En UTF-32 : 20 octets + 4 octets de BOM : 24 octets.

17° En UTF-8, quelle place mémoire occupe le caractère 'à' en UTF-8 ? Et pour '♥' ?

>>> octets = 'à'.encode('utf8') >>> octets b'\xc3\xa0' >>> list("à".encode('utf-8')) [195, 160]
>>> octets = '♥'.encode('utf8') >>> octets b'\xe2\x99\xa5' >>> list("♥".encode('utf-8')) [226, 153, 165]

...CORRECTION...

2 pour le à : il n'appartient pas à ASCII mais reste assez courant.

3 pour le coeur : il est d'usage rare.

Il nous reste à avoir une explication sur l'image d'introduction. Vous avez maintenant toutes les cartes en main pour comprendre pourquoi le é s'affiche mal sur les images suivantes :

Martine, couverture UTF-8 Perceval et l'UTF-8
Martine écrit en UTF-8, et Perceval tente de comprendre...

✎ 18° Expliquer le plus clairement possible pourquoi le "é" encodé en UTF-8 se transforme en deux caractères "é" lorsqu'on décode les octets avec latin-1.

>>> octets = "é".encode('utf-8') >>> octets b'\xc3\xa9' >>> texte = octets.decode('latin-1') >>> texte 'é'

✎ 19° Combien d'octets faut-il pour encoder le texte suivant en UTF-8 : "L'encodage UTF-8 a été publié officiellement en 1996, 5 ans après la création du Web.".

Comparer avec un encodage en latin-1 (ou ISO-8859-1).

Les QCM pour savoir ce qu'il faut retenir pour la fin.

✎ 20° Répondre aux questions suivantes :

Quel a été le premier encodage standardisé des caractères ?

  • A : ASCII
  • B : cp-437
  • C : latin-1 ou iso8859_1
  • D : UTF-8

Combien d'associations valeurs-caractères peuvent encoder au maximum les tables du type latin-1 (ou ISO-8859-1) ?

  • A : 127
  • B : 128
  • C : 255
  • D : 256

UNICODE est

  • A : un encodage qui remplace maintenant latin-1 (ou ISO-8859-1)
  • B : une simple association d'un entier et d'un caractère
  • C : un encodage sur 1 octet
  • D : un encodage sur 4 octets

UTF-8 est

  • A : une autre façon d'encoder les tables 1 octet (donc 8 bits)
  • B : un encodage basé sur UNICODE dans lequel les caractères prennent tous 8 octets
  • C : la 8e version de l'encodage UTF, incluant le symbole monétaire EURO
  • D : un encodage basé sur UNICODE dans lequel les caractères prennent de 1 à 4 octets

7 - Bilan

Que devez-vous savoir au final :

  1. Comment est représenter d’un texte en machine de façon générale ?
  2. Qu'est-ce que l'encodage ASCII ?,
  3. Qu'est-ce que l'encodage ISO-8859-1 ?,
  4. Qu'est-ce que le système UNICODE ?,
  5. Qu'est-ce que l'encodage UTF-8 ?,
  6. Comment trouver la valeur unicode d'un caractère en Python ?,
  7. Comment trouver afficher un caractère à l'aide de sa valeur UNICODE en Python ?,
  8. Comment trouver afficher un caractère à l'aide de sa valeur UNICODE en HTML (hexa ou décimal) ?,
  9. Comment encoder sous forme d'octets (bytes) un texte UNICODE en Python en respectant un encodage précis ?,
  10. Comment décoder des octets (bytes) en texte connaissant l'encodage utilisé en Python ?,

Si vous avez encore un peu de temps, voici quelques questions bonus : elles vous permettront de comprendre un peu comment fonctionne UTF-8. Mais attention : rien d'exigible pour la NSI.

Bonus 1° Voici un exemple de code permettant de trouver l'encodage UTF-8 d'une chaîne de 5 caractères purement ASCII ici. Remarquez bien que tous les bits commencent à 0 : c'est comme cela que le système sait qu'il s'agit de caractère encodé sur un octet uniquement.

1 2 3 4 5 6 7
texte = "ABCDE" octets = texte.encode('utf-8') suite_en_decimal = list(octets) suite_en_binaire = [bin(octet)[2:].rjust(8,'0') for octet in octets] print(suite_en_decimal) print(suite_en_binaire)

Vous devriez trouver

[65, 66, 67, 68, 69] ['01000001', '01000010', '01000011', '01000100', '01000101']

On obtient bien 5 octets.

Bonus 2° Même chose mais avec une chaîne de 11 caractères dont un caractère non ASCII : le é.

1 2 3 4 5 6 7
texte = "C'est écrit" octets = texte.encode('utf-8') suite_en_decimal = list(octets) suite_en_binaire = [bin(octet)[2:].rjust(8,'0') for octet in octets] print(suite_en_decimal) print(suite_en_binaire)

Voici le résultat que vous devriez obtenir :

[67, 39, 101, 115, 116, 32, 195, 169, 99, 114, 105, 116] ['01000011', '00100111', '01100101', '01110011', '01110100', '00100000', '11000011', '10101001', '01100011', '01110010', '01101001', '01110100']

Le é est donc encodé avec ces deux octets :

'11000011', '10101001'

Questions

  1. Combien d'octets pour encoder le é ?
  2. Combien de 1 successifs au début du premier octet encodant le é ?
  3. Dans UTF, les octets supplémentaires possèdent une sorte de détection d'erreur : tous les octets supplémentaires commencent par la séquence 10 : pourquoi peut-on dire que le second octet encodant le é est à priori valide ?

...CORRECTION...

Le é nécessite 2 octets.

Et justement, le premier octet commence par une séquence de 2 bit à 1 : c'est comme cela que le système sait que ce caractère nécessite la lecture de deux octets en tout.

Le second octet est valide puisqu'il commence bien par une séquence 10.

Bonus 3° Même chose mais avec une chaîne comprtant quelques caractères encore plus rare.

1 2 3 4 5 6 7
texte = "Voici un ♥" octets = texte.encode('utf-8') suite_en_decimal = list(octets) suite_en_binaire = [bin(octet)[2:].rjust(8,'0') for octet in octets] print(suite_en_decimal) print(suite_en_binaire)

Voici le résultat que vous devriez obtenir :

[86, 111, 105, 99, 105, 32, 117, 110, 32, 226, 153, 165] ['01010110', '01101111', '01101001', '01100011', '01101001', '00100000', '01110101', '01101110', '00100000', '11100010', '10011001', '10100101']

Le ♥ est donc encodé avec ces trois octets :

'11100010', '10011001', '10100101']

Questions

  1. Combien d'octets pour encoder le ♥ ?
  2. Combien de 1 successifs au début du premier octet encodant le ♥ ?
  3. Dans UTF, les octets supplémentaires possèdent une sorte de détection d'erreur : tous les octets supplémentaires commencent par la séquence 10 : pourquoi peut-on dire que les octets suivants sont à priori valides ?

...CORRECTION...

Le ♥ nécessite 3 octets.

Et justement, le premier octet commence par une séquence de 3 bit à 1 : c'est comme cela que le système sait que ce caractère nécessite la lecture de trois octets en tout.

Les octets suivants sont valides puisqu'ils commencent bien par une séquence 10.

8 - FAQ

Et UTF-16, c'est quoi cet encodage ?

L'encodage UTF-16 est utilisé notamment dans les pays asiatiques. Tous les caractères sont encodés sur 2 octets, 3 ou 4 octets. Il permet de placer beaucoup plus de caractères sur la zone de deux octets.

Cette encodage est donc plus performant que l'UTF-8 pour les langages qui utilisent des caractères rares présents dans la zone des caractères 3 octets dans UTF-8 : la plupart des caractères 3 octets sont des caractères 2 octets en UTF-16.

D'ou viennent les quelques octets en plus en UTF-32 par rapport aux tables 1 octet ?

Il s'agit des octets du BOM : Byte Order Mark.

Comme on doit encoder les valeurs sur plus d'un octet, il faut savoir quel octet va être multiplié par 1 (20), quel octet va être multiplié par 256 (28), quel octet va être multiplié par 65536 (216)...

Exemple : on veut stocker 65538.

On va donc décomposer de cette façon 65536*1 + 256*0 + 1*2.

Il faudra donc utiliser 3 octets valant 1, 0 et 2. L'octet 1 représente ici l'octet de poids fort.

Le problème ?

Dans la mémoire, le système a deux possibilités :

  1. BIG ENDIAN : l'octet de poids fort est stocké d'abord : 1-0-2 ou
  2. LITTLE ENDIAN : on stocke d'abord l'octet de poids faible : 2-0-1...

Comment savoir alors ?

C'est là qu'intervient le BOM : on place volontairement une suite d'octets connus FE16-FF16 en début du fichier texte.

Lorsque le système va lire le début du fichier, il lira donc

  • soit d'abord l'octet FE16 : on comprend qu'il faudra utiliser BIG ENDIAN.
  • soit d'abord l'octet FF16 : on comprend qu'il faudra utiliser LITTLE ENDIAN.

Exemple en UTF-16 : 2 octets pour le BOM

>>> o = [hex(v) for v in list("ABCD".encode("utf-16"))] >>> o ['0xff', '0xfe', '0x41', '0x0', '0x42', '0x0', '0x43', '0x0', '0x44', '0x0']

On voit ici qu'on est en LITTLE ENDIAN :

  • Le BOM est dans l'ordre FF puis FE
  • Le A de code 004116 est dans l'ordre 41 puis 00
  • ...

Exemple en UTF-32 : 4 octets pour le BOM

>>> o = [hex(v) for v in list("ABCD".encode("utf-32"))] >>> o ['0xff', '0xfe', '0x0', '0x0', '0x41', '0x0', '0x0', '0x0', '0x42', '0x0', '0x0', '0x0', '0x43', '0x0', '0x0', '0x0', '0x44', '0x0', '0x0', '0x0']

On voit ici qu'on est en LITTLE ENDIAN :

  • Le BOM est dans l'ordre FF puis FE puis 0 puis 0
  • Le A de code 004116 est dans l'ordre 41 puis 00 00 00
  • ...

Comment sait-on qu'on doit lire 1, 2 ou 4 octets lorsqu'on lit un texte encodé en UTF-8 ?

UN OCTET : caractères courants (dans notre culture occidentale)

Le contenu binaire de la forme  :  0xxx xxxx  x  représente un bit quelconque.

Le bit de poids fort est  0  : cela indique en UTF-8 que le caractère est encodé sur un seul octet :

Il y a 7  x  : on peut encoder 27 caractères : ceux de la table ASCII.

Exemple :

Le code ASCII du A est 65 en décimal et en binaire :  100 0001 .

Son code UTF-8 est donc obtenu en rajoutant  0  devant :  0100 0001 .

Il correspond donc toujours à un octet donnant 65 en décimal. C'est un A.

DEUX OCTETS : caractères un peu moins courants

Les deux octets ont cette structure :  110x xxxx  -  10xx xxxx .

Le premier octet de la suite commence par  11  suivi d'un bit  0  : cela veut dire qu'il faudra deux octets pour trouver la valeur UNICODE du caractère.

Le deuxième octet commence par  10 

Il y a 11  x  : on peut encoder 211 caractères supplémentaires.

Exemple :

Le "é" est encodé en UTF-8 par  1100 0011   1010 1001 .

En décimal, cela donne  195   169 .

TROIS OCTETS : caractères "rares"

Les trois octets ont cette structure :  110x xxxx  -  10xx xxxx  -  10xx xxxx .

Le premier octet commence par  111  suivi d'un bit  0  : cela veut dire qu'il faudra trois octets pour trouver la valeur UNICODE du caractère.

Les deux octets suivants commencent par  10 

Il y a 16  x  : on peut encoder 216 caractères supplémentaires (soit 65536).

Exemple :

Le "✐" est encodé par  1110 0010   1001 1100   1001 0000  .

En décimal, cela donne  226   156   144 .

QUATRE OCTETS : les caractères encore plus rares qu'on voudra y placer

Les quatre octets ont cette structure :  110x xxxx  -  10xx xxxx  -  10xx xxxx  -  10xx xxxx .

Le premier octet commence par  1111  suivi d'un bit  0  : cela veut dire qu'il faudra quatre octets pour trouver la valeur UNICODE du caractère.

Les trois octets suivants commencent par  10 

Il y a 21  x  : on peut encoder 221 caractères supplémentaires (soit plus de 2 millions).

Voilà pour l'encodage. C'est une source d'erreur très courante dans les programmes, d'où la nécessité d'en parler.

La prochaine activité Python fera juste le point sur l'enregistrement des fichiers-texte et nous pourrons voir l'influence de l'encodage sur la taille des enregistrements en enregistrant un même texte sous différents encodages.

Activité publiée le 09 02 2020
Dernière modification : 22 03 2021
Auteur : ows. h.