10 Opérations Essentielles du Système de Fichiers à Connaître

Travailler avec des fichiers et des répertoires est une partie fondamentale du développement Python, pourtant de nombreux développeurs s’en tiennent aux opérations de base sans explorer toute la puissance des capacités de gestion de chemins de Python. Que vous construisiez des applications web, des pipelines de traitement de données ou des scripts d’automatisation, maîtriser ces modèles essentiels du système de fichiers rendra votre code plus robuste, efficace et maintenable.
1. Découverte Intelligente de Fichiers avec les Motifs Glob
Vous avez souvent besoin de trouver des fichiers basés sur un motif, et pas seulement sur un nom exact. La méthode glob de l’objet Path est un outil puissant et intuitif pour cela.
Recherche de Base et Récursive
Supposons que vous ayez un projet dans un dossier src/. Voici comment trouver tous vos fichiers Python.
from pathlib import Path
# L'objet Path est votre outil principal pour les opérations sur le système de fichiers.
project_dir = Path("src/")
# 1. Trouver tous les fichiers Python dans le niveau supérieur du répertoire 'src'.
# L'astérisque (*) est un joker pour "correspondre à n'importe quoi".
print("--- Fichiers .py de niveau supérieur ---")
for f in project_dir.glob("*.py"):
print(f)
# 2. Trouver tous les fichiers Python RÉCURSIVEMENT à travers tous les sous-répertoires.
# La méthode 'rglob' est votre meilleure amie pour les recherches profondes.
print("\n--- Tous les fichiers .py du projet ---")
for f in project_dir.rglob("*.py"):
print(f)
# Exemple de Sortie :
# --- Fichiers .py de niveau supérieur ---
# src/main.py
# --- Tous les fichiers .py du projet ---
# src/main.py
# src/utils/helpers.py
# src/api/models.py
Correspondance de Motifs Avancée
glob prend en charge plus que seulement *. Vous pouvez utiliser ? pour correspondre à n’importe quel caractère unique et [] pour correspondre à une plage de caractères, tout comme dans le shell.
from pathlib import Path
# Pour exécuter ceci, créez un répertoire 'logs' avec les fichiers d'exemple.
logs_dir = Path("logs/")
logs_dir.mkdir(exist_ok=True)
Path("logs/app1.log").touch()
Path("logs/app2.log").touch()
Path("logs/app_extra.log").touch()
Path("logs/2023-10-01.log").touch()
Path("logs/2023-11-01.log").touch()
# Trouver les logs comme 'app1.log', 'app2.log', mais pas 'app_extra.log'
print("--- Joker de caractère unique ---")
for f in logs_dir.glob("app?.log"):
print(f)
# Trouver les logs d'octobre ou novembre 2023
print("\n--- Plage de caractères ---")
for f in logs_dir.glob("2023-[10-11]-*.log"):
print(f)
# Exemple de Sortie :
# --- Joker de caractère unique ---
# logs/app1.log
# logs/app2.log
#
# --- Plage de caractères ---
# logs/2023-10-01.log
# logs/2023-11-01.log
2. Naviguer dans les Répertoires avec Précision
Parfois, vous avez besoin de plus de contrôle que ce que rglob offre, par exemple lorsque vous devez ignorer des répertoires spécifiques. Au lieu de revenir à os.walk, vous pouvez écrire une fonction récursive propre utilisant les propres méthodes de pathlib.
from pathlib import Path
def smart_directory_walk(root_path, skip_dirs=None, file_patterns=None):
"""
Parcourt une arborescence de répertoires en utilisant pathlib, vous permettant
d'ignorer des sous-arborescences entières et de ne renvoyer que les fichiers
correspondant à des motifs spécifiques.
"""
if skip_dirs is None:
# Un ensemble (set) est utilisé pour des recherches rapides.
skip_dirs = {'.git', '__pycache__', 'node_modules', '.venv'}
if file_patterns is None:
file_patterns = ["*"]
root = Path(root_path)
for item in root.iterdir():
# Si l'élément est un répertoire, décidez s'il faut y récurser.
if item.is_dir() and item.name not in skip_dirs:
# yield from est une manière propre de transmettre les résultats de l'appel récursif.
yield from smart_directory_walk(item, skip_dirs, file_patterns)
# S'il s'agit d'un fichier, vérifiez s'il correspond à nos motifs.
elif item.is_file():
if any(item.match(p) for p in file_patterns):
yield item
if __name__ == "__main__":
print("Recherche de fichiers Python et texte, ignorant les environnements virtuels standards :")
# Pour exécuter ceci, créez quelques fichiers et dossiers factices.
project_root = Path(".")
(project_root / "src").mkdir(exist_ok=True)
(project_dir / "src" / "main.py").touch()
(project_root / ".venv").mkdir(exist_ok=True)
(project_root / ".venv" / "ignored.py").touch()
for f in smart_directory_walk(project_root, file_patterns=["*.py", "*.txt"]):
print(f"Trouvé : {f}")
3. Écrire des Fichiers Atomiquement pour la Sécurité des Données
Que se passe-t-il si votre script plante à mi-chemin de l’écriture de config.json ? Vous obtenez un fichier corrompu. Une opération atomique empêche cela : c’est une action tout ou rien. La manière standard de procéder est d’écrire dans un fichier temporaire, puis d’effectuer une seule opération move atomique.
import tempfile
import shutil
import os
from pathlib import Path
def atomic_write(file_path, content, encoding='utf-8'):
"""
Écrit le contenu dans un fichier de manière atomique pour éviter la corruption des données.
"""
target_path = Path(file_path)
target_path.parent.mkdir(parents=True, exist_ok=True)
# Crée un fichier temporaire dans le même répertoire que le fichier final.
# C'est crucial car déplacer un fichier sur le même système de fichiers est atomique.
with tempfile.NamedTemporaryFile(
mode='w',
encoding=encoding,
dir=target_path.parent,
delete=False,
suffix='.tmp'
) as tmp_file:
tmp_file.write(content)
# Pour les données critiques, os.fsync() garantit que les données sont physiquement écrites
# sur le disque. C'est l'un des rares cas où le module os est
# encore nécessaire pour un contrôle de bas niveau.
tmp_file.flush()
os.fsync(tmp_file.fileno())
temp_path = tmp_file.name
# L'opération atomique : renommer le fichier temporaire vers la destination finale.
# shutil.move est intelligent et fonctionne sur différents systèmes de fichiers.
shutil.move(temp_path, target_path)
print(f"Écrit atomiquement dans {target_path}")
if __name__ == "__main__":
# Ceci créera 'config/settings.json' en toute sécurité.
atomic_write("config/settings.json", '{"theme": "dark", "retries": 3}')
4. Maîtriser les Fichiers et Répertoires Temporaires
Les fichiers temporaires sont essentiels pour le traitement intermédiaire. Le module tempfile de Python s’intègre magnifiquement avec pathlib, créant des fichiers et des répertoires sécurisés qui se nettoient automatiquement.
import tempfile
import json
from pathlib import Path
# Une fonction factice pour simuler le travail
def process_file(file_path):
print(f"Traitement de {file_path}...")
return f"processed_{file_path.name}"
def batch_process_data(list_of_data):
"""
Utilise un répertoire temporaire pour traiter un lot de données, garantissant
que tous les fichiers intermédiaires sont nettoyés automatiquement.
"""
# TemporaryDirectory crée un répertoire qui est supprimé lorsque le bloc 'with' se termine.
with tempfile.TemporaryDirectory() as tmp_dir_str:
tmp_dir = Path(tmp_dir_str)
print(f"Répertoire temporaire créé : {tmp_dir}")
results = []
for i, data_item in enumerate(list_of_data):
# Créer un objet Path temporaire à l'intérieur de notre répertoire temporaire
temp_file = tmp_dir / f"input_{i}.json"
temp_file.write_text(json.dumps(data_item))
result = process_file(temp_file)
results.append(result)
# Le 'tmp_dir' et tout son contenu sont automatiquement supprimés ici.
return results
if __name__ == "__main__":
data_to_process = [{"id": 1, "value": "A"}, {"id": 2, "value": "B"}]
final_results = batch_process_data(data_to_process)
print(f"\nRésultats finaux : {final_results}")
print("Le répertoire temporaire a été supprimé.")
5. Valider et Assainir les Chemins Fournis par l’Utilisateur
Ne faites jamais confiance à un chemin provenant d’une source externe. Un utilisateur malveillant pourrait fournir ../../etc/passwd pour tenter de lire des fichiers sensibles. Vous devez valider et assainir toutes les entrées de chemin externes en utilisant les fonctionnalités de sécurité intégrées de pathlib.
import re
from pathlib import Path
def sanitize_filename(filename, replacement='_'):
"""
Nettoie une chaîne pour qu'elle devienne un nom de fichier valide et sûr pour tout OS.
"""
invalid_chars = r'[<>:"/\\|?*\x00-\x1f]'
sanitized = re.sub(invalid_chars, replacement, filename)
# Des vérifications supplémentaires peuvent être ajoutées ici (par exemple, pour les noms réservés Windows)
return sanitized.strip(' .')
def validate_and_resolve_path(base_dir, user_path_str):
"""
Résout en toute sécurité un chemin fourni par l'utilisateur, en s'assurant qu'il reste dans un répertoire de base.
Ceci est CRITIQUE pour prévenir les attaques de traversée de répertoire.
"""
base_dir = Path(base_dir).resolve()
# resolve() crée un chemin canonique et absolu, nettoyant les segments '..'.
resolved_path = (base_dir / user_path_str).resolve()
# La vérification de sécurité clé : le chemin final est-il toujours à l'intérieur de notre base_dir sécurisée ?
# Path.is_relative_to() a été ajouté dans Python 3.9 et est parfait pour cela.
if resolved_path.is_relative_to(base_dir):
return resolved_path
else:
raise PermissionError("Tentative de traversée de chemin détectée.")
if __name__ == "__main__":
# 1. Assainir un nom de fichier potentiellement désordonné
dirty_name = "My Report: Part 1/2 <Final?>.docx"
clean_name = sanitize_filename(dirty_name)
print(f"Assaini '{dirty_name}' en '{clean_name}'")
# 2. Valider un chemin utilisateur
upload_dir = "uploads"
Path(upload_dir).mkdir(exist_ok=True)
try:
# Chemin sûr
safe_path = validate_and_resolve_path(upload_dir, "images/profile.jpg")
print(f"OK : Le chemin est sûr : {safe_path}")
# Chemin malveillant
malicious_path_str = "../../../etc/hosts"
print(f"\nTest du chemin malveillant : '{malicious_path_str}'")
validate_and_resolve_path(upload_dir, malicious_path_str)
except PermissionError as e:
print(f"ERREUR : {e}")
6. Calculer la Taille d’un Répertoire avec pathlib
Calculer la taille d’un répertoire est une tâche classique. Bien que os.scandir soit connu pour sa vitesse, une approche purement pathlib utilisant rglob est souvent plus lisible et pratique pour toutes les applications, sauf celles qui sont les plus critiques en termes de performance sur des systèmes de fichiers massifs.
from pathlib import Path
def calculate_directory_size(directory):
"""
Calcule la taille totale d'un répertoire et de tous ses sous-répertoires
en utilisant une approche purement pathlib lisible.
"""
dir_path = Path(directory)
total_size = 0
# rglob('*') est un générateur, il ne charge donc pas tous les chemins en mémoire à la fois.
for path in dir_path.rglob('*'):
# Nous ne faisons la somme que de la taille des fichiers.
if path.is_file():
try:
# path.stat().st_size donne la taille en octets.
total_size += path.stat().st_size
except (PermissionError, FileNotFoundError):
# Ignorer les fichiers auxquels nous ne pouvons pas accéder.
continue
return total_size
def format_size(size_bytes):
"""Formate les octets en une chaîne lisible par l'homme (KB, MB, GB)."""
if size_bytes == 0:
return "0B"
units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
i = 0
while size_bytes >= 1024 and i < len(units) - 1:
size_bytes /= 1024
i += 1
return f"{size_bytes:.2f} {units[i]}"
if __name__ == "__main__":
target_directory = "."
print(f"Calcul de la taille de '{Path(target_directory).resolve()}'...")
total_bytes = calculate_directory_size(target_directory)
print(f"Taille totale : {format_size(total_bytes)} ({total_bytes:,} octets)")
7. Copier des Fichiers Volumineux avec Progression et Vérification
Lors de la copie de fichiers volumineux, vous souhaitez fournir un retour d’information à l’utilisateur et vous assurer que le fichier copié n’est pas corrompu. Ce modèle combine shutil pour la copie, hashlib pour l’intégrité des données et la bibliothèque tqdm pour une belle barre de progression.
Note : Vous devrez installer tqdm : pip install tqdm
import shutil
import hashlib
import os
from pathlib import Path
from tqdm import tqdm
def calculate_file_hash(file_path, algorithm='sha256', chunk_size=65536):
"""Calcule le hachage d'un fichier."""
hash_obj = hashlib.new(algorithm)
with Path(file_path).open('rb') as f:
for chunk in iter(lambda: f.read(chunk_size), b""):
hash_obj.update(chunk)
return hash_obj.hexdigest()
def copy_with_verification(src, dst):
"""
Copie un fichier avec une barre de progression et vérifie l'intégrité de la copie.
"""
src_path, dst_path = Path(src), Path(dst)
dst_path.parent.mkdir(parents=True, exist_ok=True)
src_size = src_path.stat().st_size
print(f"Calcul du hachage pour {src_path.name}...")
src_hash = calculate_file_hash(src_path)
print(f"Copie de {src_path.name} vers {dst_path}...")
with src_path.open('rb') as fsrc, \
dst_path.open('wb') as fdst, \
tqdm(total=src_size, unit='B', unit_scale=True, desc=src_path.name) as pbar: \
shutil.copyfileobj(fsrc, fdst, length=16*1024*1024)
# Mettre à jour manuellement la barre de progression si copyfileobj se termine avant que tqdm ne mette à jour
pbar.n = src_size
pbar.refresh()
print("Vérification de la copie...")
dst_hash = calculate_file_hash(dst_path)
if src_hash != dst_hash:
dst_path.unlink() # Supprimer la copie corrompue
raise IOError(f"Échec de la vérification ! Les hachages ne correspondent pas pour {dst_path}")
print(f"Succès ! {dst_path.name} copié et vérifié.")
return dst_path
if __name__ == "__main__":
source_file = Path("large_file.dat")
if not source_file.exists():
print(f"Création du fichier factice '{source_file}'...")
# os.urandom est utilisé ici simplement pour obtenir des octets aléatoires pour un fichier factice.
source_file.write_bytes(os.urandom(50 * 1024 * 1024)) # 50 Mo
try:
copy_with_verification(source_file, Path("backup/large_file.dat"))
except (IOError, FileNotFoundError) as e:
print(f"Une erreur s'est produite : {e}")
8. Surveiller un Répertoire pour les Changements
Vous souhaitez traiter automatiquement les fichiers déposés dans un dossier ? Vous avez besoin d’un observateur de système de fichiers. Bien que les bibliothèques comme watchdog soient meilleures pour la production, il est bon de savoir comment en construire un simple vous-même en utilisant pathlib et le sondage (polling).
import time
from pathlib import Path
from collections import defaultdict
class SimpleFileWatcher:
"""Un observateur de fichiers basique qui sonde un répertoire pour les changements."""
def __init__(self, watch_directory, patterns=None):
self.watch_dir = Path(watch_directory)
self.patterns = patterns or ["*"]
self._file_states = {} # Stocke {chemin: mtime}
self.callbacks = defaultdict(list)
def on(self, event_type, callback):
self.callbacks[event_type].append(callback)
def _trigger(self, event_type, file_path):
for callback in self.callbacks[event_type]:
try:
callback(file_path)
except Exception as e:
print(f"Erreur dans le rappel : {e}")
def watch(self, poll_interval=1.0):
print(f"Surveillance de {self.watch_dir.resolve()}... (Appuyez sur Ctrl+C pour arrêter)")
try:
while True:
self._scan()
time.sleep(poll_interval)
except KeyboardInterrupt:
print("\nArrêt de l'observateur.")
def _scan(self):
current_files = set()
for pattern in self.patterns:
for path in self.watch_dir.rglob(pattern):
if path.is_file():
current_files.add(path)
try:
mtime = path.stat().st_mtime
if path not in self._file_states:
self._file_states[path] = mtime
self._trigger('created', path)
elif self._file_states[path] != mtime:
self._file_states[path] = mtime
self._trigger('modified', path)
except FileNotFoundError:
continue
deleted_files = set(self._file_states.keys()) - current_files
for path in deleted_files:
del self._file_states[path]
self._trigger('deleted', path)
def log_change(event):
def handler(path):
print(f"[{event.upper()}] - {path.name} à {time.ctime()}")
return handler
if __name__ == "__main__":
watch_folder = Path("watched_folder")
watch_folder.mkdir(exist_ok=True)
watcher = SimpleFileWatcher(watch_folder, patterns=["*.txt", "*.csv"])
watcher.on('created', log_change('created'))
watcher.on('modified', log_change('modified'))
print("Observateur démarré. Essayez de créer/modifier des fichiers dans 'watched_folder'.")
watcher.watch()
9. Gérer les Fichiers de Configuration avec Flexibilité
Les applications ont besoin de configurations provenant de fichiers (JSON, YAML, INI). Ce ConfigManager gère différents formats avec élégance, utilisant pathlib pour gérer les chemins et notre fonction atomic_write pour sauvegarder les paramètres en toute sécurité.
Note : Vous aurez besoin de PyYAML : pip install pyyaml
import json
import yaml
import configparser
from pathlib import Path
from typing import Any, Dict
# Dans un projet réel, la fonction 'atomic_write' du Modèle 3 serait
# dans un fichier utils.py partagé. Nous supposons qu'elle est disponible ici.
class ConfigManager:
"""Un gestionnaire pour charger et sauvegarder des fichiers de configuration dans divers formats."""
def __init__(self, config_path: str):
self.path = Path(config_path)
self.type = self.path.suffix.lower().strip('.')
def load(self) -> Dict[str, Any]:
if not self.path.exists():
return {}
try:
with self.path.open('r', encoding='utf-8') as f:
if self.type == 'json':
return json.load(f)
elif self.type in ['yaml', 'yml']:
return yaml.safe_load(f) or {}
# Ajouter d'autres formats comme INI si nécessaire
except Exception as e:
raise IOError(f"Échec du chargement de la configuration {self.path}: {e}")
return {}
def save(self, config: Dict[str, Any]):
content = ""
try:
if self.type == 'json':
content = json.dumps(config, indent=2)
elif self.type in ['yaml', 'yml']:
content = yaml.dump(config, default_flow_style=False)
# Utiliser notre fonction d'écriture sécurisée
atomic_write(self.path, content)
except Exception as e:
raise IOError(f"Échec de la sauvegarde de la configuration {self.path}: {e}")
if __name__ == "__main__":
json_config = ConfigManager("config.yml")
settings = {
"database": {"host": "db.example.com", "port": 5432},
"features": {"new_ui": True, "beta_access": False}
}
print(f"Sauvegarde des paramètres dans {json_config.path}...")
json_config.save(settings)
loaded = json_config.load()
print("Paramètres chargés :")
print(yaml.dump(loaded))
assert settings == loaded
10. Gérer les Archives (ZIP, TAR) en Toute Sécurité
Travailler avec des fichiers .zip ou .tar.gz est courant. Ce ArchiveManager utilise pathlib pour fournir une interface simple et sûre pour créer et extraire des archives, y compris des vérifications cruciales pour prévenir les attaques de traversée de chemin.
import zipfile
import tarfile
from pathlib import Path
class ArchiveManager:
"""Une interface sûre et simple pour gérer les archives zip et tar."""
def _is_path_safe(self, path_str, target_dir):
target_dir = Path(target_dir).resolve()
resolved_path = (target_dir / path_str).resolve()
return resolved_path.is_relative_to(target_dir)
def extract(self, archive_path, extract_to):
archive_path = Path(archive_path)
extract_to = Path(extract_to)
extract_to.mkdir(parents=True, exist_ok=True)
if archive_path.suffix == '.zip':
with zipfile.ZipFile(archive_path, 'r') as archive:
for member_name in archive.namelist():
if self._is_path_safe(member_name, extract_to):
archive.extract(member_name, extract_to)
else:
print(f"AVERTISSEMENT : Chemin non sûr ignoré dans zip : {member_name}")
elif '.tar' in "".join(archive_path.suffixes):
with tarfile.open(archive_path, 'r:*') as archive:
for member in archive.getmembers():
if self._is_path_safe(member.name, extract_to):
archive.extract(member, path=extract_to, set_attrs=False)
else:
print(f"AVERTISSEMENT : Chemin non sûr ignoré dans tar : {member.name}")
else:
raise ValueError(f"Type d'archive non pris en charge : {archive_path.suffix}")
print(f"Extraction réussie de {archive_path.name} vers {extract_to}")
def create(self, source_dir, output_path):
source_dir = Path(source_dir)
output_path = Path(output_path)
output_path.parent.mkdir(parents=True, exist_ok=True)
if output_path.suffix == '.zip':
with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as archive:
for path in source_dir.rglob("*"):
archive.write(path, path.relative_to(source_dir))
elif output_path.name.endswith('.tar.gz'):
with tarfile.open(output_path, 'w:gz') as archive:
archive.add(source_dir, arcname='.')
else:
raise ValueError(f"Type d'archive non pris en charge : {output_path.suffix}")
print(f"Création réussie de l'archive {output_path}")
if __name__ == "__main__":
project_dir = Path("my_project")
(project_dir / "data").mkdir(parents=True, exist_ok=True)
(project_dir / "main.py").write_text("print('hello')")
manager = ArchiveManager()
archive_file = Path("backups/my_project.tar.gz")
manager.create(project_dir, archive_file)
extract_dir = Path("restored_project")
manager.extract(archive_file, extract_dir)
Pensées Finales
Vous venez d’explorer dix modèles puissants pour les opérations sur le système de fichiers en utilisant le module pathlib de Python.
- Adoptez
pathlib: Son approche orientée objet est plus propre, plus sûre et plus expressive que les anciennes méthodes basées sur des chaînes de caractères. Faites-en votre outil par défaut pour toutes les manipulations de chemins. - La Sécurité d’Abord : Validez toujours les entrées externes avec
is_relative_to, écrivez les fichiers critiques de manière atomique et extrayez les archives en toute sécurité. - La Lisibilité Compte : Le code
pathlibest souvent auto-documenté. Une chaîne de méthodes sur un objetPathest bien plus facile à suivre qu’une série d’appels imbriqués à os.path.join.
En intégrant ces modèles dans votre travail, vous écrirez du code Python qui n’est pas seulement plus professionnel, mais aussi considérablement plus robuste et sécurisé. Bon codage !