Mémo de survie pour l'encodage des caractères en python
En python, un nombre incalculable de plantages vient de problèmes d'encodage. La principale difficulté est que python utilise deux types de chaînes différents.
Rappel sur les encodages
La norme de codage ascii est la seule à être vraiment reconnue partout. Elle code un caractère par octet. Elle ne code que les caractères latins non accentués plus quelques symboles standards. Elle ne spécifie que 128 caractères sur les 256 que l'on peut coder avec un octet.
Plusieurs normes étendent l'ascii pour prendre en charge les accents tout en conservant le principe un caractère = un octet. Ascii laisse la place pour 128 caractères supplémentaires, ce qui est peu. On doit donc choisir, en fonction de la langue utilisée, la norme de codage appropriée. Cela pose problème notamment dans les documents bilingues. En français, on peut utiliser le latin-1, également connu sous le nom iso-8859-1. Afin de prendre en charge le symbole € et quelques autres, la variante iso-8859-15 est également utilisée.
Unicode est un codage universel qui prend en charge tous les caractères possibles et imaginables. Pour cela, il utilise quatre octets par caractère au lieu de un. Comme c'est une grosse perte d'espace, on encode rarement directement en unicode. Utf-8 est une norme pour écrire de l'unicode avec un nombre variable d'octets par caractère selon l'indice du caractère dans la table unicode.
Remarque : un texte qui ne contient que des caractères ascii a exactement la même représentation en ascii, en latin-1 et en utf-8.
Types de chaînes
Normale. Ici, un octet = une case, c'est ce type qui est utilisé quand lit ou quand ou écrit dans un fichier.
>>> 'rusé' # Ici l'encodage du système est utf-8, donc un "é" prend deux octets (le fameux "é" à la place du "é") 'rus\xc3\xa9' >>> len('rusé') # 5 octets au total 5 >>> print 'rus\xc3\xa9' rusé
Unicode. Ici, un caractère = une case, indépendamment de la représentation en mémoire. À moins d'être masochiste, c'est le format à utiliser pour la plupart des opérations sur les chaînes (extraction de sous chaînes, split, parsage etc...).
>>> u'rusé' u'rus\xe9' >>> len(u'rusé') # 4 caractères au total 4 >>> print u'rus\xe9' rusé
Raw. Identique à une chaîne normale mais pas de caractères d'échappement. Rarement utilisé.
>>> print r'rus\xe9' rus\xe9 >>> len(r'rus\xe9') # Pas de caractères d'échappement 7
Caractères d'échappement
On peut reprendre la majorité des caractères d'échappement du C, comme le \n.
>>> 'ligne 1\nligne 2' 'ligne 1\nligne 2' >>> print 'ligne 1\nligne 2' ligne 1 ligne 2 >>> print u'ligne 1\nligne 2' #idem dans une chaîne unicode ligne 1 ligne 2
Pour rentrer des caractères arbitraires dans une chaîne normale, \xhh insère l'octet hh (en hexadécimal)
>>> u'rusé'.encode('latin-1') 'rus\xe9' >>> u'rusé'.encode('utf-8') 'rus\xc3\xa9'
Dans une chaîne unicode, c'est un peu plus retors :
\xhh insère un des 256 premiers caractères de la table unicode, ce qui devrait donner plus ou moins la même chose que l'encodage latin-1
\uhhhh insère un des 65536 premiers caractères de la table unicode
>>> u'rusé' u'rus\xe9' >>> u'Eĥoŝanĝo ĉiuĵaŭde' u'E\u0125o\u015dan\u011do \u0109iu\u0135a\u016dde'
Encode() et decode()
encode('un-type-d-encodage') permet de passer d'une chaine unicode à une chaîne normale encodée avec l'encodage donné.
>>> u"rusé".encode('utf-8') 'rus\xc3\xa9' >>> u"rusé".encode('latin-1') 'rus\xe9'
Réciproquement, decode('un-type-d-encodage') permet de passer d'une chaîne encodée avec l'encodage donné à une chaîne unicode.
>>> 'rus\xe9'.decode('latin-1') u'rus\xe9' >>> unicode('rus\xe9', 'latin-1') # de manière équivalente u'rus\xe9'
Exception notable : l'encodage hex_codec va d'une chaîne normale à une chaîne normale 2 fois plus longue et réciproquement.
>>> 'rusé'.encode('hex_codec') '727573c3a9' >>> '727573c3a9'.decode('hex_codec') 'rusé'
Afin qu'un décodage n'échoue pas, il est judicieux de préciser comment traiter les caractères erronés.
>>> 'rus\xe9'.decode('utf-8') # Invalide pour de l'utf-8 Traceback (most recent call last): File "<stdin>", line 1, in <module> File "encodings/utf_8.py", line 16, in decode UnicodeDecodeError: 'utf8' codec can't decode byte 0xe9 in position 3: unexpected end of data >>> 'rus\xe9'.decode('utf-8','ignore') u'rus' >>> 'rus\xe9'.decode('utf-8','replace') u'rus\ufffd' >>> print u'rus\ufffd' rus�
Deux choses à ne pas faire :
u"bla bla".decode("un-type-d-encodage")
"bla bla".encode("un-type-d-encodage")
On pourrait parler de faute de typage. encode() et decode() s'utilisent dans l'autre sens.
Encodages par défaut
Il y en a de plusieurs sortes :
Encodage par défaut de python. Défini quelque part dans le /etc. En général c'est ascii, il ne vaut mieux pas le changer. Il sera utilisé par encode() et decode() lorsque l'encodage n'est pas précisé.
>>> import sys >>> sys.getdefaultencoding() 'ascii'
Encodage du système de fichier. Pour manipuler les noms de fichiers. Attention toutefois, certains noms de fichiers peuvent avoir un encodage invalide. Il n'est donc pas prudent de stocker les noms de fichiers dans des chaînes unicode.
>>> import sys >>> sys.getfilesystemencoding() 'UTF-8'
Locale : encodage par défaut du système. C'est celui qu'il faut prendre a priori pour lire et écrire dans un fichier. Encore une fois, il peut quand même y avoir des entrées invalides.
>>> import locale >>> locale.getlocale() (None, None) >>> locale.getdefaultlocale() ('fr_FR', 'UTF8')
Encodage des fichiers sources
Lorsqu'on utilise des caractères accentués dans le code source, il faut préciser quel est l'encodage du fichier. Cela se fait au début du fichier.
# -*- coding: utf-8 -*-
Éviter d'écrire des caractères non ascii dans des chaînes normales dans un fichier source.
UnicodeEncodeError: 'ascii' codec can't encode character u'\xe9' in position 0: ordinal not in range(128)
Cette erreur arrive principalement quand une fonction qui prenait une chaîne normale en argument a reçu une chaîne unicode. Par exemple, quand on écrit dans un fichier.