Unicode en python3 et script pour corriger les problèmes d’encoding de noms de fichiers

La gestion de l’unicode a toujours été la galère en programmation, et notamment en python. Il y a une vingtaine d’année avec les Windows-1252 ou latin1, au pire on se retrouvait un affichage bizarre mais cela marchait toujours. Avec la généralisation d’UTF8, maintenant standard partout que ce soit pour l’affichage sur les consoles ou les systèmes de fichiers la donne a changé, et les scripts python se retrouvent souvent à planter à cause d’un problème d’encoding, et les messages comme celui ci-dessous sont devenus l’enfer du développeur python, surtout avec python2 :

C’était devenu tellement complexe avec les conversions explicites ou implicites qu’on se retrouvait alors à tâtonner en ajoutant des .encode  et des .decode  un peu dans tous les sens jusqu’à ce que ça tombe en marche… Pas terrible…

Trucs rapides pour maîtriser les encodings en python3

Python3 rationalise ça et apporte une solution bien plus efficace en séparant proprement les chaines de caractères unicode (str) des suites d’octets (bytes), et des fonctions d’encodage / décodage pour passer de l’une à l’autre : str.encode -> bytes, et bytes.decode -> str.  Pour faire la différence sur les chaînes littérales, il faut utiliser b” devant les bytes et sinon python3 interprétera en str, en décodant depuis l’encoding de votre fichier source, qu’il faut donc penser à indiquer avec “# coding: ” en début de script.

En utilisation normale python3 va donc parfaitement se comporter, faisant implicitement pour votre compte les traductions unicode -> encoding lorsque cela est nécessaire, par exemple pour ‘print’, pour l’ouverture d’un fichier en mode texte, etc.

Malheureusement, si le contenu que vous traitez comporte des erreurs, python3 est toujours aussi intolérant et vous retrouverez les exeptions UnicodeEncodeError & co…  Comme la distinction str / bytes est maintenant bien marquée il n’est plus aussi simple qu’avant que de l’embrouiller… Heureusement il y a cependant quelques astuces pour ce faire.  Votre bible est la section python specific encodings de la page 7.2 codecs de la documentation python3. Vous y trouverez des codecs particuliers pour faire des choses pas très catholiques, et un peu plus haut dans la page, la liste des errors handlers possibles.

En gros à chaque endroit où vous pouvez specifier un paramètre encoding il existe également un autre paramètre errors qui va spécifier le comportement du codec en cas d’erreur d’encoding. Si vous ne spécifiez rien, cela correspond à ‘ strict ‘ et la fameuse exception sera levée. Dans la liste possible, ‘ ignore ‘ est sans doute le plus simple : le caractère fautif est simplement ignoré. Vous pouvez redéfinir les options de stdout pour spécifier le comportement de print et ne plus avoir de plantage liés à des print de debugging…

Si vous n’avez pas besoin d’un comportement exact c’est sans doute le plus simple, sinon il faut regarder de plus près les autres, qui vont tous substituer le caractère fautif en une autre forme acceptée (le cas des *replace), ou temporairement tolérée (le cas de surrogateescape). Le problème se pose alors si vous avez besoin de retrouver la chaine de caractère originale avec son problème d’encoding (c’est le cas des fichiers qu’on veut renommer). Pour sa gestion interne des systèmes de fichiers python3 semble avoir adopté la gestion du surrogateescape ; ainsi un os.listdir ne retournera pas d’exception si un nom de fichier ne suit pas l’encoding du filesystem mais la chaine de caractère unicode comportera des caractères ‘surrogate’. L’avantage sur surrogateescape est qu’il est simple de retrouver la forme d’origine, il suffit d’encoder en ajoutant le paramètre errors="surrogateescape" ; l’inconvénient est que par défaut tout codec renverra une erreur sur les caractères surrogate et qu’il faut donc spécifier systématiquement un comportement. Il existe “ surrogatepass ” pour les ignorer. Donc par exemple pour afficher sur la console des chaines potentiellement avec surrogate :

Le plus confortable pour ne pas craindre à chaque instant une exception est donc d’utiliser un des modes ‘*replace’ qui va remplacer le caractère fautif par une séquence d’échappement acceptée. Il n’est cependant pas toujours facile de revenir à la version initiale. Après avoir galéré un certain temps avec ‘backslashreplace’, j’ai trouvé dans ce fil stackoverflow une méthode simple pour le faire, qui se base en partie sur le codec unicode-escape, et en partie sur la capacité du codec latin1 de convertir 100% des bytes en string et inversement pour pallier le fonctionnement de unicode-escape uniquement sur des bytes. Pour inverser un ‘backslashreplace’, la fonction est donc :

 

Assainir les noms de fichiers de son filesystem

Cette longue introduction pour présenter une application directe de ces éléments sur le cas qui m’intéresse, à savoir pouvoir faire en sorte que mon filesystem ne comporte que des noms de fichiers valides en UTF8. En effet, au fil de plus de 20 ans, entre le passage initial à utf8, les copies depuis des Windows plus ou moins au fait d’utf8, la décompression de vieux fichiers zip mal encodés, j’avais plus de 1000 noms de fichiers dont l’encoding n’était pas correct. Ça ne me gênait pas outre mesure jusque là, mais en voulant mettre en place un backup via rclone, ce dernier râle copieusement lorsque le nom de fichier n’est pas conforme… Pas facile à identifier et corriger à la main, donc j’ai fait un script.

Si identifier les fichiers fautifs est assez facile, c’est plus compliqué de deviner le bon encoding d’origine. Il existe un très bon outil chardet qui permet d’identifier l’encoding d’un texte inconnu. Malheureusement il ne supporte que quelques encodings et pas tous ceux que j’avais sur mon disque, c’est à dire à peu près tous ceux utilisés pour du français : latin1/15 le classique européen, cp1252 la déclinaison par Windows, et cp850, l’ancienne version sous DOS/Windows. Ce dernier n’est pas supporté par chardet (et l’ajout d’un nouvel encoding ne semble pas hyper simple dans cet outil). Il me fallait donc trouver un autre moyen. J’ai fait sale, mais ça marche pas si mal 🙂 : j’ai basiquement fait une liste des caractères qui peuvent légitimement se retrouver dans mes noms de fichiers. Si un encoding me sort un nom de fichier qui n’utilise que ces caractères il est alors sans doute correct, sinon il est sans doute faux. Je fais le test sur la liste de ceux que je suis susceptible de rencontrer, et si plusieurs matchent, je privilégie celui trouvé par chardet qui est un peu plus intelligent que ce que j’ai fait…  Si aucun ne correspond, je sélectionne celui qui comporte le moins de caractères en écart et sort une alerte pour l’utilisateur.

Pour les besoins de la mise au point de ce script, plutôt que de scanner à chaque fois mon disque j’ai préféré travailler sur un fichier de l’ensemble de la liste des fichiers, basiquement produit par find / .  Si c’est super simple pour détecter les fichiers fautifs, je me suis ensuite rendu compte qu’une structure à plat serait compliquée à traiter pour l’étape de renommage. J’ai donc opté pour une conversion du fichier à plat en une structure arborescente, qui serait plus facile à construire directement depuis le disque que depuis la liste des fichiers à plat.

Enfin j’aurais souhaité initialement pouvoir générer un script shell avec la liste des commandes mv plutôt que de renommer les fichiers en python (ce que je fais d’habitude et qui est plus pratique pour tester et plus sécurisant). Malheureusement la fonction shlex.quote  pour transformer une chaîne de caractère en argument pour le shell ne semble pas se comporter à l’identique du shell sur les noms de fichiers avec un mauvais encoding, il était donc plus simple de renommer en python directement plutôt que de devoir implémenter l’échappement correct.

Voici donc le code résultant :

A noter que ce script est quand même assez expérimental / rudimentaire, et à ne pas utiliser aveuglement…

Il existe un script convmv disponible dans toutes les bonnes distributions qui permet de convertir assez simplement d’un encoding en utf8. Pour les cas simples, c’est LE script à regarder en premier. J’en avais fait il y a longtemps, à l’occasion de mon premier cleanup, une variante qui utilise le module Encode::Guess : convmv-detect (script perl à télécharger et dézipper). L’usage est identique à convmv, il suffit d’indiquer “-f detect” pour demander la détection et changer en ligne 449 la liste des encodings “suspects” (oui c’est moche : c’est du perl et je suis nul en perl…)

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.