12 Les fichiers et le système de fichiers
La manipulation de fichiers comporte deux aspects :
- Manipuler le contenu du fichier.
- Manipuler les fichiers, c’est à dire le système de fichiers.
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 :
os: https://docs.python.org/3/library/os.html
Permet d’utiliser des fonctions dépendant du système d’exploitationos.path: https://docs.python.org/3/library/os.path.html#module-os.path
Permet de manipuler les chemins. os.path est un module virtuel dont l’implémentation réelle est chargée en fonction du système sur lequel s’exécute le code.shutil: https://docs.python.org/3/library/shutil.html#module-shutil
Permet des manipulations de haut-niveau des fichiers. Certaines fonctions peuvent appeler des fonctions du module os.
À 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 pourscandir).DirEntry.is_dir(): retourne True si il s’agit d’un répertoire sinonFalseDirEntry.is_file(): retourne True si il s’agit d’un fichier sinonFalseDirEntry.stat(): retourne un objet de typestat_resultcomportant 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.csvAvec 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.csv12.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 nombasenamedu contenu du répertoire.os.scandir(path): retourne un itérateur sur des objets de typeDirEntryrepré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 UNIXos.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 fichieros.rmdir(path): supprime un répertoireos.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.destpeut ê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.destpeut ê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épertoireos.rename(src, dest): renomme fichier ou répertoire. Sidestest 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. Sidestest 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 existeos.path.isdir(path): est-ce un répertoireos.path.isfile(path): est-ce un fichieros.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épertoiresos.path.basename(path): retourne le nom du fichieros.path.split(path): retourne un tuple des deux précédentsos.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 PathCette 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]: FalseCet 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éfautw: écriture, écrase un fichier existant, crée un fichier inexistantx: 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 binairet: 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 chainestrdans 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 curseurseek(offset[, whence]): permet de déplacer le curseur de la valeur offset en fonction du paramètrewhence. Le paramètrewhencepeut prendre trois valeurs :0ouos.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.1ouos.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.2ouos.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éeload(file): charge une donnée
Le module pickle propose également deux classes :
Pickler(file)qui possède une méthodedump(object)qui sauvegarde une donnéeUnpickler(file)qui possède une méthodeload()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.closeimport pickle
f = open("pile_backup", r)
pile_name = pickle.load(f)
pile_value = pickle.load(f)
f.closeComme 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.