Content-Hash-Datei-Cache-Muster
Zwischenergebnisse von aufwändigen Dateiverarbeitungen (PDF-Parsing, Textextraktion, Bildanalyse) mit SHA-256-Inhaltshashen als Cache-Schlüssel cachen. Anders als pfadbasiertes Caching überlebt dieser Ansatz Dateiverschiebungen/-umbenennungen und invalidiert automatisch bei Inhaltsänderungen.
Wann aktivieren
- Beim Erstellen von Dateiverarbeitungspipelines (PDF, Bilder, Textextraktion)
- Wenn die Verarbeitung teuer ist und dieselben Dateien wiederholt verarbeitet werden
- Wenn eine
--cache/--no-cache-CLI-Option benötigt wird - Wenn bestehenden, reinen Funktionen nachträglich Caching hinzugefügt werden soll, ohne sie zu ändern
Kernmuster
1. Content-Hash-basierter Cache-Schlüssel
Dateiinhalt (nicht Pfad) als Cache-Schlüssel verwenden:
import hashlib
from pathlib import Path
_HASH_CHUNK_SIZE = 65536 # 64KB chunks for large files
def compute_file_hash(path: Path) -> str:
"""SHA-256 des Dateiinhalts (gechunkt für große Dateien)."""
if not path.is_file():
raise FileNotFoundError(f"Datei nicht gefunden: {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()
Warum Content-Hash? Datei umbenennen/verschieben = Cache-Treffer. Inhaltsänderung = automatische Invalidierung. Keine Indexdatei nötig.
2. Frozen-Dataclass für Cache-Eintrag
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class CacheEntry:
file_hash: str
source_path: str
document: ExtractedDocument # Das gecachte Ergebnis
3. Dateibasierte Cache-Speicherung
Jeder Cache-Eintrag wird als {hash}.json gespeichert – O(1)-Lookup per Hash, keine Indexdatei nötig.
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 # Korruption als Cache-Miss behandeln
4. Service-Layer-Wrapper (SRP)
Die Verarbeitungsfunktion bleibt rein. Caching wird als separate Service-Schicht hinzugefügt.
def extract_with_cache(
file_path: Path,
*,
cache_enabled: bool = True,
cache_dir: Path = Path(".cache"),
) -> ExtractedDocument:
"""Service-Schicht: Cache-Check -> Extraktion -> Cache-Schreiben."""
if not cache_enabled:
return extract_text(file_path) # Reine Funktion, kein Cache-Wissen
file_hash = compute_file_hash(file_path)
# Cache prüfen
cached = read_cache(cache_dir, file_hash)
if cached is not None:
logger.info("Cache-Treffer: %s (hash=%s)", file_path.name, file_hash[:12])
return cached.document
# Cache-Miss -> extrahieren -> speichern
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
Wichtige Designentscheidungen
| Entscheidung | Begründung |
|---|---|
| SHA-256-Inhaltshash | Pfadunabhängig, invalidiert automatisch bei Inhaltsänderung |
{hash}.json-Dateibenennung | O(1)-Lookup, keine Indexdatei nötig |
| Service-Layer-Wrapper | SRP: Extraktion bleibt rein, Cache ist separate Sorge |
| Manuelle JSON-Serialisierung | Volle Kontrolle über Frozen-Dataclass-Serialisierung |
Korruption gibt None zurück | Graceful Degradation, wird beim nächsten Durchlauf erneut verarbeitet |
cache_dir.mkdir(parents=True) | Verzögerte Verzeichniserstellung beim ersten Schreiben |
Best Practices
- Inhalt hashen, nicht Pfade – Pfade ändern sich, die Inhaltsidentität nicht
- Große Dateien chunkweise hashen – vermeidet das Laden ganzer Dateien in den Speicher
- Verarbeitungsfunktionen rein halten – sie sollten nichts vom Caching wissen
- Cache-Treffer/-Fehlschläge loggen mit gekürzten Hashes zum Debuggen
- Korruption elegant behandeln – ungültige Cache-Einträge als Fehlschlag betrachten, nie abstürzen
Anti-Patterns vermeiden
# SCHLECHT: Pfadbasiertes Caching (bricht bei Dateiverschiebung/-umbenennung)
cache = {"/path/to/file.pdf": result}
# SCHLECHT: Caching-Logik innerhalb der Verarbeitungsfunktion (SRP-Verletzung)
def extract_text(path, *, cache_enabled=False, cache_dir=None):
if cache_enabled: # Jetzt hat diese Funktion zwei Verantwortlichkeiten
...
# SCHLECHT: dataclasses.asdict() mit verschachtelten Frozen-Dataclasses
# (kann bei komplexen verschachtelten Typen Probleme verursachen)
data = dataclasses.asdict(entry) # Stattdessen manuelle Serialisierung verwenden
Wann verwenden
- Dateiverarbeitungspipelines (PDF-Parsing, OCR, Textextraktion, Bildanalyse)
- CLI-Tools, die von
--cache/--no-cache-Optionen profitieren - Batch-Verarbeitung, bei der dieselben Dateien über mehrere Läufe hinweg auftauchen
- Nachträgliches Hinzufügen von Caching zu bestehenden, reinen Funktionen ohne deren Änderung
Wann NICHT verwenden
- Daten, die immer aktuell sein müssen (Echtzeit-Feeds)
- Cache-Einträge, die extrem groß wären (stattdessen Streaming in Betracht ziehen)
- Ergebnisse, die von Parametern außerhalb des Dateiinhalts abhängen (z. B. unterschiedliche Extraktionskonfigurationen)