Python Unicode
Un article de Haypo.
Sommaire |
Python et Unicode
Depuis sa version 2.0, publié en octobre 2000, Python possède le type « unicode » qui permet de stocker du texte dans le charset Unicode. En interne, c'est un type de 16 bits (UCS-2) ou 32 bits (UCS-4) qui est utilisé. On est donc limité aux codes 0 à 65.535 / 0 à 4.294.967.295. Testez avec :
>>> import sys; print sys.maxunicode 1114111
(résultat sur Ubuntu Feisty, Python 2.5.1)
Travailler en Unicode
Avec Python 2.x, str() contient des octets et unicode() contient des caractères. Pour travailler simplement en Unicode, il faut travailler le plus longtemps possible avec des caractères (type 'unicode'). C'est-à-dire :
- Entrée : les convertir le plus tôt possible en unicode
- Sortie : les convertir le plus tard possible en octets
Pour les fichiers : utiliser le module codecs, « codecs.open(filename, mode, charset) » convertit automatiquement les lignes d'un fichier texte.
Pour le terminal (stdin/stdout) : il faut détecter le charset du terminal. Voir par exemple ma fonction getTerminalCharset().
Pour le reste, il faut deviner le charset. J'ai écrit ma fonction minimaliste pour différencier ASCII, UTF-8 et ISO-8859-1 : fonction guessBytesCharset(). Pour les besoins plus lourds (différencier les différents ISO-8859-*, les charsets asiatiques, etc.) : consultez mon article Détecter un charset.
Enfin, pour la conversion en entrée, si la détection échoue, utilisez le charset "ISO-8859-1". Mais ceci risque de poser des problèmes...
Ne pas mélanger !
Mélange des octets et des caractères peut déclencher des erreurs inattendues. Exemple :
>>> u"a" + "é" UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 0: ordinal not in range(128)
Erreur typique : quand on mélange les types str et unicode, Python va tenter de convertir les chaînes d'octets (str) en utilisant le charset le plus restrictif qu'il existe : ASCII en mode strict !
>>> unicode("é", "ASCII", "strict") UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 0: ordinal not in range(128)
Solution : convertir toutes les variables en utilisant le charset adéquat :
- Si c'est une chaîne écrite directement dans le code, lui ajouter le préfixe u : remplacer « "é" » par « u"é" ». Il faudra peut-être rajouter le charset au début de votre fichier source, du genre :
# -*- coding: utf8 -*-
- Si la chaîne vient d'une base de données : configurer la base pour qu'elle donne de l'unicode ou convertir manuellement en utilisant le charset utilisé par la base de données
- Si la chaîne vient de raw_input() : utiliser le charset du terminal (voir plus bas dans cet article)
- D'une manière générale : récupérer le bon charset et écrire « chaine = unicode(chaine, charset) »
Des fois ça marche !
Malheureusement pour nous, pauvres programmeurs, des fois les mélanges str/unicode fonctionnent ! C'est quand la chaîne d'octets ne contient que des codes dans l'interface [0; 127] : charset ASCII. Exemple :
>>> print u"salut %s !" % "victor" salut victor !
Ce comportement laisse penser que ça marche, or ça ne fonctionne pas toujours :
>>> print u"salut %s !" % "hervé" UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 4: ordinal not in range(128)
Solution : comme toujours, s'assurer qu'on ne manipule que de l'Unicode :
(prenom est par exemple saisi au clavier par l'utilisateur) >>> prenom = unicode(prenom, "utf8") >>> print u"salut %s !" % prenom salut hervé !
Attention à bien saisir le bon charset sous peine d'avoir un problème au moment de la conversion :
(...) >>> prenom = unicode(prenom, "utf8") UnicodeDecodeError: 'utf8' codec can't decode byte 0xff in position 0: unexpected code byte (...)
Exception
La classe BaseException n'a pas de méthode __unicode__(). Du coup, unicode(Exception(u"\xE9")) appelle __str__(). Or la méthode __str__() tente de faire u"\xE9".encode("ASCII", "strict"), ce qui échoue :-(
Idée pour contourner le problème :
- Créer une classe UnicodeException qui contient un attribut unicode_message
- Convertir le message ASCII pour le contructeur d'Exception
Implémentation :
class UnicodeException(Exception): def __init__(self, message): if isinstance(message, unicode): self.unicode_message = message message = message.encode('ASCII', 'replace') else: message = str(message) try: self.unicode_message = unicode(message, "utf8") except UnicodeEncodeError: self.unicode_message = unicode(message, "ISO-8859-1") Exception.__init__(self, message) def __unicode__(self): return self.unicode_message
Type str
Le type « str » ne devrait pas servir à contenir du texte. C'est un tableau d'octet et sûrement pas un tableau de caractères (contrairement au type « unicode »).
Deviner le charset utilisé par un type str n'est pas une mince affaire. Voici néanmoins quelques cas particulier.
isASCII()
def isASCII(text): try: text = unicode(text, 'ASCII', 'strict') return True except UnicodeDecodeError: return False
Comprendre le résultat :
- False indique que text contient des valeurs supérieures ou égales à 128
- True indique que text utilise (semble utiliser ?) le charset ASCII
isUTF8()
def isUTF8(text): try: text = unicode(text, 'UTF-8', 'strict') return True except UnicodeDecodeError: return False
Comprendre le résultat :
- False indique que la conversion a échoué, une séquence d'octets invalide a été trouvée (chaîne tronquée ? autre charset utilisé ?)
- True indique que la conversion s'est bien déroulée, il y a de très fortes chances que la chaîne soit formatée en UTF-8
guessBytesCharset()
J'ai écrit une fonction appelée guessBytesCharset() qui tente au mieux de détecter le charset d'une chaîne binaire. On peut la trouver dans le code d'Hachoir : module hachoir_core.i18n.
Encodage
Encodage "=E9" (MIME)
Pour décoder la chaîne "S=C3=A9bastien", on peut utiliser la regex du pauvre :
>>> import re; re.sub("=([A-F0-9]{2})", lambda regs: chr(int(regs.group(1), 16)), "S=C3=A9bastien") 'S\xc3\xa9bastien'
Mais le module mimify sert exactement à ça :-)
Encodage "%E9" (HTML)
Pour décoder la chaîne "S%E9bastien", on peut utiliser :
>>> import urllib; unicode(urllib.unquote("S%E9bastien"), "ISO-8859-1") u'S\xe9bastien'
Base64
Pour décoder des données encodée en base64 (ex: 'VG90bw=='), le module base64 dispose des fonctions b64encode() et b64decode().
Voir aussi la RFC 3548, The Base16, Base32, and Base64 Data Encodings.
Autres encodages
Voir aussi les modules binascii, quopri (quoted printable), uu, etc.
Conversion en ASCII (supprimer les accents)
Voir mon script unicode2ascii.py.
Voir également le billet de Peter Bengtsson : Unicode strings to ASCII... nicely.
Lire aussi
- Charsets et encoding (et Python)
- (en) Python Unicode Tutorial