Motif de Cache par Hachage de Contenu de Fichier
Mettez en cache les résultats coûteux de traitement de fichiers (analyse PDF, extraction de texte, analyse d'images) en utilisant des hachages SHA-256 du contenu comme clés de cache. Contrairement à la mise en cache basée sur le chemin, cette approche survit aux déplacements/renommages de fichiers et s'invalide automatiquement lorsque le contenu change.
Quand Activer
- Construction de pipelines de traitement de fichiers (PDF, images, extraction de texte)
- Le coût de traitement est élevé et les mêmes fichiers sont traités de manière répétée
- Besoin d'une option
--cache/--no-cacheen CLI - Souhait d'ajouter du cache à des fonctions pures existantes sans les modifier
Motif Principal
1. Clé de Cache Basée sur le Hachage du Contenu
Utilisez le contenu du fichier (pas le chemin) comme clé de cache :
import hashlib
from pathlib import Path
_HASH_CHUNK_SIZE = 65536 # 64KB chunks for large files
def compute_file_hash(path: Path) -> str:
"""SHA-256 of file contents (chunked for large files)."""
if not path.is_file():
raise FileNotFoundError(f"File not found: {path}")
sha256 = hashlib.sha256()
with open(path, "rb") as f:
while True:
chunk = f.read(_HASH_CHUNK_SIZE)
if not chunk:
break
sha256.update(chunk)
return sha256.hexdigest()
Pourquoi un hachage de contenu ? Renommage/déplacement de fichier = hit de cache. Changement de contenu = invalidation automatique. Aucun fichier d'index nécessaire.
2. Dataclass Figée pour l'Entrée de Cache
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class CacheEntry:
file_hash: str
source_path: str
document: ExtractedDocument # The cached result
3. Stockage de Cache Basé sur Fichier
Chaque entrée de cache est stockée sous forme de {hash}.json — recherche O(1) par hachage, aucun fichier d'index requis.
import json
from typing import Any
def write_cache(cache_dir: Path, entry: CacheEntry) -> None:
cache_dir.mkdir(parents=True, exist_ok=True)
cache_file = cache_dir / f"{entry.file_hash}.json"
data = serialize_entry(entry)
cache_file.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
def read_cache(cache_dir: Path, file_hash: str) -> CacheEntry | None:
cache_file = cache_dir / f"{file_hash}.json"
if not cache_file.is_file():
return None
try:
raw = cache_file.read_text(encoding="utf-8")
data = json.loads(raw)
return deserialize_entry(data)
except (json.JSONDecodeError, ValueError, KeyError):
return None # Treat corruption as cache miss
4. Encapsulation de la Couche Service (SRP)
Gardez la fonction de traitement pure. Ajoutez le cache en tant que couche de service séparée.
def extract_with_cache(
file_path: Path,
*,
cache_enabled: bool = True,
cache_dir: Path = Path(".cache"),
) -> ExtractedDocument:
"""Service layer: cache check -> extraction -> cache write."""
if not cache_enabled:
return extract_text(file_path) # Pure function, no cache knowledge
file_hash = compute_file_hash(file_path)
# Vérification du cache
cached = read_cache(cache_dir, file_hash)
if cached is not None:
logger.info("Cache hit: %s (hash=%s)", file_path.name, file_hash[:12])
return cached.document
# Absence de cache -> extraction -> stockage
logger.info("Cache miss: %s (hash=%s)", file_path.name, file_hash[:12])
doc = extract_text(file_path)
entry = CacheEntry(file_hash=file_hash, source_path=str(file_path), document=doc)
write_cache(cache_dir, entry)
return doc
Décisions Clés de Conception
| Décision | Justification |
|---|---|
| Hachage de contenu SHA-256 | Indépendant du chemin, s'invalide automatiquement lors d'un changement de contenu |
Nommage de fichier {hash}.json | Recherche O(1), aucun fichier d'index nécessaire |
| Encapsulation de la couche service | SRP : l'extraction reste pure, le cache est une préoccupation séparée |
| Sérialisation JSON manuelle | Contrôle total sur la sérialisation des dataclasses figées |
Corruption retourne None | Dégradation gracieuse, retraite au prochain lancement |
cache_dir.mkdir(parents=True) | Création paresseuse du répertoire lors de la première écriture |
Bonnes Pratiques
- Hachez le contenu, pas les chemins — les chemins changent, l'identité du contenu non
- Découpez les gros fichiers lors du hachage — évitez de charger les fichiers entiers en mémoire
- Gardez les fonctions de traitement pures — elles ne doivent rien savoir du cache
- Enregistrez les hits/miss de cache avec des hachages tronqués pour le débogage
- Gérez la corruption gracieusement — traitez les entrées de cache invalides comme des absences, ne plantez jamais
Anti-Patterns à Éviter
# MAUVAIS : Mise en cache basée sur le chemin (casse lors du déplacement/renommage de fichier)
cache = {"/chemin/vers/fichier.pdf": resultat}
# MAUVAIS : Ajout de la logique de cache dans la fonction de traitement (violation SRP)
def extract_text(chemin, *, cache_active=False, repertoire_cache=None):
if cache_active: # Cette fonction a maintenant deux responsabilités
...
# MAUVAIS : Utilisation de dataclasses.asdict() avec des dataclasses figées imbriquées
# (peut causer des problèmes avec des types imbriqués complexes)
donnees = dataclasses.asdict(entree) # Utilisez plutôt une sérialisation manuelle
Quand Utiliser
- Pipelines de traitement de fichiers (analyse PDF, OCR, extraction de texte, analyse d'images)
- Outils CLI qui bénéficient d'options
--cache/--no-cache - Traitement par lots où les mêmes fichiers apparaissent dans plusieurs exécutions
- Ajout de cache à des fonctions pures existantes sans les modifier
Quand NE PAS Utiliser
- Données qui doivent toujours être fraîches (flux en temps réel)
- Entrées de cache qui seraient extrêmement volumineuses (envisagez plutôt le streaming)
- Résultats qui dépendent de paramètres au-delà du contenu du fichier (par exemple, différentes configurations d'extraction)