Python Unicode

Un article de Haypo.

(Différences entre les versions)
Version du 11 juillet 2008 à 21:19 (modifier)
Haypo (Discuter | Contributions)
(Ne pas mélanger !)
← Différence précédente
Version actuelle (18 novembre 2012 à 01:16) (modifier) (défaire)
Haypo (Discuter | Contributions)
(En pratique avec Python3)
 
(48 révisions intermédiaires masquées)
Ligne 1 : Ligne 1 :
[[Catégorie:Langage de programmation]] [[Catégorie:Langage de programmation]]
{{Retour|Python|Retour à Python}} {{Retour|Python|Retour à Python}}
 +
 +Je suis en train d'écrire un livre sur la [https://github.com/haypo/unicode_book/wiki programmation avec Unicode], en anglais.
== Python et Unicode == == Python et Unicode ==
Ligne 8 : Ligne 10 :
1114111 1114111
(résultat sur Ubuntu Feisty, Python 2.5.1) (résultat sur Ubuntu Feisty, Python 2.5.1)
 +
 +== Conférence Pycon FR 2009 ==
 +
 +J'ai donné une conférence fin mai 2009 sur Unicode :
 +* Voir la vidéo de la conférence [http://video.pycon.fr/videos/free/69/ Comprendre les erreurs Unicode]
 +* [http://dl.afpy.org/pycon-fr-09/Comprendre_les_erreurs_unicode.pdf Diaporama (pdf) de la conférence]
== Travailler en Unicode == == Travailler en Unicode ==
Ligne 17 : Ligne 25 :
Pour les fichiers : utiliser le module codecs, « codecs.open(filename, mode, charset) » convertit automatiquement les lignes d'un fichier texte. 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 [http://hachoir.org/browser/trunk/hachoir-core/hachoir_core/i18n.py#L23 fonction getTerminalCharset()].+== Deviner le jeu de caractères ==
-Pour le reste, il faut deviner le charset. J'ai écrit ma fonction minimaliste pour différencier ASCII, UTF-8 et ISO-8859-1 : [http://hachoir.org/browser/trunk/hachoir-core/hachoir_core/i18n.py#L158 fonction guessBytesCharset()]. Pour les besoins plus lourds (différencier les différents ISO-8859-*, les charsets asiatiques, etc.) : consultez mon article [http://www.haypocalc.com/wiki/Détecter_un_charset Détecter un charset].+=== Terminal ===
-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...+Pour détecter le jeu de caractères du terminal, utilisez la fonction [http://bitbucket.org/haypo/hachoir/src/tip/hachoir-core/hachoir_core/i18n.py#cl-24 getTerminalCharset()].
 + 
 +=== Système de fichier ===
 + 
 +[http://docs.python.org/library/sys.html#sys.getfilesystemencoding sys.getfilesystemencoding()] indique le jeu de caractères par défaut du système de fichier.
 + 
 +=== 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 ma fonction minimaliste pour différencier ASCII, UTF-8 et ISO-8859-1 : [http://bitbucket.org/haypo/hachoir/src/tip/hachoir-core/hachoir_core/i18n.py#cl-164 fonction guessBytesCharset()].
 + 
 +Pour les besoins plus lourds (différencier les différents ISO-8859-*, les jeux de caractères asiatiques, etc.) : consultez mon article [http://www.haypocalc.com/wiki/Détecter_un_charset Détecter un charset].
 + 
 +Si la détection échoue, utilisez le charset "ISO-8859-1". Mais ceci risque de poser des problèmes...
== Ne pas mélanger ! == == Ne pas mélanger ! ==
Ligne 51 : Ligne 95 :
>>> print u"salut %s !" % prenom >>> print u"salut %s !" % prenom
salut hervé ! 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 == == Exception ==
Ligne 80 : Ligne 129 :
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 »). 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 [http://hachoir.org/browser/trunk/hachoir-core/hachoir_core/i18n.py hachoir_core.i18n]. 
== Encodage == == Encodage ==
Ligne 139 : Ligne 158 :
== Conversion en ASCII (supprimer les accents) == == Conversion en ASCII (supprimer les accents) ==
-Voir mon script '''{{HaypoSVN svnweb link|misc/unicode2ascii.py|unicode2ascii.py}}'''.+Voir mon script '''[http://bitbucket.org/haypo/misc/src/tip/python/unicode2ascii.py unicode2ascii.py]'''.
Voir également le billet de Peter Bengtsson : ''[http://www.peterbe.com/plog/unicode-to-ascii Unicode strings to ASCII... nicely]''. Voir également le billet de Peter Bengtsson : ''[http://www.peterbe.com/plog/unicode-to-ascii Unicode strings to ASCII... nicely]''.
-== Article connexions ==+== Base de données ==
 + 
 +* MySQL
 +** db = MySQLdb.connect(..., use_unicode="True", charset="utf8")
 +** Une table MySQL a un jeu de caractères : « CREATE TABLE .... [DEFAULT] '''CHARACTER SET charset_name''' [COLLATE collation_name] » (depuis MySQL 4.1)
 +** MySQL n'aime pas "utf-8", utiliser utf8
 +** Lire le chapitre [http://dev.mysql.com/doc/refman/5.0/fr/charset.html Chapitre 10. Jeux de caractères et Unicode] de la documentation MySQL
 +* PostgreSQL
 +** [http://initd.org/psycopg/docs/connection.html#connection.encoding connection.encoding]
 +** [http://initd.org/psycopg/docs/usage.html#unicode-handling Unicode handling]
 +* LDAP (python-ldap)
 +** LDAP parle UTF-8 depuis la version 3 ([http://tools.ietf.org/html/rfc4511 RFC 4511])
 +** en février 2011, python-ldap ne supportait pas encore le type unicode de Python2 pour consever la compatibilité LDAP version 2 qui exige un encodage explicite
 + 
 +== Texte encodé deux fois en UTF-8 ==
 + 
 +Fonction pour convertir du texte encodé deux fois en UTF-8 :
 + def decodeUtf8Utf8(octets):
 + return octets.decode("utf8").encode("latin1").decode("utf8")
 + 
 +Exemple d'utilisation :
 + >>> print decodeUtf8Utf8('\xc3\x83\xc2\xa9')
 + é
 + 
 +Si vous avez une chaîne unicode :
 + >>> print decodeUtf8Utf8(u'\xc3\x83\xc2\xa9'.encode("latin1"))
 + é
 + 
 +== Python3 ==
 + 
 +* [http://www.python.org/dev/peps/pep-0383/ PEP 383]: Non-decodable Bytes in System Character Interfaces
 +* [http://bugs.python.org/issue5915 Issue #5915]: Implement PEP 383, Non-decodable Bytes in System Character Interfaces.
 +** [http://svn.python.org/view?view=rev&revision=72313 Commit 72313]
 +* [http://bugs.python.org/issue3672 Issue #3672]: Reject surrogates in utf-8 codec; add surrogates error handler.
 +** [http://svn.python.org/view?view=rev&revision=72208 Commit 72208]
 +* [http://bugs.python.org/issue3187 Issue #3187]: os.listdir can return byte strings
 + 
 +== OS encoding(s) ==
 + 
 +OS data:
 +* command line arguments
 +* environment variables
 +* standard input, output and error
 +* filenames
 + 
 +Mac OS X:
 +* filenames: UTF-8
 +* command line arguments:
 +** locale encoding for Python < XXX
 +** UTF-8 for Python > XXX (changeset 45079ad1e260, issue #4388)
 + 
 +Windows:
 +* filenames: MBCS (ANSI code page)
 + 
 +UNIX:
 +* filenames: locale encoding, setlocale(LC_CTYPE, ""); encoding = nl_langinfo(CODESET)
 + 
 +== En pratique avec Python3 ==
 + 
 +=== Jeu de caractères par défaut ===
 + 
 +* Exemple d'utilisation : b"abc".decode() et "abc".encode()
 +* sys.getdefaultencoding() == 'utf8'
 +* Bien qu'il existe sys.setdefaultencoding(), on ne peut pas modifier le jeu de caractères par défaut (un jeu de caractères autre que "utf8" émet une erreur)
 + 
 +=== Noms de fichiers et variables d'environnement ===
 + 
 +* Jeu de caractères sys.getdefaultencoding(), "surrogateescape" error handler
 +* sys.getfilesystemencoding()
 +** Windows : "mbcs"
 +** Mac OS X : "utf-8"
 +** Autre : nl_langinfo(CODESET), ou "utf8" si nl_langinfo() échoue ou n'existe pas
 +* (sys.setfilesystemencoding() : fonction supprimée par le module site)
 + 
 +Pour les variables d'environnement, Python 3.2 a en plus os.environb et os.getenvb() pour récupérer les variables dans le type bytes (avant qu'elles soient décodées).
 + 
 +=== TextIOWrapper, stdin, stdout et stderr ===
 + 
 +os.device_encoding(fd) :
 +* None is fd n'est pas un TTY
 +* Windows : GetConsoleCP() si fd==0, GetConsoleOutputCP() si fd in (1, 2), None sinon
 +* POSIX : nl_langinfo(CODESET) (sans modifier la locale)
 + 
 +Choix du jeu de caractères par io.TextIOWrapper() si l'encodage n'est pas spécifié :
 +* os.device_encoding(buffer.fileno())
 +* ou locale.getpreferredencoding()
 +* ou "ascii"
 +* Note : utilise errors="strict" si errors n'est pas spécifié
 + 
 +sys.stdin, sys.stdout, sys.stderr :
 +* Utilise la variable d'environnement PYTHONIOENCODING si elle est définie. Exemples :
 +** PYTHONIOENCODING=utf8
 +** PYTHONIOENCODING=utf8:backslashreplace
 +* Sinon, délègue le choix à TextIOWrapper()
 +* Exception : si stdout n'est pas un terminal (sys.stdout.isatty() == False), utilise "ascii". Pareil pour stderr.
 +** http://bugs.python.org/issue7745
 +** http://www.mail-archive.com/python-dev@python.org/msg44650.html
 + 
 +=== Arguments de la ligne de commande ===
 + 
 +L'algorithme ressemble à ça :
 + # argv est une liste de bytes
 + encoding = locale.getpreferredencoding()
 + sys.argv = [arg.decode(encoding, 'surrogateescape') for arg in argv]
 + 
 +Les arguments sont décodés selon le jeu de caractères de la locale, et les caractères ne pouvant être décodés sont stockés sous forme de ''surrogates''.
 + 
 +<code>char** argv</code> est converti en <code>wchar_t**</code> par _Py_char2wchar() qui utilise mbstowcs() ou mbrtowc()+surrogates), puis en liste de chaînes unicode par PySys_SetArgv() qui utilise PyUnicode_FromWideChar().
 + 
 +Pour récupérer sys.argv sous forme d'octets, utilisez :
 + encoding = locale.getpreferredencoding()
 + argv_bytes = [arg.encode(encoding, "surrogateescape") for arg in sys.argv]
 + 
 +=== Fonctions du module locale ===
 + 
 +locale.getpreferredencoding() :
 +* Windows : _locale._getdefaultlocale()[1]
 +* POSIX avec CODESET : modifie la locale avec setlocale(LC_TYPE, "") (sauf si l'argument do_setlocale est défini à False) puis appelle nl_langinfo(CODESET). Sous Mac OS X, renvoie "utf-8" le résultat est None
 +* POSIX sans CODESET : getdefaultlocale()[1], ou "ascii" si le résultat est None
 + 
 +Lecture du jeu de caractères avec locale._getdefaultlocale() :
 +* _locale._getdefaultlocale() si la fonction existe
 +* Sinon cherche la première variable d'environnement définie parmis LC_ALL, LC_CTYPE, LANG, LANGUAGE : utilise "C" si aucune n'est définie. Puis devine le jeu de caractères à partir de la locale :
 +** Si la locale contient un point : ce qui est écrit après le point ("fr_FR.utf8" => UTF-8)
 +** ISO-8859-15 si la locale se termine par "..."@euro"
 +** None sinon
 + 
 +== En pratique avec Python2 ==
 + 
 +=== Jeu de caractères par défaut ===
 + 
 +* Exemple d'utilisation : "abc".decode() et u"abc".encode()
 +* sys.getdefaultencoding() == 'ascii' par défaut, mais c'est modifiable avec sys.setdefaultencoding() (bien que très déconseillé : nombreux effets de bord !)
 + 
 +=== Noms de fichiers et variables d'environnement ===
 + 
 +* Jeu de caractères sys.getdefaultencoding() (mode strict)
 +* sys.getfilesystemencoding()
 +** Windows : "mbcs"
 +** Mac OS X : "utf-8"
 +** Autre : nl_langinfo(CODESET), ou "utf8" si nl_langinfo() échoue ou n'existe pas
 + 
 +=== io.TextIOWrapper ===
 + 
 +Choix du jeu de caractères par io.TextIOWrapper() si l'encodage n'est pas spécifié :
 +* locale.getpreferredencoding()
 +* ou "ascii"
 + 
 +=== Fonctions du module locale ===
 + 
 +locale.getpreferredencoding() :
 +* Windows : _locale._getdefaultlocale()[1]
 +* POSIX avec CODESET : modifie la locale avec setlocale(LC_TYPE, "") (sauf si l'argument do_setlocale est défini à False) puis appelle nl_langinfo(CODESET)
 +* POSIX sans CODESET : getdefaultlocale()[1]
 + 
 +== En pratique avec Python3 (code source) ==
 + 
 +a :
 +* PyUnicode_GetDefaultEncoding(), PyUnicode_SetDefaultEncoding
 +** En Python : sys.getdefaultencoding()
 +** PyUnicode_GetDefaultEncoding() est utilisé quand encoding==NULL, exemple Python : b"abc".decode()
 +** Python3 : valeur utf8, non modifiable
 +* sys.getfilesystemencoding()
 +** Python/pythonrun.c : Py_InitializeEx() initialise Py_FileSystemDefaultEncoding avec nl_langinfo(CODESET)
 +** Python/bltinmodule.c : Py_FileSystemDefaultEncoding
 +** Défaut : "mbcs" sous Windows", "utf8" sous Mac OS X, None sinon
 + 
 +b :
 +* os.environ
 +** Modules/posixmodule.c : convertenviron()
 +** Windows : PyUnicode_FromWideChar()
 +** POSIX : PyUnicode_Decode(..., Py_FileSystemDefaultEncoding, "surrogateescape")
 + 
 +c :
 +* locale.getpreferredencoding()
 +** Windows : _locale._getdefaultlocale()[1]
 +** POSIX avec CODESET : setlocale(LC_TYPE, ""); nl_langinfo(CODESET); setlocale(LC_TYPE, oldloc) ou "utf8"
 +** POSIX sans CODESET : getdefaultlocale()[1] ou "ascii", parse les variables d'environnement
 +* os.device_encoding(buffer.fileno())
 +** Windows : GetConsoleCP() si fd==0, GetConsoleOutputCP() si fd in (1, 2), None sinon
 +** POSIX : nl_langinfo(CODESET)
 +* io.TextIOWrapper() si encoding==None :
 +** os.device_encoding(buffer.fileno())
 +** ou locale.getpreferredencoding()
 +** ou "ascii"
 + 
 +d :
 +* sys.stdin, sys.stdout, sys.stderr
 +** sys.stdin.encoding, sys.stdout.encoding, sys.stderr.encoding
 +** Python/pythonrun.c : initstdio()
 +** Variable d'environement PYTHONIOENCODING : PYTHONIOENCODING=utf8 ou PYTHONIOENCODING=utf8:backslashreplace
 +** Appelle io.TextIOWrapper(..., encoding, errors, ...)
 + 
 +Python 3.2 sous Linux :
 + >>> locale.getpreferredencoding()
 + 'UTF-8'
 + >>> sys.getdefaultencoding()
 + 'utf-8'
 + >>> sys.getfilesystemencoding()
 + 'utf-8'
 + >>> sys.stdout.encoding
 + 'UTF-8'
 +Python 3.2 sous Linux avec LANG= :
 + >>> locale.getpreferredencoding()
 + 'ANSI_X3.4-1968'
 + >>> sys.getdefaultencoding()
 + 'utf-8'
 + >>> sys.getfilesystemencoding()
 + 'ascii'
 + >>> sys.stdout.encoding
 + 'ANSI_X3.4-1968'
 + 
 +== wchar_t ==
 + 
 +* Debian Sid sur x86 (32 bits): sizeof(wchar_t) == 4
 +* Sous Windows, sizeof(wchar_t) == 2 : utilise UTF-16
 + 
 +== Voir aussi ==
 + 
 +=== Lire aussi ===
 + 
 +* [http://sebsauvage.net/python/charsets_et_encoding.html Charsets et encoding] (et Python)
 +* (en) [http://docs.python.org/howto/unicode.html Unicode HOWTO] : Guide de la documentation officielle Python
 +* (en) [http://www.reportlab.com/i18n/python_unicode_tutorial.html Python Unicode Tutorial]
 +* (en) [http://docs.djangoproject.com/en/dev/ref/unicode/ Unicode data in Django]
 + 
 +== Article connexes ==
* [[Python]] * [[Python]]
* [[Démystifier Unicode]] * [[Démystifier Unicode]]

Version actuelle

Retour à la page précédente Retour à Python

Je suis en train d'écrire un livre sur la programmation avec Unicode, en anglais.

Sommaire

[modifier] 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)

[modifier] Conférence Pycon FR 2009

J'ai donné une conférence fin mai 2009 sur Unicode :

[modifier] 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.

[modifier] Deviner le jeu de caractères

[modifier] Terminal

Pour détecter le jeu de caractères du terminal, utilisez la fonction getTerminalCharset().

[modifier] Système de fichier

sys.getfilesystemencoding() indique le jeu de caractères par défaut du système de fichier.

[modifier] 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

[modifier] 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

[modifier] guessBytesCharset()

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 jeux de caractères asiatiques, etc.) : consultez mon article Détecter un charset.

Si la détection échoue, utilisez le charset "ISO-8859-1". Mais ceci risque de poser des problèmes...

[modifier] 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) »

[modifier] 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
(...)

[modifier] 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

[modifier] 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 »).

[modifier] Encodage

[modifier] 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 :-)

[modifier] 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'

[modifier] 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.

[modifier] Autres encodages

Voir aussi les modules binascii, quopri (quoted printable), uu, etc.

[modifier] Conversion en ASCII (supprimer les accents)

Voir mon script unicode2ascii.py.

Voir également le billet de Peter Bengtsson : Unicode strings to ASCII... nicely.

[modifier] Base de données

  • MySQL
    • db = MySQLdb.connect(..., use_unicode="True", charset="utf8")
    • Une table MySQL a un jeu de caractères : « CREATE TABLE .... [DEFAULT] CHARACTER SET charset_name [COLLATE collation_name] » (depuis MySQL 4.1)
    • MySQL n'aime pas "utf-8", utiliser utf8
    • Lire le chapitre Chapitre 10. Jeux de caractères et Unicode de la documentation MySQL
  • PostgreSQL
  • LDAP (python-ldap)
    • LDAP parle UTF-8 depuis la version 3 (RFC 4511)
    • en février 2011, python-ldap ne supportait pas encore le type unicode de Python2 pour consever la compatibilité LDAP version 2 qui exige un encodage explicite

[modifier] Texte encodé deux fois en UTF-8

Fonction pour convertir du texte encodé deux fois en UTF-8 :

def decodeUtf8Utf8(octets):
  return octets.decode("utf8").encode("latin1").decode("utf8")

Exemple d'utilisation :

>>> print decodeUtf8Utf8('\xc3\x83\xc2\xa9')
é

Si vous avez une chaîne unicode :

>>> print decodeUtf8Utf8(u'\xc3\x83\xc2\xa9'.encode("latin1"))
é

[modifier] Python3

[modifier] OS encoding(s)

OS data:

  • command line arguments
  • environment variables
  • standard input, output and error
  • filenames

Mac OS X:

  • filenames: UTF-8
  • command line arguments:
    • locale encoding for Python < XXX
    • UTF-8 for Python > XXX (changeset 45079ad1e260, issue #4388)

Windows:

  • filenames: MBCS (ANSI code page)

UNIX:

  • filenames: locale encoding, setlocale(LC_CTYPE, ""); encoding = nl_langinfo(CODESET)

[modifier] En pratique avec Python3

[modifier] Jeu de caractères par défaut

  • Exemple d'utilisation : b"abc".decode() et "abc".encode()
  • sys.getdefaultencoding() == 'utf8'
  • Bien qu'il existe sys.setdefaultencoding(), on ne peut pas modifier le jeu de caractères par défaut (un jeu de caractères autre que "utf8" émet une erreur)

[modifier] Noms de fichiers et variables d'environnement

  • Jeu de caractères sys.getdefaultencoding(), "surrogateescape" error handler
  • sys.getfilesystemencoding()
    • Windows : "mbcs"
    • Mac OS X : "utf-8"
    • Autre : nl_langinfo(CODESET), ou "utf8" si nl_langinfo() échoue ou n'existe pas
  • (sys.setfilesystemencoding() : fonction supprimée par le module site)

Pour les variables d'environnement, Python 3.2 a en plus os.environb et os.getenvb() pour récupérer les variables dans le type bytes (avant qu'elles soient décodées).

[modifier] TextIOWrapper, stdin, stdout et stderr

os.device_encoding(fd) :

  • None is fd n'est pas un TTY
  • Windows : GetConsoleCP() si fd==0, GetConsoleOutputCP() si fd in (1, 2), None sinon
  • POSIX : nl_langinfo(CODESET) (sans modifier la locale)

Choix du jeu de caractères par io.TextIOWrapper() si l'encodage n'est pas spécifié :

  • os.device_encoding(buffer.fileno())
  • ou locale.getpreferredencoding()
  • ou "ascii"
  • Note : utilise errors="strict" si errors n'est pas spécifié

sys.stdin, sys.stdout, sys.stderr :

[modifier] Arguments de la ligne de commande

L'algorithme ressemble à ça :

# argv est une liste de bytes
encoding = locale.getpreferredencoding()
sys.argv = [arg.decode(encoding, 'surrogateescape') for arg in argv]

Les arguments sont décodés selon le jeu de caractères de la locale, et les caractères ne pouvant être décodés sont stockés sous forme de surrogates.

char** argv est converti en wchar_t** par _Py_char2wchar() qui utilise mbstowcs() ou mbrtowc()+surrogates), puis en liste de chaînes unicode par PySys_SetArgv() qui utilise PyUnicode_FromWideChar().

Pour récupérer sys.argv sous forme d'octets, utilisez :

encoding = locale.getpreferredencoding()
argv_bytes = [arg.encode(encoding, "surrogateescape") for arg in sys.argv]

[modifier] Fonctions du module locale

locale.getpreferredencoding() :

  • Windows : _locale._getdefaultlocale()[1]
  • POSIX avec CODESET : modifie la locale avec setlocale(LC_TYPE, "") (sauf si l'argument do_setlocale est défini à False) puis appelle nl_langinfo(CODESET). Sous Mac OS X, renvoie "utf-8" le résultat est None
  • POSIX sans CODESET : getdefaultlocale()[1], ou "ascii" si le résultat est None

Lecture du jeu de caractères avec locale._getdefaultlocale() :

  • _locale._getdefaultlocale() si la fonction existe
  • Sinon cherche la première variable d'environnement définie parmis LC_ALL, LC_CTYPE, LANG, LANGUAGE : utilise "C" si aucune n'est définie. Puis devine le jeu de caractères à partir de la locale :
    • Si la locale contient un point : ce qui est écrit après le point ("fr_FR.utf8" => UTF-8)
    • ISO-8859-15 si la locale se termine par "..."@euro"
    • None sinon

[modifier] En pratique avec Python2

[modifier] Jeu de caractères par défaut

  • Exemple d'utilisation : "abc".decode() et u"abc".encode()
  • sys.getdefaultencoding() == 'ascii' par défaut, mais c'est modifiable avec sys.setdefaultencoding() (bien que très déconseillé : nombreux effets de bord !)

[modifier] Noms de fichiers et variables d'environnement

  • Jeu de caractères sys.getdefaultencoding() (mode strict)
  • sys.getfilesystemencoding()
    • Windows : "mbcs"
    • Mac OS X : "utf-8"
    • Autre : nl_langinfo(CODESET), ou "utf8" si nl_langinfo() échoue ou n'existe pas

[modifier] io.TextIOWrapper

Choix du jeu de caractères par io.TextIOWrapper() si l'encodage n'est pas spécifié :

  • locale.getpreferredencoding()
  • ou "ascii"

[modifier] Fonctions du module locale

locale.getpreferredencoding() :

  • Windows : _locale._getdefaultlocale()[1]
  • POSIX avec CODESET : modifie la locale avec setlocale(LC_TYPE, "") (sauf si l'argument do_setlocale est défini à False) puis appelle nl_langinfo(CODESET)
  • POSIX sans CODESET : getdefaultlocale()[1]

[modifier] En pratique avec Python3 (code source)

a :

  • PyUnicode_GetDefaultEncoding(), PyUnicode_SetDefaultEncoding
    • En Python : sys.getdefaultencoding()
    • PyUnicode_GetDefaultEncoding() est utilisé quand encoding==NULL, exemple Python : b"abc".decode()
    • Python3 : valeur utf8, non modifiable
  • sys.getfilesystemencoding()
    • Python/pythonrun.c : Py_InitializeEx() initialise Py_FileSystemDefaultEncoding avec nl_langinfo(CODESET)
    • Python/bltinmodule.c : Py_FileSystemDefaultEncoding
    • Défaut : "mbcs" sous Windows", "utf8" sous Mac OS X, None sinon

b :

  • os.environ
    • Modules/posixmodule.c : convertenviron()
    • Windows : PyUnicode_FromWideChar()
    • POSIX : PyUnicode_Decode(..., Py_FileSystemDefaultEncoding, "surrogateescape")

c :

  • locale.getpreferredencoding()
    • Windows : _locale._getdefaultlocale()[1]
    • POSIX avec CODESET : setlocale(LC_TYPE, ""); nl_langinfo(CODESET); setlocale(LC_TYPE, oldloc) ou "utf8"
    • POSIX sans CODESET : getdefaultlocale()[1] ou "ascii", parse les variables d'environnement
  • os.device_encoding(buffer.fileno())
    • Windows : GetConsoleCP() si fd==0, GetConsoleOutputCP() si fd in (1, 2), None sinon
    • POSIX : nl_langinfo(CODESET)
  • io.TextIOWrapper() si encoding==None :
    • os.device_encoding(buffer.fileno())
    • ou locale.getpreferredencoding()
    • ou "ascii"

d :

  • sys.stdin, sys.stdout, sys.stderr
    • sys.stdin.encoding, sys.stdout.encoding, sys.stderr.encoding
    • Python/pythonrun.c : initstdio()
    • Variable d'environement PYTHONIOENCODING : PYTHONIOENCODING=utf8 ou PYTHONIOENCODING=utf8:backslashreplace
    • Appelle io.TextIOWrapper(..., encoding, errors, ...)

Python 3.2 sous Linux :

>>> locale.getpreferredencoding()
'UTF-8'
>>> sys.getdefaultencoding()
'utf-8'
>>> sys.getfilesystemencoding()
'utf-8'
>>> sys.stdout.encoding
'UTF-8'

Python 3.2 sous Linux avec LANG= :

>>> locale.getpreferredencoding()
'ANSI_X3.4-1968'
>>> sys.getdefaultencoding()
'utf-8'
>>> sys.getfilesystemencoding()
'ascii'
>>> sys.stdout.encoding
'ANSI_X3.4-1968'

[modifier] wchar_t

  • Debian Sid sur x86 (32 bits): sizeof(wchar_t) == 4
  • Sous Windows, sizeof(wchar_t) == 2 : utilise UTF-16

[modifier] Voir aussi

[modifier] Lire aussi

[modifier] Article connexes