Qu'est-ce qu'Unicode ?

Rapidement: Les ordinateurs travaillent sur des nombres. Unicode est une table de correspondance entre des valeurs numériques et des caractères, et ce pour un large éventail de systèmes d'écritures et de langues.

Pour plus de détails sur l'historique d'Unicode, sur les encodages qui existaient auparavant (et qui d'ailleurs existent toujours, pour nous compliquer la vie), voir sur le Wikipedia francophone. Entre autres, vous pouvez aller voir bien sûr Unicode, mais aussi ASCII, latin1, ou encore EBCDIC. Vous pouvez aussi lire la page de Jacques Poitou sur le sujet: L'ordinateur face à la diversité des langues.

Traduction en français du texte de joelonsoftware: http://local.joelonsoftware.com/mediawiki/index.php/Le_minimum_absolu_que_tout_d%C3%A9veloppeur_doit_absolument_savoir_sur_Unicode_et_les_jeux_de_caract%C3%A8res

Tout cela vous donnera une idée de l'existant avec lequel il faut faire.

Liens

Le site institutionnel Unicode: http://www.unicode.org/

Sur ce même site, conservez une référence vers les Unicode Character Code Charts (codes, noms des caractères, relations avec d'autres caractères, etc).

Une grande introduction à Unicode, présentée à l'EuroPython 2002, est disponible sous forme PDF (en anglais).

Un tutoriel (en anglais aussi) est disponible chez ReportLab: Python Unicode Tutorial.

Les pages unicode pour les programmeurs (en anglais encore) par Jason Orendorff: Unicode for Programmers et la page spécifique Python.

La page sur Unicode et Python sur effbot.org (toujours en anglais): Python Unicode Objects.

La page sur l'utilisation d'utf8 avec Python par Evan Jones (en... anglais): How to Use UTF-8 with Python.

La liste non-exaustive des encodages supportés par les bibliothèques standard de Python est disponible dans la documentation: Standard Encodings.

Unicode et Python

Type Unicode

A côté du simple type str il existe en Python un type unicode. Celui-ci code chaque caractère Unicode sur deux ou quatre octets, suivant une option de compilation(*).

La valeur maximale d'un code Unicode pour votre installation de Python est accessible dans sys.maxunicode.

(*) Attention à ce que les modules d'extension compilés que vous utilisez aient été compilés avec la même taille de caractère Unicode que l'interpréteur qui les charge. Sinon, s'ils ont à manipuler des chaînes Unicode, vous allez droit au crash Python.

Encodage d'un fichier source

Python supporte des fichiers sources dans différents encodages. Il considère par défaut que les fichiers utilisent un encodage ASCII (plus précisément sys.getdefaultencoding()(*). Pour éviter tout problème, il est très fortement recommandé d'indiquer en première ou seconde ligne de tout fichier source une directive d'encodage de la forme:

# -*- coding: nomencodage -*-

Ainsi Python connaît dès le début du fichier l'encodage utilisé.

On retrouve généralement les noms d'encodages suivant:

Cette notation a été définie dans le PEP263, [http://www.python.org/peps/pep-0263.html Defining Python Source Code Encodings].

(*) Il est très fortement recommandé de laisser l'encodage par défaut à ASCII, il s'agit en effet d'une propriété globale, et si chacun commence à l'adapter à sa sauce...

Configuration de la console

Ou encore, adapter Python à la console. Le but ici est de fixer un encodage qui soit utilisé par Python lors de ses entrées sorties en accord avec ce que la console accepte.

Pour commencer, il est possible de connaître l'encodage utilisé sur les flots d'entrée-sortie (utilisés par input/raw_input et par print) en regardant l'attribut encoding des fichiers correspondants.

>>> import sys
>>> sys.stdout.encoding
'ISO-8859-15'
>>> sys.stdin.encoding
'ISO-8859-15'

Python s'est informé auprès de la console de l'encodage utilisé, et il s'y est adapté. Reste donc à savoir comment modifier l'encodage des caractères sur la console.

Sous Windows, la console (ou boîte DOS) peut être paramétrée via la commande chcp (change code-page).

C:\>chcp 1252
Active code page: 1252

(a terminer - config console sous Unix/Linux)

Encodage de chaînes

Si vous avez indiqué correctement l'encodage de votre fichier, Python est capable de traiter correctement les chaînes lors de la phase de compilation.

Mais si vous déclarez simplement la chaîne sous la forme "Bébête chaîne", il crée un objet de type str, qui ne contient pas d'information spécifique d'encodage, bref une simple séquence d'octets. Au moment de l'exécution, lorsqu'il a besoin de convertir cette chaîne str dans un encodage particulier (par exemple en windows-1252 ou en dos CP437 pour afficher sur la console), n'ayant plus l'information d'encodage issue du fichier, Python considère que la chaîne utilise l'encodage sys.getdefaultencoding() donc ASCII. Et lorsque l'on utilise des codes > 127 pour les accents, Python lève une exception car il ne connaît pas ces codes dans l'encodage ASCII: UnicodeError: ASCII encoding error: ordinal not in range(128).

Pour avoir explicitement une chaîne avec des accents et être capable de la manipuler sans surprise sur divers plateformes et dans divers environnements, il est fortement conseillé de déclarer directement cette chaîne comme chaîne Unicode, sous la forme u"Une chaîne Unicode". Cela indique à Python de créer non pas un objet chaîne str mais un objet chaîne unicode. Connaissant l'encodage de la chaîne dans le fichier source, lors de la phase de compilation Python la convertie en chaîne Unicode - avec les accents correctement interprétés et stockés.

Caractères Unicode

Si l'encodage de votre fichier source ne permet pas de représenter certains caractères Unicode dont vous avez besoin, vous pouvez utiliser dans les chaînes la syntaxe:

   1 u"Mon \u0153vre"

qui permet une séquence d'échappement \u suivie du code hexadécimal du caractère unicode sur quatre digits.

Vous pouvez aussi utiliser la syntaxe:

   1 u"Mon \N{LATIN SMALL LIGATURE OE}uvre"

qui permet une séquence d'échappement \N suivie du nom du caractère unicode entre accolades (a priori seuls les noms principaux sont autorisés, pas les alias).

Enfin il est possible d'utiliser la fonction unichr (fonction parallèle à chr mais pour Unicode), qui pour une valeur numérique donnée retourne le caractère Unicode ayant cette valeur pour code.

Pour les codes et noms des caractères: Unicode Character Code Charts.

Changement d'encodage

Les objets de type str ont une méthode decode qui permet d'effectuer un traitement de décodage sur le flot d'octets contenu dans la chaîne, ainsi qu'une méthode encode qui réalise l'opération inverse.

Outre une indication d'encodage d'origine ou de destination, ces méthodes contiennent aussi un paramètre error permettant de spécifier comment doivent être traitées les erreurs de conversion:

Encodage X vers Unicode

La méthode decode, utilisée sur un objet de type str, avec un encodage de caractères x, retourne un objet unicode représentant la même chaîne, mais avec des caractères Unicode correspondant aux caractères de l'encodage x.

   1 "Bonjour à tous".decode("latin1","replace")

Il est aussi possible de contruire un objet de type unicode en lui donnant directement à la construction le flot d'octets à utiliser et l'encodage dans lequel est exprimé ce flot d'octets.

   1 s = Unicode("Bonjour à tous","latin1")

Unicode vers encodage X

La méthode encode, utilisée sur un objet de type unicode, avec un encodage de caractères x, retourne un objet str représentant la même chaîne, mais avec des caractères de l'encodage x correspondant aux caractères Unicode.

   1 u"Tempête sur Java".encode("latin1","replace")

Autres encodages

Les objets str de Python sont des conteneurs pour n'importe quel séquence d'octets, ils peuvent contenir des chaînes mais pas uniquement.

Les méthodes encode et decode permettent de travailler aussi sur des données binaires stockées dans des str en utilisant des codecs ad-hoc, par exemple des données compressées (encodage "zip").

>>> s = "Bonjour a tous.".encode("zip")
>>> s
'x\x9cs\xca\xcf\xcb\xca/-RHT(\xc9/-\xd6\x03\x00,\xc0\x05z'
>>> s.decode("zip")
'Bonjour a tous.'

Note: Il est aussi possible d'appeler decode sur une chaîne Unicode avec l'encodage xml... mais je n'ai pas plus de détail (travail sur les entités, sur les références nommées...?). Si quelqu'un connaît il peut compléter.

Expressions Régulières

Outils

Lire un fichier texte

Module codecs et fonction open de ce module, pour spécifier directement l'encodage d'un fichier lors de son ouverture et avoir la conversion de/vers cet encodage à la volée lors des lecture/écritures.

Sinon, il faut le faire à la main avec les méthodes encode et decode indiquées précédemment.

Supprimer les accents

On a souvent besoin d'une version "simple" d'une chaîne de caractères, une version d'où les marques diacritiques (accents et autres petites marques ajoutées aux caractères) seraient absentes. Par exemple pour pouvoir faire un tri, ou encore pour avoir une version facilement comparable même si l'utilisateur oublie de resaisir le caractère accentué...

Avec Unicode le nombre de caractères possibles est très très important. Et même en se limitant aux caractères latins, le nombre de variations avec des marques diacritiques est encore étonnant (regardez les tables Unicode pour avoir une idée).

Voici une petite fonction qui pourra rendre des service pour désaccentuer des chaînes:

   1 _reptable = {}  
   2 def _fill_reptable():
   3     _corresp = [
   4         (u"A",  [0x00C0,0x00C1,0x00C2,0x00C3,0x00C4,0x00C5,0x0100,0x0102,0x0104]),
   5         (u"AE", [0x00C6]),
   6         (u"a",  [0x00E0,0x00E1,0x00E2,0x00E3,0x00E4,0x00E5,0x0101,0x0103,0x0105]),
   7         (u"ae", [0x00E6]),
   8         (u"C",  [0x00C7,0x0106,0x0108,0x010A,0x010C]),
   9         (u"c",  [0x00E7,0x0107,0x0109,0x010B,0x010D]),
  10         (u"D",  [0x00D0,0x010E,0x0110]),
  11         (u"d",  [0x00F0,0x010F,0x0111]),
  12         (u"E",  [0x00C8,0x00C9,0x00CA,0x00CB,0x0112,0x0114,0x0116,0x0118,0x011A]),
  13         (u"e",  [0x00E8,0x00E9,0x00EA,0x00EB,0x0113,0x0115,0x0117,0x0119,0x011B]),
  14         (u"G",  [0x011C,0x011E,0x0120,0x0122]),
  15         (u"g",  [0x011D,0x011F,0x0121,0x0123]),
  16         (u"H",  [0x0124,0x0126]),
  17         (u"h",  [0x0125,0x0127]),
  18         (u"I",  [0x00CC,0x00CD,0x00CE,0x00CF,0x0128,0x012A,0x012C,0x012E,0x0130]),
  19         (u"i",  [0x00EC,0x00ED,0x00EE,0x00EF,0x0129,0x012B,0x012D,0x012F,0x0131]),
  20         (u"IJ", [0x0132]),
  21         (u"ij", [0x0133]),
  22         (u"J",  [0x0134]),
  23         (u"j",  [0x0135]),
  24         (u"K",  [0x0136]),
  25         (u"k",  [0x0137,0x0138]),
  26         (u"L",  [0x0139,0x013B,0x013D,0x013F,0x0141]),
  27         (u"l",  [0x013A,0x013C,0x013E,0x0140,0x0142]),
  28         (u"N",  [0x00D1,0x0143,0x0145,0x0147,0x014A]),
  29         (u"n",  [0x00F1,0x0144,0x0146,0x0148,0x0149,0x014B]),
  30         (u"O",  [0x00D2,0x00D3,0x00D4,0x00D5,0x00D6,0x00D8,0x014C,0x014E,0x0150]),
  31         (u"o",  [0x00F2,0x00F3,0x00F4,0x00F5,0x00F6,0x00F8,0x014D,0x014F,0x0151]),
  32         (u"OE", [0x0152]),
  33         (u"oe", [0x0153]),
  34         (u"R",  [0x0154,0x0156,0x0158]),
  35         (u"r",  [0x0155,0x0157,0x0159]),
  36         (u"S",  [0x015A,0x015C,0x015E,0x0160]),
  37         (u"s",  [0x015B,0x015D,0x015F,0x01610,0x017F]),
  38         (u"T",  [0x0162,0x0164,0x0166]),
  39         (u"t",  [0x0163,0x0165,0x0167]),
  40         (u"U",  [0x00D9,0x00DA,0x00DB,0x00DC,0x0168,0x016A,0x016C,0x016E,0x0170,0x172]),
  41         (u"u",  [0x00F9,0x00FA,0x00FB,0x00FC,0x0169,0x016B,0x016D,0x016F,0x0171]),
  42         (u"W",  [0x0174]),
  43         (u"w",  [0x0175]),
  44         (u"Y",  [0x00DD,0x0176,0x0178]),
  45         (u"y",  [0x00FD,0x00FF,0x0177]),        
  46         (u"Z",  [0x0179,0x017B,0x017D]),
  47         (u"z",  [0x017A,0x017C,0x017E])
  48         ]
  49     global _reptable
  50     for repchar,codes in _corresp :
  51         for code in codes : 
  52             _reptable[code] = repchar
  53 _fill_reptable()
  54 def suppression_diacritics(s) :
  55     """Suppression des accents et autres marques.
  56     
  57     @param s: le texte à nettoyer.
  58     @type s: str ou unicode
  59     @return: le texte nettoyé de ses marques diacritiques.
  60     @rtype: unicode
  61     """
  62     if isinstance(s,str) :
  63         s = unicode(s,"utf8","replace")
  64     res = []
  65     for c in s :
  66         res.append(_reptable.get(ord(c),c))
  67     return u"".join(res)

Vous noterez que la fonction suppression_diacritics retourne une chaîne de caractères Unicode. Vous pouvez la convertir vers d'autres formats en utilisant par exemple: s.encode("latin1","replace")

Une autre fonction sympatique pour faire cela, qui demanderait toutefois un peu de test utilise le module unicodedata :

   1 import unicodedata
   2 
   3 def suppression_diacritics(s):
   4     def remove(char):
   5         deco = unicodedata.decomposition(char)
   6         if deco:
   7            return unichr(int(deco.split()[0],16))     
   8         return char
   9 
  10     return ''.join([remove(a) for a in s])

J'ajoute aussi que dans le cas des deux fonctions, il serait bon de remplacer tous les caracteres 'mauvais' a la fin, pour eviter de se retrouver avec des CJK en plein milieu de la chaine.

   1 import string
   2 
   3 s = ''.join([a for a in s if a.replace(' ','-') in (string.ascii_letters + digits + '_-')])

Il est aussi interessant de savoir que le module unicodedata peut aussi donner le nom des caracteres. Cela peut servir dans certains cas pour transformer des caracteres exotiques en ascii. Prenons l'exemple du japonais, les ideogrammes ont tous une corespondance en ascii.

Il est cependant interessant de savoir ce que l'on fait, car l'unicode souffre de grosses lacunes sur le nommages de certains caracteres. En japonais par exemple, il faut utiliser la correspondance suivante :

(Commentaire par thelvin: Pourquoi le faut-il ? Un japonais, s'il devait transcrire son texte en caractères ascii, le ferait plus probablement comme il tape au clavier pour obtenir ses symboles. Donc avec 'si', 'ti', 'tu' et 'hu'. Unicode est parfaitement logique avec les noms de ces caractères.)

   1 repl = {
   2 'si': 'shi',
   3 'ti': 'chi',
   4 'tu': 'tsu',
   5 'hu': 'fu'
   6 }
   7 
   8 name = repl.get(name,name)

Une façon beaucoup plus simple de désaccentuer un texte :

   1 import unicodedata
   2 
   3 def unaccent(str):
   4         return unicodedata.normalize('NFKD', str).encode('ascii','ignore')

Configurer les éditeurs

Configurer emacs

Deux possibilités : soit par les menus déroulants, soit directement

Voir aussi http://www.gutenberg.eu.org/IMG/pdf/codages-scr.pdf

Déraillements

On a pris toutes les précautions et suivi tous les conseils et même écrit de petits modèles réduits pour voir si on a bien compris. Si le fameux Can't decode survient quand-même, c'est qu'on a importé un module non conforme: il ne suffit pas d'appliquer les règles pour les nouveaux modules, il faut aussi mettre à jour tous les anciens. Bon courage.

Mise à jour de scripts

Lorsque l'on passe un script en unicode il faut ajouter le caractère 'u' devant chaque chaine ayant un caractère non ascii.

Ci dessous un script qui parcourt les répértoires et fait cette manipulation (en demandant confirmation).

Il tien compte de toutes les chaines, même sur plusieurs lignes (avec triple guillemets)

   1 import os
   2 import re
   3 chaines = re.compile(r'(.)(?P<guil>\"{3}|\'{3}|\"|\')(.*?)(?P=guil)',re.DOTALL)
   4 for root, dirs, files in os.walk("."):
   5     for f in files:
   6         if f.endswith('.py'):
   7             pathname = os.path.join(root, f)
   8             transformed = False
   9             fp = open(pathname)
  10             source = fp.read()
  11             fp.close()
  12             new = source
  13             for precedent, guil, ch in chaines.findall(source): 
  14                 if precedent != 'u':
  15                     try:
  16                         ch.encode('ascii')
  17                     except:
  18                         print pathname, precedent+guil+ch+guil
  19                         print ' '*len(pathname), precedent+'u'+guil+ch+guil
  20                         new = new.replace(precedent+guil+ch+guil, precedent+'u'+guil+ch+guil)
  21                         transformed = True
  22             if transformed:
  23                 ans = raw_input("write [Y/n/e/q] ?").upper()
  24                 if ans == 'Q':
  25                     os._exit(-1)
  26                 if ans in ('Y','','E'):
  27                     target = open(pathname, 'w')
  28                     target.write(new)
  29                     target.close()
  30                     if ans == 'E':
  31                         os.system('editor %s' % pathname)

WikiPythonFr: JouerAvecUnicode (last edited 2008-08-18 06:26:48 by william)