Uniformiser l’encodage d’un corpus avec Python

Quelques articles de ce carnet ont déjà été consacrés au traitement de données dans le cadre de la constitution d’un corpus, notamment l’étiquetage morphosyntaxique (pour le chinois, avec les étiqueteurs PyNLPIR et Jieba). Toutefois je n’y avais pas abordé la question de l’encodage des caractères.

Concernant les corpus écrits, l’encodage est un paramètre important.
Il doit :

  • Permettre la reconnaissance de tous les caractères présents dans le corpus .
  • Être le même dans tous les fichiers composant le corpus.
  • Être compatible avec les outils de traitement qui seront utilisés par la suite.

Lorsqu’on construit un corpus textuel, les données ne valident pas forcément ces critères au moment de leur collecte, et il est alors indispensable d’uniformiser l’encodage au plus vite.

Je vais détailler ici l’écriture d’un script Python permettant de convertir automatiquement en utf-8 un ensemble de fichiers, quel(s) que soi(en)t leur(s) encodage(s) d’origine.

Quel usage ?

Voici quelques explications concernant la situation dans laquelle j’ai créé ce script, qui correspond à un cas d’usage possible parmi d’autres.

Il s’agissait de construire un corpus textuel, en chinois, à partir d’un ensemble de pages web collectées par des robots. Le tout représentait quelques dizaines de milliers de fichiers, organisés en arborescence plus ou moins irrégulière et comportant différents encodages.

Il est ici assez évident que l’uniformisation de l’encodage des caractères est une étape obligatoire avant de rassembler le contenu textuel des pages pour constituer le corpus, et qu’au vu du nombre de fichiers il n’est pas envisageable de les traiter un par un.

Le script Python

Le script complet ne sera pas reproduit dans cet article, il est mis à disposition sur Github. Vous pouvez également télécharger des exemples de fichiers d’entrée1 et les fichiers de sortie correspondants.

Le script va donc fonctionner de la façon suivante :

  1. Parcourir le répertoire contenant l’ensemble des fichiers.
  2. Ouvrir chaque fichier (potentiellement) pertinent.
  3. Vérifier l’encodage du fichier pour le convertir si besoin.
  4. Créer une copie du fichier encodée en utf-8.

En sortie, on obtient une copie utf-8 de chaque fichier HTML du répertoire d’origine. Les fichiers de départ, pris en entrée par le script, ne subissent aucune modification.

Pour ce script, il faudra faire les imports suivants : os, chardet et éventuellement re selon la façon dont seront nommés les fichiers de sortie2.

Parcourir les fichiers d’entrée

Le module os va permettre de lancer une boucle pour parcourir le contenu du répertoire en entrée avec os.walk ainsi que de créer le répertoire qui contiendra les fichiers de sortie, si celui-ci n’existe pas encore, avec os.makedirs. J’ai appelé rootdir le répertoire qui contient les fichiers d’entrée, et datadir le répertoire qui reçoit les fichiers de sortie. C’est aussi le moment de récupérer le chemin d’accès du fichier courant (filepath).

rootdir = 'raw_files'

os.makedirs('utf8_files', exist_ok=True)
datadir = 'utf8_files'

for subdir, dirs, files in os.walk(rootdir):
    for file in files:
        filepath = subdir + os.sep + file

En l’état le script parcourt les fichiers et récupère leur chemin d’accès mais n’en lit pas le contenu. Avant ça, j’ai ajouté une étape intermédiaire de filtrage sur la base du type MIME, que je récupère comme ceci3 :

mimetype = os.popen("file --mime-type -b " + filepath).read()

Les étapes de traitement suivantes ne porteront que sur le type de fichier le plus pertinent, en l’occurrence text/html (if mimetype == "text/html"), puisque je m’intéresse ici à des pages HTML.

Décoder

Voici le traitement de l’encodage à proprement parler. Le but est d’obtenir une copie du fichier encodée en utf-8, sans connaître l’encodage de départ.

On va alors commencer par tenter de lire le fichier en utf-8 : si ça fonctionne, on le copie tel quel, et si ça ne fonctionne pas, on sait qu’il faut détecter et convertir l’encodage. Puisqu’on essaie de lire un fichier, et donc de le décoder, l’erreur à récupérer est UnicodeDecodeError. Le code aura cette structure :

try:
    # décoder le fichier en utf-8
    # copier le fichier
except UnicodeDecodeError:
    # détecter l'encodage
    # écrire une copie du fichier encodée en utf-8

Il y a finalement assez peu de choses à dire sur ce qui est à mettre dans try, si ce n’est qu’il ne faut bien sûr pas oublier de préciser que le décodage se fait en utf-8 :

f = open(filepath, "r", 1000, encoding="utf-8").read()

Il suffit ensuite de créer un fichier de sortie utf-8 et y écrire le contenu du fichier d’entrée, stocké dans f.

Réencoder

Les fichiers qui n’ont pas pu être traités dans try soulèvent une erreur UnicodeDecodeError : ils seront traités dans except.

Au moment où survient UnicodeDecodeError, on a une information sur ce que n’est pas l’encodage du fichier courant, mais pas sur ce qu’il est. Ce n’est pas suffisant pour effectuer la conversion vers utf-8, puisque pour pouvoir réencoder sans altérer le contenu il faut d’abord décoder l’encodage d’origine.

Pour détecter l’encodage, il faut lire le fichier en binaire puis utiliser chardet.detect() :

except UnicodeDecodeError:
    f = open(filepath, "rb").read()
    res = chardet.detect(f)
    enc = res['encoding']

Attention, ce que retourne chardet.detect() n’est pas directement le nom du type d’encodage détecté mais un dictionnaire. La clé encoding permet de récupérer le nom de l’encodage.

Il se peut que chardet.detect() ne détecte aucun encodage, encoding aura alors la valeur None. Ces cas devraient être très minoritaires, j’ai décidé de les écarter sans tenter la conversion (le script complet prévoit le compte et le recensement des fichiers concernés).

if enc is not None:
    try:
        fnew = open(copy, "w", encoding="utf-8")
        fnew.write(f.decode(enc, 'ignore'))
        fnew.close()
    except UnicodeDecodeError:
        print(f"Decoding error on {filepath}")
    except UnicodeEncodeError:
        print(f"Encoding error on {filepath}")
    except TypeError:
        print(f"Type error on {filepath}")

Pour obtenir la copie encodée en utf-8, on crée d’abord le fichier de sortie en précisant l’encodage souhaité (fnew = open(copy, "w", encoding="utf-8"), puis on y écrit le contenu du fichier d’entrée, en décodant à partir du type d’encodage qui avait été identifié par chardet.detect(), et qui est ici stocké dans la variable enc (fnew.write(f.decode(enc, 'ignore'))). Pour rappel, la variable f correspond au fichier d’entrée lu en binaire (f = open(filepath, "rb").read()).

Puisque tout ce traitement se déroule dans une boucle qui parcourt un à un les fichiers du répertoire, l’encodage d’origine est détecté à chaque passage, ce qui permet de réencoder correctement en utf-8.

Rappel : Vous pouvez retrouver le script complet (et des fichiers pour le tester) sur Github.

Aperçu du résultat

Parmi les exemples téléchargeables sur Github, il y a un fichier encodé en Shift-JIS. Si on inspecte ses propriétés dans un terminal avec la commande file, on obtient ceci :

Si on utilise cette même commande file sur le fichier de sortie correspondant, converti en utf-8 par le script présenté dans cet article :

D’après ces informations, le traitement du fichier semble s’être déroulé sans problème.

Vérification avec un aperçu du contenu du fichier d’entrée et du fichier de sortie, tel qu’on peut l’afficher avec cat ou head :

Fichier d’entrée en Shift-JIS
Fichier de sortie converti en utf-8
Citer ce billet : “Uniformiser l’encodage d’un corpus avec Python”, par Alexander Delaporte. Publié sur Tekipaki le 29/06/2020. Lien : https://tekipaki.hypotheses.org/478.
  1. Il s’agit bien d’exemples créés pour l’occasion et non de fichiers issus d’un projet de recherche. N’hésitez pas à les télécharger et à les manipuler. []
  2. D’ailleurs, j’ai utilisé re.sub() par habitude dans le script proposé en téléchargement, mais la même opération aurait pu être réalisée avec str.replace() sans problème (voir la documentation). []
  3. Les fonctions du module os que j’utilise ici fonctionnent sur les systèmes Linux, si votre système d’exploitation est différent il faudra probablement les adapter. []

Auteur/autrice : Alexander Delaporte

Ingénieur d'études au Centre de recherches linguistiques sur l'Asie Orientale

2 réflexions sur « Uniformiser l’encodage d’un corpus avec Python »

  1. C’est un bon utile de vérification et d’authentification des données en sciences humaines st sociales. Le logiciel python a un vaste chantier en sciences humaines. Merci Mr Delaporte

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.

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

Rechercher dans OpenEdition Search

Vous allez être redirigé vers OpenEdition Search