12  Les fichiers et le système de fichiers

La manipulation de fichiers comporte deux aspects :

Nous allons voir dans un premier temps ce que Python propose pour la manipulation des fichiers avant de nous intéresser au contenu des fichiers.

12.1 Le système de fichiers

Il y a fondamentalement 3 modules qui vont offrir des fonctionnalités pour la manipulation du système de fichiers :

À ces trois là s’ajoute un module plus récent, pathlib, plus récent et qui propose une approche objet pour manipuler le système de fichiers. Il tend à remplacer principalement os.path.

Consultez les documentations en lien de chaque modules pour en apprendre d’avantage sur ce que propose chaque module. Dans ce document, nous allons plutôt nous intéresser à des cas pratiques.

12.1.1 Obtenir le contenu d’un répertoire

Il existe plusieurs méthodes pour obtenir le contenu d’un répertoire. La manière historique utilise la fonction os.listdir(path) qui retourne une liste de chaines de caractères. 

In [2]: import os                                                               
In [3]: os.listdir("assets")                                                    
Out[3]: 
['Paris', 'bbts12.csv','movieslist.csv', 'ile_de_france',
'showslist_bad.csv', 'ministere_culture', 'showsfiles.txt',
'files', 'movieslistdest.csv', 'showslist.csv', 'SNCF']

Bien entendu, vous ne manipulerez jamais réellement la liste mais les éléments de la liste.

La fonction listdir() ne prends pas en charge les glob (les wildcards utilisés pour filtrer le contenu d’un répertoire). Historiquement, il a été ajouté un module glob qui contient une fonction glob(). 

In [4]: import glob                                                             
In [5]: glob.glob('assets/*.csv')                                               
Out[5]: 
['assets/bbts12.csv', 'assets/movieslist.csv',
'assets/showslist_bad.csv', 'assets/movieslistdest.csv',
'assets/showslist.csv']

Depuis Python 3.5, il existe une nouvelle manière de manipuler le contenu d’un répertoire à l’aide de la fonction os.scandir(). La fonction scandir() retourne un itérateur sur des objets de type os.DirEntry. 

In [6]: for file in os.scandir("assets"): 
   ...:     print(file) 
   ...:                                                                        
<DirEntry 'Paris'>
<DirEntry 'bbts12.csv'>
<DirEntry 'movieslist.csv'>
<DirEntry 'ile_de_france'>
…

L’usage conseillé utilise un contexte manager de la manière suivante : 

In [7]: with os.scandir("assets") as entries: 
   ...:     for file in entries: 
   ...:         print(file) 
   ...:                                                                        
<DirEntry 'Paris'>
<DirEntry 'bbts12.csv'>
<DirEntry 'movieslist.csv'>
<DirEntry 'ile_de_france'>
…

La raison est que le Context Manager s’assurera de la bonne fermeture de l’itérateur et de la libération des ressources.

12.1.2 Exploiter le contenu d’un répertoire

À nouveau, tout va dépendre de la fonction que vous allez utiliser, listdir() ou scandir(). Dans le premier cas, vous avez des chaines de caractères représentant les noms basename des fichiers, dans le second, des objets de type os.DirEntry.

Les objets de type os.DirEntry possèdent des attributs et méthodes qui vont faciliter la manipulation des fichiers. Nous avons ainsi :

  • DirEntry.name : retourne le nom de base du fichier (sans l’arborescence)
  • DirEntry.path : retourne le nom du fichier avec l’arborescence relative (celle qui a été utilisée pour scandir).
  • DirEntry.is_dir() : retourne True si il s’agit d’un répertoire sinon False
  • DirEntry.is_file() : retourne True si il s’agit d’un fichier sinon False
  • DirEntry.stat() : retourne un objet de type stat_result comportant un ensemble d’informations sur le fichier. Ne sera pas couvert dans cette formation.

Ces attributs et méthodes permettent déjà d’obtenir des informations sur les éléments contenus dans un répertoire.

Ainsi, si nous souhaitons lister uniquement les fichier d’un répertoire avec un chemin relatif, avec scandir(), il suffit d’écrire le code suivant : 

In [8]: basepath = "assets"                                                    
In [9]: with os.scandir(basepath) as entries: 
   ...:     for file in entries: 
   ...:         if file.is_file(): 
   ...:             print(file.path) 
   ...:                                                                        
assets/bbts12.csv
assets/movieslist.csv
assets/showslist_bad.csv
assets/showsfiles.txt
assets/movieslistdest.csv
assets/showslist.csv

Avec listdir(), il faudra quelques éléments en plus.

Il est possible de tester si un chemin est un fichier ou un répertoire grâce aux fonctions os.path.isdir(path) et os.path.isfile(path).

Mais il faudra fournir un chemin à ces fonctions. Or listdir() ne retourne que le nom basename du fichier. Il faudra donc le reconstruire grâce à la fonction os.path.join(path, *paths). 

In [10]: basepath = "assets"                                                    

In [11]: for entry in os.listdir(basepath): 
    ...:     if os.path.isfile(os.path.join(basepath, entry)): 
    ...:         print(entry) 
    ...:                                                                        
bbts12.csv
movieslist.csv
showslist_bad.csv
showsfiles.txt
movieslistdest.csv
showslist.csv

12.2 En résumé

12.2.1 Obtenir le contenu d’un répertoire

  • os.listdir(path) : retourne le contenu d’un répertoire sous forme d’une liste de chaînes de caractères représentant le nom basename du contenu du répertoire.
  • os.scandir(path) : retourne un itérateur sur des objets de type DirEntry représentant le contenu du répertoire.
  • glob.glob(path) : liste le contenu du répertoire en fonction du pattern et permet ainsi l’utilisation de wildcards.

12.2.2 Créer des répertoires

  • os.mkdir(path, mode) : crée répertoire, mode UNIX
  • os.makedirs(path, mode) : crée une arborescence de répertoires, mode UNIX

12.2.3 Supprimer des fichiers et des répertoires 

  • os.remove(path) : supprime un fichier
  • os.rmdir(path) : supprime un répertoire
  • os.removedirs(name) : supprime répertoires récursivement à partir de la fin (leaf)
  • shutil.rmtree(path) : supprime le répertoire et tout son contenu

12.2.4 Copier, déplacer et renommer

  • shutil.copy(src, dest) : copie un fichier. dest peut être un répertoire, dans ce cas, le fichier sera copié dans ce répertoire.
  • shutil.copy2(src, dest) : copie un fichier avec les métadonnées. dest peut être un répertoire, dans ce cas, le fichier sera copié dans ce répertoire.
  • shutil.copytree(src, dest) : copie un répertoire et son contenu.
  • shutil.move(src, dest) : déplace ou renomme un fichier ou un répertoire
  • os.rename(src, dest) : renomme fichier ou répertoire. Si dest est une arborescence, déplace le fichier ou le répertoire.
  • os.renames(src, dest) : renomme fichier ou répertoire en créant les répertoires si ils n’existent pas. Si dest est une arborescence, déplace le fichier ou le répertoire.

12.2.5 Informations sur le nom de fichier

Dans les exemples suivants, path est un nom de fichier ou répertoire contenant ou non une arborescence.

  • os.path.exists(path) : est-ce que le fichier ou répertoire existe
  • os.path.isdir(path) : est-ce un répertoire
  • os.path.isfile(path) : est-ce un fichier
  • os.path.join(path, *paths) : retourne une chaine de caractères représentant un chemin, concaténation des paramètres.
  • os.path.dirname(path) : retourne l’arborescence de répertoires
  • os.path.basename(path) : retourne le nom du fichier
  • os.path.split(path) : retourne un tuple des deux précédents
  • os.path.splitext(path) : retourne un tuple pour obtenir l’extension

12.3 Le module pathlib

Le module pathlib, ajouté à partir de Python 3.4 (voir https://www.python.org/dev/peps/pep-0428/), permet de manipuler les chemins avec une approche objet. La classe principale est la classe Path que nous allons importer directement. 

In [1]: from pathlib import Path

Cette classe permet de créer un objet chemin en lui passant en paramètre une chaîne de caractères représentant ce chemin. Ce chemin peut être absolu ou relatif. Dans l’exemple de code plus bas, vous pouvez voir que le type de l’objet est PosixPath. Path() est une classe abstraite (ou plus exactement une factory et donc une abstract factory) . Elle retourne l’implémentation concrète selon le système d’exploitation : PosixPath pour les systèmes posix, WindowsPath pour Windows.

La classe Path() permet de créer facilement un objet sur le répertoire courant. Ce chemin reste abstrait. Vous pouvez créer un nouvel objet avec le chemin réel grâce en résolvant le chemin par la méthode du même nom.

Cet objet possède un attribut .parent qui retourne un nouvel objet PosixPath, utilisé communément pour remonter l’arborescence.

In [2]: p = Path('.')

In [3]: p
Out[3]: PosixPath('.')

In [4]: p.resolve()
Out[4]: PosixPath('/Users/dad3zero/formation-Python_fondamentaux')

In [5]: p.parent
Out[5]: PosixPath('.')

In [6]: p.resolve().parent
Out[6]: PosixPath('/Users/dad3zero')

12.3.1 Fichier ou répertoire ?

L’objet Path possède des méthodes .is_dir() et .is_file() qui permettent de tester si le chemin correspond à un fichier ou un répertoire.

In [7]: [px for px in p.iterdir() if px.is_file()]
Out[7]: 
[PosixPath('LICENSE'),
 PosixPath('requirements.txt'),
 PosixPath('README.md'),
 PosixPath('.gitignore')]

Attention, un retour False de ces méthodes peut signifier que le chemin n’existe pas.

12.3.2 Obtenir le contenu d’un répertoire

À partir d’un objet Path, il est possible d’obtenir son contenu si il s’agit d’un répertoire. Les éléments retournés seront également de type Path (PosixPath pour être plus précis). 

L’objet de type Path permet également d’utiliser les glob. Dans ce cas, c’est un objet de type générateur qui est retourné.

In [8]: list(p.glob('p*/*.py'))
Out[8]: 
[PosixPath('pylib/templating.py'),
 PosixPath('pylib/__init__.py'),
 PosixPath('pylib/datasource.py'),
 PosixPath('pylib/media_db.py')]

L’objet de type Path a un usage particulier de l’opérateur divisé /. Celui-ci peut être utilisé en tant que réparateur avec d’autres chaines afin de créer un nouvel objet de type PosixPath. Attention, cet objet nouvellement créé n’est pas un élément existant. Un objet PosixPath est plus proche de ce qui est manipulé par la lib os.path. Vous pouvez évidemment tester l’existence de cette ressource créée. 

In [9]: new_path = p / 'stage'

In [10]: new_path
Out[10]: PosixPath('stage')

In [11]: new_path.exists()
Out[11]: True

In [12]: new_path = p / 'toto'

In [13]: new_path
Out[13]: PosixPath('toto')

In [14]: new_path.exists()
Out[14]: False

Cet opérateur, bien qu’utilisé, est plus une curiosité inadapté à certains cas. Il existe également une méthode Path.joinpath(*pathsegments) qui permet d’ajouter autant de segment que nécessaire.

In [15]: p.joinpath('stage', 'python', 'exercices')
Out[15]: PosixPath('stage/python/exercices')

Si le chemin est un fichier, vous pouvez l’ouvrir par la méthode .open() exactement comme la fonction que nous allons voir.

In [29]: my_file = (p / "README.md").open()

In [30]: my_file
Out[30]: <_io.TextIOWrapper name='README.md' mode='r' encoding='UTF-8'>

12.4 Ouverture et fermeture de fichier

12.4.1 Ouvrir un fichier

Pour manipuler le contenu d’un fichier avec Python, aucune dépendance n’est nécessaire. La fonction open(path, mode) est une fonction de base de Python (built-in). Elle prends deux paramètres : le nom fichier à ouvrir et le mode. Le nom du fichier s’entends au sens UNIX, c’est à dire comprenant ou non le chemin relatif ou absolu. Le mode est une chaine de caractères optionnelle avec les options sont les suivantes.

Le mode :

  • r : lecture seule, paramètre par défaut
  • w : écriture, écrase un fichier existant, crée un fichier inexistant
  • x : ouverture pour création (levée d’exception si fichier existe)
  • a : écriture en mode ajout, écrit en fin de fichier, le crée si inexistant

Le type de fichier :

  • b : ajouté au précédent ouverture en mode binaire
  • t : ajouté au précédent ouverture en mode texte (défaut)

En plus…

  • + : ajouté au précédent lecture et écriture

La fonction open() retourne un objet de type fichier.

Si vous utilisez le module pathlib, vous pouvez aussi passer un objet de type Path à la fonction open(). Cet objet possède méthode .open(mode) qui se comporte comme la fonction.

12.4.2 Fermer un fichier

Vous devez toujours prévoir de fermer un fichier ouvert. En effet, Python délègue les opérations sur les fichiers au système d’exploitation. Le système d’exploitation limite le nombre de fichiers qu’un process peut ouvrir (il est de l’ordre du millier). De plus, vous pouvez subir une perte de données en écriture principalement parce que l’écriture est asynchrone. Il est donc important de toujours refermer un fichier par la méthode .close().

12.4.3 Risques d’exceptions

La manipulation d’un fichier risque toujours de lever une exception, elle se fait donc dans une clause try. Une bonne pratique consiste à toujours fermer le fichier dans une clause finally. 

In [1]: try:
   ...:     mfile = open('showsfiles.txt', 'r')
   ...: except OSError:
   ...:     if not mfile.closed():
   ...:          mfile.close()
   ...:         

In [2]: mfile
Out[2]: <_io.TextIOWrapper name='showsfiles.txt' mode='r' encoding='UTF-8'>

In [3]: mfile.close()

12.4.4 Usage idiomatique avec un context manager

Afin de simplifier l’écriture, nous pouvons utiliser un context manager avec le pattern with open(). Le context manager ferme le fichier proprement même si une exception est levée. 

In [7]: with open('showsfiles.txt', 'r') as mfile:
   ...:     mfile.read()
   ...:

Nous allons dans cette partie traiter uniquement des fichiers texte. Depuis Python 3, les fichiers text sont de type _io.TextIOWrapper.

12.5 Lire et écrire dans un fichier

Nous avons plusieurs méthodes pour lire et écrire dans un fichier.

12.5.1 Les méthodes d’écriture

Pour écrire dans un fichier, le fichier doit être ouvert en écriture. L’objet fichier propose les méthodes suivantes :

  • write(str) : écrit la chaine str dans le fichier et retourne le nombre de caractères écrits.
  • writelines(sequences) : écrit à la suite dans le fichier les chaines de la liste passée en paramètre.

Attention, aucun caractère n’est inséré en plus du paramètre. C’est donc à vous de vous assurer de la présence des caractères de fin de ligne.

12.5.2 Les méthodes de lecture

Pour lire un fichier, celui-ci doit être ouvert en lecture. L’objet fichier propose plusieurs méthodes pour lire dans un fichier :

  • read([size]) : sans argument, retourne le contenu du fichier sous forme d’une chaine de caractères. L’argument optionnel est la taille maximum lue en nombre de caractères.
  • readline([size]) : retourne le contenu du fichier jusqu’au prochain saut de ligne (inclut). Si l’argument optionnel size est renseigné, la lecture sera limitée à ce nombre de caractères.
  • readlines([hint]) : Sans argument, retourne le contenu du fichier sous la forme d’une liste de chaines de caractères correspondant aux lignes contenues dans le fichier. Le paramètre hint spécifie le nombre de caractères lus. Le nombre total de caractères de la liste ne pourra donc pas être supérieur à ce paramètre.

12.5.3 Lecture avec l’itérateur

Cependant, le plus facile et le plus efficace est d’utiliser un itérateur qui facilite le parcours et le traitement d’un fichier ligne à ligne. L’usage est le suivant : 

In [1]: for line in file:
   ...:     print(line[:-1])

12.6 Manipuler le curseur

La position dans un fichier est définie par un curseur. Il est donc possible de manipuler le curseur  sur un fichier. Pour cela, nous avons 2 méthodes :

  • tell() : retourne la position du curseur

  • seek(offset[, whence]) : permet de déplacer le curseur de la valeur offset en fonction du paramètre whence. Le paramètre whence peut prendre trois valeurs :

  • 0 ou os.SEEK_SET : déplace le curseur par rapport au début du fichier. C’est le comportement par défaut. Le paramètre offset est un entier de valeur 0 ou positif.

  • 1 ou os.SEEK_CUR : déplace le curseur par rapport à la position courante du curseur. Pour un fichier texte, le paramètre offset ne peut avoir que la valeur de 0.

  • 2 ou os.SEEK_END : déplace le curseur par rapport à la fin du fichier. Pour un fichier texte, le paramètre offset ne peut avoir que la valeur de 0.

Attention : si vous utilisez l’itérateur, depuis Python 3, le curseur est inaccessible. Ceci est justifié parce que libérateur utilise un buffer (tampon) et que donc la position du curseur ne correspond pas à la position réellement lue.

12.7 Le module pickle

Pour plus d’informations : https://docs.python.org/3/library/pickle.html

Le module pickle est le module permettant la stérialisation et désérialisation d’objets en Python. Sérialiser un objet consiste à convertir un objet en flux d’octets ce qui permet de sauvegarder une donnée et son type. Pour éviter les confusions entre serialization, marshalling, et flattening, on parlera de pickling et unpickling.

Le module pickle propose deux fonctions principales :

  • dump(objet, file) : sauvegarde la donnée
  • load(file) : charge une donnée

Le module pickle propose également deux classes :

  • Pickler(file) qui possède une méthode dump(object) qui sauvegarde une donnée
  • Unpickler(file) qui possède une méthode load() qui charge une donnée

L’usage est le suivant :  

pile_name = "ma pile"
pile_value = [42, 'answer'] 

import pickle

f = open("pile_backup", w)
pickle.dump(pile_name, f)
pickle.dump(pile_value, f)
f.close
import pickle

f = open("pile_backup", r)
pile_name = pickle.load(f)
pile_value = pickle.load(f)
f.close

Comme vous pouvez le constater, il n’y a aucune sécurité sur les données serialisées et déserialisées. Ne désérialisez donc que des données dont vous êtes à 200 % sûr.

12.8 Les fichiers CSV

12.8.1 La méthode standard… et ancienne

Python propose un module permettant de gérer les fichiers CSV. Pour la documentation complète, voir : https://docs.python.org/3/library/csv.html

Le principe général de ce module est de fournir des objets permettant la lecture ou l’écriture de fichiers CSV. L’utilisation de base fera appel aux fonctions csv.reader(csvfile) et csv.writer(csvfile) qui retourneront un objet de type csv.reader et csv.writer. Un paramètre optionnel delimiter permet de spécifier le séparateur de colonnes et un paramètre optionnel quotechar permet de spécifier le caractère encadrant les chaînes. Un exemple de lecture est proposé dans le code suivant : 

In [10]: with open('csvfile.csv') as csvfile:
    ...:     csvreader = csv.reader(csvfile, delimiter=';',
    ...:                            quotechar='"')
    ...:     for row in csvreader:
    ...:         print(row)
    ...:  

Un exemple d’écriture est proposé dans le code suivant : 

In [24]: with open('csvfile.csv', 'w', newline='') as csvfile:
    ...:     csvwriter = csv.writer(csvfile, delimiter=';') 
    ...:     for row in list_elements:                                          
    ...:         csvwriter.writerow(row)
    ...: 

Un comparatif de code est disponible dans le projet de référence et vous pouvez voir que pour un traitement de base, le module csv apporte peu de choses, en tout cas pour la lecture.

Il peut cependant être intéressant d’utiliser la classe DictReader qui instancie un objet. Cet objet est un itérateur qui retourne la ligne suivante sous forme d’un dictionnaire. Sans paramètre fieldnames, qui permet de définir les clefs, la première ligne (l’en-tête) est utilisée pour définir les clefs. 

In [10]: with open('csvfile.csv') as csvfile:
    ...:     csvreader = csv.DictReader(csvfile, delimiter=';',
    ...:                            quotechar='"')
    ...:     for row in csvreader:
    ...:         print(row)
    ...:  

12.8.2 La méthode plus moderne même si complexe

Bien qu’initialement destinée à la datascience la bibliothèque Pandas est souvent utilisée pour charger des fichiers csv. Pandas retournera un objet de type DataFrame. Ce type d’objet est un tableau qui possèdes des fonctionnalités pour sélectionner ou filtrer des données.