Python - comprendre l'Unicode, grâce à Socrate

Socratis

Les nombreux grecs (dont Socrate ci-dessus ;-) ) que vous avez fréquentés pendant vos études ont bourré, sans vous le demander, votre disque dur de milliers de fichiers MP3 et Vorbis, depuis les veilles musiques traditionnelles jusqu'aux derniers trucs pourris à la mode. Le premier problème est que vous n'avez aucun souvenir de la façon dont cela s'est passé. Ce phénomène doit pouvoir s'expliquer par un excès d'ούζο. Le deuxième problème est que certains de ces fichiers ont plus de dix ans, donc proviennent d'une époque où chaque langue avait son propre jeu de caractères. Tous ces fichiers ont donc un nom illisible. Non pas parce qu'ils sont écrits en grec, mais parce qu'ils sont encodés dans le jeu de caractère grec : iso8859-7. Pour résumer, vos fichiers s'appellent « ??????? » au lieu de s'appeler « συρτάκι ».

Qu'est-ce qu'un jeu de caractères ?

Un ordinateur ne stocke que des nombres. Pour qu'un symbole linguistique ou mathématique par exemple « Alpha » puisse avoir la moindre existence informatique, il faut le convertir en nombre. Ainsi, un jeu de caractère n'est qu'une table associative donnant la correspondance entre les symboles linguistiques et les nombres informatiques.

Dans le jeu de caractères ASCII, l'un des plus anciens (1961), le nombre en question n'occupe que 7 bits, donc on ne peut représenter que 128 caractères différents, ceux de la langue anglaise ! Pour travailler avec d'autres langues il faut utiliser d'autres jeux de caractères, par exemple iso8859-1 pour les langues accentuées ouest-européennes ou iso8859-9 pour le turc, qui sont sur 8 bits et permettent de représenter deux fois plus de caractères que l'ASCII. L'inconvenient est que le même nombre, par exemple 233, peut correspondre à « θ » en grec ou « è » en français. Pas facile dans ces conditions d'échanger des données, ni de mélanger les langues...  La solution est d'utiliser un jeu de caractère universel comme l'UCS qui fait partie du standard Unicode, et est capable de représenter de manière unique tous les symboles de toutes les langues et de toutes les époques. Ainsi, en Unicode, le caractère grec Alpha correspond au nombre 945, qu'on écrit de manière normalisée et en hexadécimal : U+03B1.  Ce nombre peut ensuite être stocké dans un ordinateur de différentes manières : la plus courante est le codage UTF-8, qui utilise de 1 à 4 octets, ce qui lui permet d'être compatible avec l'ASCII.  En Python 3, les chaînes de caractères (str) sont des objets Unicode par défaut. Leur représentation interne peut être sur 16 bits (UCS2) ou 32 bits (UCS4). Vous ne devez vous préoccuper de l'encodage que lorsque vous échangez des données avec le monde extérieur, par exemple le disque ou le réseau.

💡
Vous avez des questions sur Python ? On peut en discuter librement quand vous voulez ! Prenez rendez-vous avec le bouton juste en dessous ↓

Et notre musique ??

Revenons à notre συρτάκι. Il y a toutes les chances pour que vos noms de fichiers grecs, qui ne sont que des chaînes d'octets écrits sur votre disque dur, soient encodés en iso8859-7. Si jamais votre système est configuré pour afficher ces noms de fichiers en utilisant autre chose que iso8859-7, vous ne verrez probablement que des symboles incohérents. Vous pouvez changer cette configuration, mais dans ce cas, ce sont vos noms de fichiers accentués en français qui deviendront illisibles.

La seule solution est de renommer tous vos fichiers en UTF-8 !

Comment ?

Plutôt que de deviner vous-même les lettres grecques et de renommer les fichiers à la main un par un, nous pouvons écrire un petit script Python qui fera le boulot à notre place. Attention ce programme n'est compatible qu'avec Python 3.2 !

#!/usr/bin/env python
# coding: utf-8

import os, sys, pdb
from argparse import ArgumentParser
from os.path import join, isdir, isfile, dirname, basename

# analyse des arguments avec argparse (Python2.7+ ou 3.2+)
parser = ArgumentParser(description='Renommage de fichiers vers utf-8')
parser.add_argument('--renomme', dest='renomme', action='store_true',
                    help='Renomme pour de vrai')
parser.add_argument('-r', dest='recursif', action='store_true',
                    help='Mode recursif')
parser.add_argument('-c', dest='encodage', action='store',
                    default='iso8859-1',
                    help="Encodage d'origine des fichiers")
parser.add_argument('fichiers', metavar='fichier', nargs='+',
                    help='Noms de fichiers à renommer')
args = parser.parse_args()

# liste des fichiers à renommer
fichiers = []
encodage_systeme = sys.getfilesystemencoding()
for arg in args.fichiers:
    fichiers.append(arg.encode(encodage_systeme, 'surrogateescape'))

def renomme(racine, nom_fichier):
    """fonction qui renomme
    """
    ancien_nom = join(racine, nom_fichier)
    nouveau_nom = ancien_nom.decode(args.encodage).encode(encodage_systeme)
    if ancien_nom != nouveau_nom:
        print("%s => %s" % (ancien_nom, nouveau_nom.decode(encodage_systeme)))
        if args.renomme:
            os.rename(ancien_nom, nouveau_nom)

# lancement du traitement
for fichier in fichiers:
    if isdir(fichier) and args.recursif:
        for racine, dossiers, fichiers in os.walk(fichier, topdown=False):
            for element in fichiers + dossiers:
                renomme(racine, element)
    elif isfile(fichier) or isdir(fichier) and not args.recursif:
        renomme(dirname(fichier), basename(fichier))

Dans ce programme on note l'utilisation du module argparse. Ce module a été introduit dans Python 2.7 et 3.2 et remplace optparse. Il permet de gérer et valider facilement des arguments passés sur la ligne de commande : on crée un parser (un analyseur), puis on lui indique les arguments souhaités. On retrouve ensuite ces arguments comme attributs de la variable args. Par exemple pour --renomme, l'association de dest='renomme' et action='store_true permet de stocker la valeur True dans args.renomme. De même, dest='encodage' et action='store' permet de stocker l'encodage fourni dans args.encodage.

Encodage ? Décodage ?

Puisque la suite du programme consiste à encoder ou décoder des caractères, rappelons en quoi consistent ces deux notions. Les jeux de caractères (ASCII, ISO8859, UTF-8) sont une façon d'encoder les symboles linguistiques sous forme de chaîne d'octets. « Encoder » signifie donc transformer un symbole Unicode en chaîne d'octets, et « décoder » consiste à interpréter une chaîne d'octet pour la transformer en objet Unicode. L'objet Python Unicode sert à représenter le symbole linguistique alors que la chaîne d'octets contient sa représentation informatique.  (En réalité, l'objet Python Unicode est à fortiori lui aussi représenté en interne sous forme d'octets, mais vous n'avez pas à vous en soucier puisque Python prend en charge nativement l'Unicode, contrairement à PHP.)

On peut résumer ceci dans le schéma suivant :

Le reste des arguments de la ligne de commande se retrouve dans notre attribut args.fichiers. Une particularité de Python 3 est que ces arguments sont automatiquement décodés en Unicode en utilisant l'encodage du système (par ex utf-8). Dans notre cas le décodage est normalement impossible car cela revient à considérer de l'iso8859-7 comme de l'utf-8. Néanmoins pour éviter les erreurs à la lecture, Python applique une gestion automatique des erreurs de décodage appelée « surrogateescape » qui revient à transformer les caractères indécodables en caractères de substitution (des « surrogates »). Il faut donc d'abord réencoder ces arguments dans leur état d'origine, en utilisant la même option surrogateescape dans l'autre sens, pour rétablir les octets initiaux depuis les caractères de substitution. C'est un cas un peu tordu et heureusement assez rare. Pour résumer, Python s'attend donc à ce que vos fichiers soient encodés correctement, mais ne fait pas d'erreur s'ils ne le sont pas. C'est à vous de traiter ce cas particulier. Vous trouverez plus d'explications sur les « surrogates » dans le PEP 383.

Nous avons ensuite une fonction de renommage, qui effectue le transcodage et le renommage, et qui est utilisé lors du parcours de l'arborescence. Ce parcours se fait grâce à la fonction os.walk qui évite d'avoir à écrire une fonction récursive. L'option « topdownFalse » inverse l'ordre de génération de la liste des répertoires pour commencer par les plus profonds, en remontant jusqu'aux dossiers proches de la racine. Sinon vous serez interrompus en plein milieu par une erreur Fichier non trouvé.

Conclusion

La gestion de l'Unicode dans Python 3 est plus cohérente et plus facile à comprendre que dans Python 2. Elle devrait éviter de nombreuses erreurs et incompréhensions. Par défaut une chaîne de caractères (str) est toujours en unicode.  Et si vous ne travaillez pas avec des chaînes de caractère Unicode, vous pouvez travailler avec des chaînes d'octets (bytes). Il y a toutefois un changement de vocabulaire auquel il faudra faire attention :

Changement sur les types :

Python 2 Python 3
unicode str
str bytes