Aller au contenu

Chapitre 2.6 - Codage des caractères⚓︎

Tout pour comprendre et éviter les erreurs d'encodage

image

1. Au commencement était l'ASCII⚓︎

pour American Standard Code for Information Interchange, créé en 1960 aux États-Unis.

En ASCII, 127 «points de code» (nombres associés aux caractères) sont disponibles. Les caractères sont donc codés sur 7 bits.

Exercice 1

Décoder l'expression suivante, écrite en ASCII :

1101100 1100101 1110011 100000 1001110 1010011 1001001 100000 1100011 100111 1100101 1110011 1110100 100000 1101100 1100101 1110011 100000 1101101 1100101 1101001 1101100 1101100 1100101 1110101 1110010 1110011

  • la fonction split(" ") permet de décomposer une chaine de caractères en une liste, en se servant de l'espace " " comme caractère séparateur.
  • int("1101100",2) permet de récupérer facilement la valeur en base 10 du nombre binaire 1101100.
Correction
Python
msg = '1101100 1100101 1110011 100000 1001110 1010011 1001001 100000 1100011 100111 1100101 1110011 1110100 100000 1101100 1100101 1110011 100000 1101101 1100101 1101001 1101100 1101100 1100101 1110101 1110010 1110011'
msg = msg.split(' ')
s = ''
for k in msg :
    s += chr(int(k,2))
print(s)

2. Et le reste du monde ?⚓︎

Lorsque d'autres personnes que des americains ou des anglais ont voulu s'échanger des données faisant intervenir du texte, certains caractères (é, è, à, ñ, Ø, Ö, β, 漢...) étaient manquants. Les 127 caractères de l'ASCII étaient largement insuffisants. Il a donc été décidé de passer à... 256 caractères ! Il suffisait pour cela de coder les caractères non plus sur 7 bits mais sur 8 bits.

Ainsi naquît, après de nombreuses modifications successives (la dernière en date rajoutant par exemple le symbole €), la célèbre table ISO 8859-15, dite aussi Latin-9 :

Utilisation :⚓︎

Les codes sont donnés en hexadécimal :

  • le caractère € correspond au code hexadécimal A4, donc au nombre décimal 164.
  • le caractère A correspond au code hexadécimal 41, donc au nombre décimal 65.

65... comme en ASCII ! Oui, la (seule) bonne idée aura été d'inclure les caractères ASCII avec leur même code, ce qui rendait cette nouvelle norme rétro-compatible.

Exemple :

Le fichier test.txt contient un texte enregistré avec l'encodage Latin-9. Ce fichier est ensuite ouvert avec un éditeur hexadécimal, qui permet d'observer la valeur des octets qui composent le fichier. (Comme le fichier est un .txt, le fichier ne contient que les données et rien d'autre.)

Parfait, mais comment font les Grecs pour écrire leur alphabet ? Pas de problème, il leur suffit d'utiliser... une autre table, appelée ISO-8859-7 :

On retrouve les caractères universels hérités de l'ASCII, puis des caractères spécifiques à la langue grecque... oui mais les Thaïlandais alors ?

Pas de problème, ils ont la ISO-8859-11 :

Évidemment, quand tous ces gens veulent discuter entre eux, les problèmes d'encodage surviennent immédiatement : certains caractères sont remplacés par d'autres.

3. Que fait un logiciel à l'ouverture d'un fichier texte ?⚓︎

Il essaie de deviner l'encodage utilisé... Parfois cela marche, parfois non.

Normalement, pour un navigateur, une page web correctement codée doit contenir dans une balise meta le charset utilisé.

Mais parfois, il n'y a pas d'autre choix pour le logiciel d'essayer de deviner l'encodage qui semble être utilisé.

Exercice 2

Le mot représenté par les octets ci-dessous est-il encodé en ASCII ou en Latin-9 ?

C'est du Latin-9, et c'est le mot "vélo"

4. Enfin une normalisation : l'arrivée de l'UTF⚓︎

image

En 1996, le Consortium Unicode décide de normaliser tout cela et de créer un système unique qui contiendra l'intégralité des caractères dont les êtres humains ont besoin pour communiquer entre eux.

Ils créent l'Universal character set Transformation Format : l'UTF. Ou plutôt ils en créent... plusieurs 😢 :

  • l'UTF-8 : les caractères sont codés sur 1, 2, 3 ou 4 octets.
  • l'UTF-16 : les caractères sont codés sur 2 ou 4 octets.
  • l'UTF-32 : les caractères sont codés sur 4 octets.

Pourquoi est-ce encore si compliqué ? En UTF-32, 32 bits sont disponibles, soit \(2^{32}=4294967296\) caractères différents encodables.

C'est largement suffisant, mais c'est surtout très très lourd !
D'autres encodages plus légers, mais plus complexes, sont donc proposés :

Arrêtons-nous sur l'UTF-8 :

Remarques : en UTF-8...

  • lorsqu'un caractère est codé sur un seul octet, on peut le décoder directement à partir de la valeur de cet octet car le premier bit étant égal à 0, cela n'interfère pas sur la valeur de l'octet.

  • lorsqu'un caractère est codé sur deux octets, il ne faut pas tenir compte des premiers bits de chaque octet (110 et 10).

  • lorsqu'un caractère est codé sur trois octets, il ne faut pas tenir compte des premiers bits de chaque octet (1110 puis 10 pour les suivants).

  • lorsqu'un caractère est codé sur quatre octets...

Le principe fondateur de l'UTF-8 est qu'il est adaptatif : les caracères les plus fréquents sont codés sur un octet, qui est la taille minimale (et qui donne le 8 de "UTF-8"). Les autres caractères peuvent être codés sur 2, 3 ou 4 octets au maximum.

L'encodage avec Python⚓︎

Depuis la version 3, Python représente ses chaînes de caractères en Unicode, et utilise par défaut l'encodage UTF-8 pour écrire des fichiers.

On peut passer d'un caractère à son code unicode par la fonction ord et réciproquement avec la fonction chr.

Tester : ord('A'), chr(65), ord('6'), ord('€')

Exercice 3

Utiliser Python pour retrouver l’expression codée en binaire avec la table Unicode :

0100 0010 0111 0010 0110 0001 0111 0110 0110 1111 0010 0001

Python
message_binaire = '010000100111001001100001011101100110111100100001'
message = ''

for i in range(len(message_binaire)//8): #Découpe la chaine par "paquet" de 8 soit ici 6 paquets
message += chr(int('0b' + message_binaire[0 + 8 * i:8 + 8 *  i],2)) #Convertir le paquet de 8 bits en décimal puis ensuite en caractére.

#Pour i = 0 [0 + 8 * 0:8 + 8 *  0], 2)) = [0:8,2] cela permet de lire les 8 premiers bits (de 0 à 7)
#Pour i = 1 [0 + 8 * 1:8 + 8 *  1], 2)) = [8:16,2] cela permet de lire les 8 bits qui suivent (de 8 à 15)

print(message)

5. La réponse à une question existentielle⚓︎

Pourquoi le caractère é en UTF-8 devient-il é en ISO 8859-15 ?

image

Q1. Grâce à la fonction ord puis à la fonction bin, écrire en binaire le nombre associé au caractère é en UTF-8.

Correction

Python
>>> ord('é')
233
>>> bin(233)
'0b11101001'
Donc en UTF-8, é est associé au nombre 11101001.

Q2. D'après l'explication de fonctionnement de l'encodage adaptatif de l'UTF-8 (voir ci-dessus), les 8 bits nécessaires à l'encodage de é en UTF-8 vont être «encapsulés» dans 2 octets de la forme 110XXXXX 10XXXXXX, où les 11 X représentent les 11 bits d'information disponibles. Écrire ces 2 octets en complétant si nécessaire avec des 0 à gauche.

Correction

Sur 11 bits, le nombre 11101001 va s'écrire 00011101001. En séparant ces 11 bits en deux groupes de 5 bits et 6 bits (00011et 101001), et en les encapsulant, on obtient les deux octets 11000011 10101001.

Q3. Convertir les deux octets obtenus en notation décimale (grâce à int) puis en hexadécimal (grâce à hex).

Correction
Python
>>> int('11000011', 2)
195
>>> hex(195)
'0xc3'
>>> int('10101001', 2)
169
>>> hex(169)
'0xa9'

Q4. Si un logiciel considère à tort que les deux octets servant à encoder le é en UTF-8 servent à encoder deux caractères en ISO 8859-15, quels seront ces deux caractères ?

Correction

Le premier octet, c3 en hexadécimal, sera perçu en ISO 8859-15 comme le caractère Ã.
Le deuxième octet, a9 en hexadécimal, sera perçu en ISO 8859-15 comme la lettre ©.

Finalement, ce qui aurait dû être un é en UTF-8 se retrouvera être un é en ISO 8859-15.

image

6. Utilisation grandissante de l'encodage UTF-8⚓︎

La majorité des sites internet utilisent maintenant l'UTF-8, tout comme les systèmes d'exploitation récents.

7. Exercices⚓︎

Exercice 1⚓︎

Lecture et écriture de fichiers (TRAVAILLER AVEC PYZO )

Partie 1⚓︎

Il convient d'être vigilant lors de la lecture/écriture de fichiers texte pour bien relire un fichier en spécifiant l'encodage avec lequel il a été encodé (par défaut UTF-8).

Un encodage différent peut être spécifié à l'ouverture du fichier en lecture ou en écriture. La liste des encodages standard peut être trouvée dans la documentation du langage Python

Recopier le programme texte_utf8 ci-dessous qui crée un fichier texte au format utf-8. (Enregistrer le fichier python sur le bureau)

Python
with open("mon_fichier.txt", mode='w', encoding='utf-8') as f:
    f.write("Ceci est un texte accentué, enregistré en UTF-8")

Exécuter le programme (un fichier "mon_fichier.txt" se créer sur le bureau. Ce fichier peut-être ouvert avec un éditeur de texte)

A la suite du programme texte_utf8 écrire et exécuter le script :

Python
with open("mon_fichier.txt", mode='r', encoding='latin1') as f:
    print(f.read())
Comprendre le problème et modifier le script afin de lire le texte correctement.

Partie 2⚓︎

  1. Ecrire un programme qui crée un fichier texte en latin1
  2. Lire ce fichier en latin1
  3. Réécrire ce même fichier en UTF-8
  4. Utiliser un éditeur de texte quelconque pour voir son contenu
Correction
Python
with open("second_fichier.txt", mode='w', encoding='latin1') as f:
    f.write("Ceci est un texte accentué, enregistré initialement en latin1")

with open("second_fichier.txt", mode='r', encoding='latin1') as f:
    texte = f.read()

with open("second_fichier.txt", mode='w', encoding='utf8') as f:
    f.write(texte)

Exercice 2⚓︎

Conversion de chaînes de caractères

Étant donnée une chaîne de caractères, l'objectif est de produire une copie de cette chaîne convertie en bas de casse, en « minuscules » dit-on couramment.

Par exemple, la chaîne "Les algorithmes de Bellman-Ford et de Dijkstra" sera convertie en "les algorithmes de bellman-ford et de dijkstra".

On rappelle qu'un caractère est encodé par un nombre entier que l'on obtient avec la fonction ord. Par exemple ord('A') est évalué à 65. Les codes des caractères alphabétiques non accentués en majuscule se suivent : ord('A') vaut 65, ord('B') vaut 66, ord('C') vaut 67, ...

Réciproquement, étant donné un entier positif, on obtient le caractère encodé par cet entier avec la fonction chr. Par exemple chr(65) est évalué à 'A'.

On se limite à convertir les caractères allant de 'A' à 'Z'.

Compléter la fonction minuscule qui prend en paramètre une chaîne de caractères chaine et renvoie une nouvelle chaîne qui est la copie de la chaîne chaine convertie en minuscules.

Pour cela, on remarquera que si c_maj est un caractère en majuscule et c_min le même caractère en minuscule, alors la différence ord(c_min) - ord(c_maj) vaut 32.

Exemple

Python
>>> minuscule('ABCDE')
'abcde'
>>> minuscule("Les algorithmes de Bellman-Ford et de Dijkstra")
'les algorithmes de bellman-ford et de dijkstra'

Fonction, opérateur ou module interdit

Dans cet exercice on interdit d'utiliser la fonction, l'opérateur ou le module :

str.lower()

À vous de jouer

###(Dés-)Active le code après la ligne # Tests (insensible à la casse)
(Ctrl+I)
Entrer ou sortir du mode "deux colonnes"
(Alt+: ; Ctrl pour inverser les colonnes)
Entrer ou sortir du mode "plein écran"
(Esc)
Tronquer ou non le feedback dans les terminaux (sortie standard & stacktrace / relancer le code pour appliquer)
Si activé, le texte copié dans le terminal est joint sur une seule ligne avant d'être copié dans le presse-papier

😀 La correction est arrivée ...

Solution

Python
def minuscule(chaine):
copie = ""
for caractere in chaine:
    code = ord(caractere)
    if ord("A") <= code <= ord("Z"):
        code = code + 32
    copie = copie + chr(code)
return copie
Remarques :

Cette différence de 32 entre les entiers qui encode un caractère en minuscule et le même caractère en majuscule n'est probablement pas le fruit du hasard.

On considére les 128 premiers caractères encodés par un entier allant de 0 à 127. Les caractères allant de 'A' à 'Z' sont encodés par les entiers allant de 65 à 90, soit en binaire de 1000001 à 1011010.

Une règle simple permettant de passer des majuscules aux minuscules et réciproquement, qui est la même pour tous les caractères alphabétiques non accentués, consiste à modifier le deuxième bit à partir de la gauche (le seul envisageable).

Le codage de 'A' est 100 0001, celui de 'a' est 110 0001, le codage de 'Z' est 1011010, celui de 'z' est 1111010. Or, modifier ce bit de 0 à 1 revient à ajouter 32, le modifier de 1 à 0 revient à soustraire 32.

Exercice 3⚓︎

Le chiffre de César

Le chiffrement de César transforme un message en changeant chaque lettre par une autre obtenue par décalage circulaire dans l'alphabet de la lettre d'origine. Par exemple, avec un décalage de 3, le 'A' se transforme en 'D', le 'B' en 'E', ..., le 'X' en 'A', le 'Y' en 'B' et le 'Z' en 'C'.

Les autres caractères ('!', '?'...) ne sont pas transformés et sont simplement recopiés tels quels dans le message codé.

Dans cet exercice, nous ne prendrons que des lettres majuscules.

On fournit les deux fonctions

  • indice : renvoie l'indice dans l'alphabet d'une lettre majuscule en commençant à 0.
  • majuscule : renvoie la lettre majuscule d'indice donné.

Exemple

Python
>>> indice('C')
2
>>> majuscule(4)
'E'
Remarque

Pour opérer un décalage circulaire d'un indice, on utilise l'opération modulo 26 qui renvoie un résultat de 0 inclus à 26 exclu.

Exemple si dessous pour un décalage de 8 pour la lettre 'Z'

Exemple

Python
>>> indice('Z')
>>> 25 + 8
33
>>> 33 % 26
7
>>> majuscule(7)
'H'

Le chiffrement de 'Z' sera ici 'H' pour un décalage de 8

Objectif

Écrire la fonction cesar qui prend en paramètres une chaine de caractères message et un nombre entier decalage et renvoie le nouveau message chiffré avec le chiffre de César utilisant ce decalage.

On constate que pour déchiffrer un message, il suffit d'utiliser la clé opposée à celle du chiffrement.

Exemple

Python
>>> cesar("HELLO WORLD!", 5) 
'MJQQT BTWQI!'
>>> cesar("MJQQT BTWQI!", -5) 
'HELLO WORLD!'
À vous de jouer

###(Dés-)Active le code après la ligne # Tests (insensible à la casse)
(Ctrl+I)
Entrer ou sortir du mode "deux colonnes"
(Alt+: ; Ctrl pour inverser les colonnes)
Entrer ou sortir du mode "plein écran"
(Esc)
Tronquer ou non le feedback dans les terminaux (sortie standard & stacktrace / relancer le code pour appliquer)
Si activé, le texte copié dans le terminal est joint sur une seule ligne avant d'être copié dans le presse-papier

😀 La correction est arrivée ...

Solution

Python
def indice(caractere):
    "Renvoie l'indice de `caractere` qui doit être une majuscule"
    return ord(caractere) - ord("A")


def majuscule(i):
    """Renvoie la majuscule d'indice donnée
    majuscule(0) renvoie 'A'
    majuscule(25) renvoie 'Z'
    """
    return chr(ord("A") + i)


def cesar(message, decalage):
    resultat = ""
    for caractere in message:
        if "A" <= caractere <= "Z":
            i = indice(caractere)
            i = (i + decalage) % 26
            resultat += majuscule(i)
        else:
            resultat += caractere
    return resultat
Remarques :

** Aspect sécurité**

Jules César utilisait cette méthode avec l'alphabet grec, non compris de la plupart des gaulois, mais maitrisé par l'élite romaine.

Cette méthode de chiffrement n'est pas sécurisée et très facile à casser en tentant tous les décalages possibles ou en faisant une analyse des fréquences des lettres.

Aujourd'hui, elle est utilisée dans les livres-jeu où on souhaite donner un indice masqué, facilement accessible.

Exercice 4⚓︎

La perfection d'un mot

On affecte à chaque lettre de l'alphabet un code selon le tableau ci-dessous :

Pour un mot donné (une chaine de caractères non vide uniquement composée de lettres majuscules), on détermine :

  • d'une part son code alphabétique concaténé, obtenu par la juxtaposition du texte des codes de chacun de ses caractères, et lu comme un unique entier,
  • et d'autre part, son code additionné, qui est la somme des codes de chacun de ses caractères.

On dit que ce mot est « parfait » si le code additionné divise le code concaténé.

Exemple

  1. Pour le mot "PAUL", les codes sont 16,1,21,12.
  2. Le code concaténé est la chaine 1612112, soit l'entier 1 612 112
  3. Son code additionné est l'entier 16 + 1 + 21 + 12 qui donne 50
  4. 50 ne divise pas l'entier 1 612 112 ; par conséquent, le mot "PAUL" n'est pas parfait.

  5. Pour le mot "ALAIN", les codes sont 1,12,1,9,14

  6. Le code concaténé est la chaine 1121914, soit l'entier 1 121 914
  7. Son code additionné est l'entier 1 + 12 + 1 + 9 + 14 qui donne 37
  8. 37 divise l'entier 1 121 914 ; par conséquent, le mot "ALAIN" est parfait.

Rappel conversion

Pour cet exercice on pourra utiliser str et int comme fonctions de conversion. D'autre part, on pourra utiliser la fonction ord qui renvoie le code ASCII d'un caractère ASCII passé en paramètre.

Python
>>> ord('A')
65
>>> ord('B')
66
Python
>>> str(12)
'12'
>>> str(12) + str(14)
'1214'
>>> int('1214')
1214
>>> int('1214') % 10
4

Compléter la fonction est_parfait ci-dessous qui prend comme argument une chaine de caractères mot (en lettres majuscules) et qui renvoie le code alphabétique concaténé, le code additionné de mot, ainsi qu'un booléen qui indique si mot est parfait ou pas.

Examples

Python
>>> est_parfait("PAUL")
(50, 1612112, False)
>>> est_parfait("ALAIN")
(37, 1121914, True)
À vous de jouer

###(Dés-)Active le code après la ligne # Tests (insensible à la casse)
(Ctrl+I)
Entrer ou sortir du mode "deux colonnes"
(Alt+: ; Ctrl pour inverser les colonnes)
Entrer ou sortir du mode "plein écran"
(Esc)
Tronquer ou non le feedback dans les terminaux (sortie standard & stacktrace / relancer le code pour appliquer)
Si activé, le texte copié dans le terminal est joint sur une seule ligne avant d'être copié dans le presse-papier

😀 La correction est arrivée ...

Solution
Python
def code(lettre):
    "Renvoie le code suivant l'énoncé."
    return ord(lettre) - ord("A") + 1


def est_parfait(mot):
    code_int_cumul = 0
    code_str_concatene = ""
    for lettre in mot:
        valeur = code(lettre)
        code_int_cumul += valeur
        code_str_concatene += str(valeur)
    code_concatene = int(code_str_concatene)
    mot_est_parfait = code_concatene % code_int_cumul == 0  # un booléen
    return (code_int_cumul, code_concatene, mot_est_parfait)

Exercice 5⚓︎

Voir ENT->capytale : Code du Notebook bfcb-279837