Posted on Aug 03, 2025· Updated on Aug 03, 2025

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

#python #intermediate #file system
Image for 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 pathlib est souvent auto-documenté. Une chaîne de méthodes sur un objet Path est 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 !