Schneller Code ist der Unterschied zwischen einem Skript, das man gerne ausführt, und einem, vor dem man sich scheut. Niemand wartet gerne auf eine langsame Antwort, sei es eine ladende Webseite, ein trainierendes Modell oder eine Funktion, die dasselbe Ergebnis neu berechnet, das sie eine Sekunde zuvor schon geliefert hat. Eine der einfachsten Methoden, Python zu beschleunigen, ist Caching: das Ergebnis aufwendiger Arbeit aufbewahren, damit man es beim nächsten Bedarf sofort zurückgeben kann.

Dieser Leitfaden erklärt, was ein Cache ist und wann er tatsächlich nützt, und führt dann der Reihe nach durch die praktischen Werkzeuge: einen manuellen Dictionary-Cache, den man selbst schreibt, die eingebauten Dekoratoren functools.lru_cache und @cache, zeitbasierte Caches mit cachetools und schließlich das Cachen von HTTP-Antworten, damit ein Scraper dieselbe Seite nicht zweimal abruft. Jedes Code-Snippet ist praxistauglich und direkt verwendbar.

Was Sie bauen werden

Am Ende haben Sie einen kleinen Werkzeugkasten mit Caching-Mustern, die Sie in jedes Projekt einbauen können. Jedes zielt auf eine andere Art wiederkehrender Arbeit ab.

  • Ein manueller Memoization-Dekorator. Ein Dictionary-basierter Wrapper, der Ergebnisse nach den Argumenten einer Funktion verschlüsselt speichert.
  • lru_cache und @cache. Die Dekoratoren der Standardbibliothek, die in einer Zeile einen begrenzten oder unbegrenzten Cache liefern.
  • Ein TTL-Cache. Ein cachetools-Cache, dessen Einträge nach einer festgelegten Anzahl von Sekunden ablaufen, für Daten, die veralten.
  • Ein gecachter Fetcher. Eine Funktion, die HTTP-Antworten zwischenspeichert, damit wiederholte Anfragen für dieselbe URL sofort zurückkehren, ohne das Netzwerk zu belasten.

Was ist ein Cache, und wann hilft er?

Ein Cache ist ein temporärer Speicher für Daten, die teuer in der Erzeugung oder im Abruf sind. Statt ein Ergebnis jedes Mal neu zu berechnen oder es aus einer Datenbank oder einer API erneut anzufordern, bewahrt man eine Kopie an einem schnellen Ort auf (in der Regel im Arbeitsspeicher) und liest sie beim nächsten Aufruf zurück. Der erste Aufruf trägt die vollen Kosten; jeder weitere Aufruf danach ist nahezu kostenlos.

Caching lohnt sich in drei wiederkehrenden Situationen:

  • Schnellerer Zugriff. Das Lesen aus einem In-Memory-Cache ist weit schneller als das Neuberechnen eines Ergebnisses oder das Abrufen aus einer langsameren Quelle wie einem Datenträger, einer Datenbank oder einer Remote-API.
  • Weniger Last. Jeder Cache-Treffer ist eine Anfrage weniger an die Datenbank oder einen externen Dienst. Das entlastet diese Systeme und hilft, Engpässe und Rate-Limits zu vermeiden.
  • Bessere Nutzererfahrung. Geringere Latenz bedeutet schnellere Seiten und flüssigere Interaktionen, was am meisten zählt, wenn Nutzer direkt warten.

Es lohnt sich, genau zu sein, wann Caching das falsche Werkzeug ist. Caching hilft nur, wenn dieselben Eingaben wiederholt vorkommen und das zugrunde liegende Ergebnis sich zwischen den Aufrufen nicht ändert (oder sich so langsam ändert, dass ein wenig Veralterung akzeptabel ist). Eine Funktion mit jedem Mal einem anderen Argument profitiert nicht davon, nur der Speicher-Overhead erhöht sich. Und ein Cache, der nie abläuft, kann veraltete Daten für immer liefern, weshalb Ablaufzeiten wichtig werden, sobald sich die Quelle ändern kann.

Ein kurzes Wort zu Eviction-Strategien

Ein Cache kann nicht unbegrenzt wachsen; wenn er voll ist, muss er entscheiden, was verworfen wird. Diese Entscheidung ist die Eviction-Strategie, und die richtige hängt davon ab, wie auf die Daten zugegriffen wird.

  • FIFO (First-In, First-Out). Der älteste Eintrag wird zuerst verworfen, unabhängig davon, wie oft er genutzt wird. Einfach und ein vernünftiger Standard, wenn die Zugreifreihenfolge ungefähr der Nützlichkeit entspricht.
  • LRU (Least Recently Used). Der Eintrag, auf den am längsten nicht zugegriffen wurde, wird zuerst verworfen. Das ist der Hauptansatz für Web- und Datenbank-Caches, da kürzlich genutzte Daten dazu neigen, bald wieder genutzt zu werden.
  • LFU (Least Frequently Used). Der Eintrag mit den wenigsten Treffern wird zuerst verworfen, was Daten bevorzugt, die konstant beliebt sind, gegenüber solchen, die einmalig einen Spitzenwert hatten.
  • TTL (Time To Live). Jeder Eintrag trägt eine Ablaufzeit, und veraltete Einträge werden nach einer festen Anzahl von Sekunden verworfen, unabhängig davon, ob der Cache voll ist. Das ist das Mittel der Wahl, wenn sich die Quelldaten im Laufe der Zeit ändern.

Die Standardbibliothek bietet LRU von Haus aus. Für TTL und LFU greift man auf eine kleine Drittanbieter-Bibliothek zurück, die weiter unten behandelt wird.

Voraussetzungen

Grundlegendes Python. Sie sollten mit dem Schreiben von Funktionen, dem Ausführen eines Skripts und dem Installieren von Paketen mit pip vertraut sein. Kenntnisse über Dekoratoren sind hilfreich, aber nicht erforderlich, da der erste Abschnitt einen von Grund auf aufbaut.

Python 3.9 oder höher. Prüfen Sie Ihre Version mit python --version. Der unten verwendete @cache-Dekorator wurde in Python 3.9 hinzugefügt; alles andere funktioniert ab 3.2 aufwärts. Wenn Sie Python installieren müssen, holen Sie es von python.org.

Manuelles Caching mit einem Dekorator

Der klarste Weg, Caching zu verstehen, ist, eines selbst zu schreiben. Ein Dekorator ist einfach eine Funktion, die eine andere Funktion umschließt, und ein Caching-Dekorator speichert jedes Ergebnis in einem Dictionary, das nach den verwendeten Argumenten aufgeschlüsselt ist. Kommen dieselben Argumente erneut herein, gibt er den gespeicherten Wert zurück, anstatt die umschlossene Funktion auszuführen.

python
import requests

def memoize(func):
    cache = {}

    def wrapper(*args):
        if args in cache:
            return cache[args]
        result = func(*args)
        cache[args] = result
        return result

    return wrapper

@memoize
def get_html(url):
    # Only the first call for a given URL hits the network
    response = requests.get(url, timeout=10)
    return response.text

print(get_html("https://example.com"))
print(get_html("https://example.com"))  # served from cache, no request

Der erste Aufruf von get_html ruft die Seite ab und speichert den Inhalt unter seiner URL. Der zweite Aufruf mit derselben URL findet den Eintrag bereits in cache und gibt ihn zurück, ohne das Netzwerk anzutasten. Der Schlüssel ist das args-Tupel, weshalb dieses Muster nur mit hashbaren, positionellen Argumenten funktioniert: Listen und Dicts können keine Dictionary-Schlüssel sein, und Schlüsselwortargumente werden hier ignoriert. Genau diese Einschränkung ist der Grund, warum die Version der Standardbibliothek existiert, und die schauen wir uns als Nächstes an.

Cache the result, not the side effect

Memoization setzt voraus, dass die umschlossene Funktion eine reine Suche ist: gleiche Eingabe, gleiche Ausgabe, keine wichtigen Nebeneffekte. Eine Funktion zu cachen, die in eine Datei schreibt oder globalen Zustand verändert, überspringt diese Arbeit bei Wiederholungsaufrufen, was in der Regel ein Fehler ist. Cachen Sie Funktionen, die einen Wert berechnen oder abrufen und zurückgeben.

functools.lru_cache und @cache

Python liefert einen produktionsreifen Caching-Dekorator im functools-Modul, sodass man selten einen eigenen schreiben muss. lru_cache speichert Ergebnisse zwischen und verwirft beim Erreichen der Größengrenze den zuletzt am wenigsten genutzten Eintrag, um Platz zu schaffen. Die Obergrenze legt man mit maxsize fest.

python
from functools import lru_cache

@lru_cache(maxsize=128)
def fib(n):
    return n if n < 2 else fib(n - 1) + fib(n - 2)

print(fib(50))
print(fib.cache_info())

Ohne Cache berechnet ein naives rekursives fib(50) dieselben Teilprobleme Milliarden Male neu und braucht eine Ewigkeit. Mit lru_cache wird jeder Wert von n einmal berechnet und wiederverwendet, was eine exponentielle Funktion in eine lineare verwandelt. Die Methode cache_info() ist ein kleiner Bonus: Sie berichtet Treffer, Fehlschläge und die aktuelle Größe, sodass man bestätigen kann, dass der Cache seine Arbeit tut. Hier ist das erste Ergebnis, das man ausführen und überprüfen kann.

json
12586269025
CacheInfo(hits=48, misses=51, maxsize=128, currsize=51)

Wenn man überhaupt keine Größenbeschränkung möchte, ist @cache (hinzugefügt in Python 3.9) lru_cache(maxsize=None) mit einem kürzeren Namen. Es verwirft nie, also greift man nur darauf zurück, wenn der Satz möglicher Eingaben begrenzt und klein genug ist, um im Speicher zu bleiben.

python
from functools import cache

@cache
def expensive_computation(x, y):
    return x * y

print(expensive_computation(5, 6))

Beide Dekoratoren verwenden jeden von der Funktion empfangenen Wert als Schlüssel, positionell und als Schlüsselwort, und beide erfordern, dass diese Argumente hashbar sind. Wenn man eine Methode auf einer Klasse oder eine Funktion cachen muss, die nicht-hashbare Eingaben akzeptiert, muss man die Eingabe anpassen (zum Beispiel eine Liste in ein Tupel umwandeln), bevor sie den gecachten Aufruf erreicht.

Zeitbasierte Caches mit cachetools

Die Dekoratoren der Standardbibliothek lassen einen Eintrag niemals von selbst ablaufen; ein LRU-Eintrag geht nur, wenn der Cache voll ist und etwas Neueres ihn verdrängt. Das ist für reine Berechnungen in Ordnung, aber falsch für Daten, die veralten, wie etwa ein Preis, ein Wechselkurs oder eine API-Antwort, die sich im Laufe des Tages aktualisiert. Dafür braucht man einen TTL-Cache, bei dem jeder Eintrag nach einer festgelegten Anzahl von Sekunden abläuft. Die Bibliothek cachetools bietet genau das, zusammen mit LFU und anderen Strategien.

bash
pip install cachetools

Man wendet es mit dem @cached-Dekorator an und übergibt eine TTLCache-Instanz, die sowohl die maximale Größe als auch die Time-to-Live in Sekunden festlegt.

python
from cachetools import cached, TTLCache
import requests

# Up to 100 entries, each valid for 300 seconds
cache = TTLCache(maxsize=100, ttl=300)

@cached(cache)
def get_rate(symbol):
    response = requests.get(f"https://api.example.com/rate/{symbol}", timeout=10)
    return response.json()["price"]

print(get_rate("BTC"))  # fetches and caches
print(get_rate("BTC"))  # cached for up to 5 minutes

Für die nächsten fünf Minuten geben wiederholte Aufrufe für dasselbe Symbol den gecachten Preis ohne HTTP-Anfrage zurück. Nach Ablauf des TTL verfällt der Eintrag, und der folgende Aufruf ruft einen frischen Wert ab. Das liefert den Großteil der Caching-Geschwindigkeit, während die Daten einigermaßen aktuell bleiben, was das richtige Gleichgewicht für alles ist, was sich im Laufe der Zeit ändert. Wenn man stattdessen eine häufigkeitsbasierte Strategie benötigt, bietet cachetools auch LFUCache mit derselben Dekorator-Schnittstelle.

HTTP-Antworten für einen Scraper cachen

Caching ist am wichtigsten, wenn die aufwendige Operation eine Netzwerkanfrage ist. Ein Scraper, der eine Liste abarbeitet, Links folgt oder fehlgeschlagene Seiten erneut versucht, fordert dieselbe URL häufig mehr als einmal in einem Durchlauf an, und jeder doppelte Abruf kostet Zeit und belastet das Ziel. Ein Antwort-Cache behebt das: jede URL einmal abrufen, den Inhalt speichern und Wiederholungen aus dem Arbeitsspeicher bedienen.

Das Muster kombiniert einen TTL-Cache mit einer einfachen Abruffunktion. Die Verschlüsselung nach der URL stellt sicher, dass dieselbe Seite innerhalb des TTL-Fensters nur einmal heruntergeladen wird.

python
from cachetools import cached, TTLCache
import requests

page_cache = TTLCache(maxsize=500, ttl=3600)
headers = {"User-Agent": "Mozilla/5.0 (cache tutorial)"}

@cached(page_cache)
def fetch(url):
    response = requests.get(url, headers=headers, timeout=10)
    response.raise_for_status()
    return response.text

urls = [
    "https://quotes.toscrape.com/page/1/",
    "https://quotes.toscrape.com/page/1/",  # duplicate, served from cache
    "https://quotes.toscrape.com/page/2/",
]

for url in urls:
    html = fetch(url)
    print(f"{len(html)} chars from {url}")

Die zweite Anfrage für Seite 1 verlässt Ihren Rechner nie; sie gibt den beim ersten Aufruf gespeicherten Inhalt zurück. Mit einem einstündigen TTL werden beim erneuten Ausführen des Scrapers innerhalb dieses Zeitfensters die gecachten Seiten wiederverwendet statt neu heruntergeladen, was eine echte Hilfe ist, während man am Parser-Code iteriert und die Seite nicht bei jedem Testlauf belasten möchte. Zum Aufbau des Parsers selbst auf Basis des abgerufenen HTML, siehe how to scrape a website with Python.

Crawlbase Crawling API

Ein Antwort-Cache erspart Ihnen doppelte Abrufe, aber der erste Abruf jeder Seite muss trotzdem gelingen, und bei echten Zielen bedeutet das JavaScript-Rendering und das Überwinden von Bot-Abwehrmaßnahmen. Die Crawling API übernimmt das Rendering und rotiert serverseitig durch Residential-IPs und gibt fertiges HTML zurück, das man direkt in die gecachte fetch-Funktion oben einspeisen kann, sodass echte Seiten gecacht werden statt blockierter Antworten. Probieren Sie es im kostenlosen Tarif aus, bevor Sie Ihre eigene Headless-Flotte und Ihren eigenen Proxy-Pool aufbauen.

Das Cachen von Antworten ist auch eine Höflichkeit gegenüber den Seiten, die man abruft. Jede Seite, die man aus dem Cache bedient, ist eine Anfrage weniger, die das Ziel bearbeiten muss, was den eigenen Footprint klein hält und den Scraper weniger wahrscheinlich Rate-Limits auslösen lässt. Das Kombinieren eines Caches mit vernünftiger Drosselung ist eine der einfachsten Möglichkeiten, ein wohlerzogener Client zu sein, ein Thema, das ausführlich in how to scrape websites without getting blocked behandelt wird.

Den richtigen Cache wählen

Mit vier Werkzeugen zur Auswahl hängt die Entscheidung meist von zwei Fragen ab: Ändern sich die Daten, und braucht man Speichergrenzen?

  • Reine Berechnungen, die sich nie ändern. @cache für einen unbegrenzten Cache verwenden oder lru_cache(maxsize=N), wenn der Eingaberaum groß ist und man eine Speicherobergrenze möchte.
  • Daten, die veralten. Einen cachetools-TTLCache verwenden, damit Einträge ablaufen und periodisch neu abgerufen werden.
  • Eine kleine benutzerdefinierte Strategie. Einen manuellen Dictionary-Dekorator schreiben, wenn man ein Verhalten benötigt, das die Bibliotheken nicht bieten, aber die Standardwerkzeuge bevorzugen, wenn sie passen.

Für einen breiteren Überblick über die Bibliotheken, die gut zu diesen Mustern passen, wenn man vom Caching zu vollständigen Extraktionspipelines übergeht, siehe the best Python web scraping libraries.

Zusammenfassung

Wichtigste Erkenntnisse

  • Caching tauscht Speicher gegen Geschwindigkeit. Das Ergebnis aufwendiger Arbeit speichern und bei Wiederholungsaufrufen zurückgeben, damit nur der erste Aufruf die vollen Kosten trägt.
  • Zuerst auf die Standardbibliothek zurückgreifen. functools.lru_cache liefert in einer Zeile einen begrenzten LRU-Cache, und @cache ist die unbegrenzte Version.
  • Für sich ändernde Daten einen TTL-Cache verwenden. cachetools.TTLCache lässt Einträge nach einer festgelegten Anzahl von Sekunden ablaufen, sodass man nie dauerhaft veraltete Werte liefert.
  • HTTP-Antworten cachen, um doppelte Abrufe zu vermeiden. Eine Abruffunktion nach der URL als Schlüssel zu versehen, verwandelt wiederholte Anfragen in sofortige Cache-Treffer und verringert die Last für das Ziel.
  • Nur reine, wiederholbare Arbeit cachen. Funktionen mit Nebeneffekten oder stets einzigartigen Argumenten profitieren nicht davon und können auf subtile Weise brechen.

Häufig gestellte Fragen

Was ist Caching in Python?

Caching in Python bedeutet, das Ergebnis eines aufwendigen Funktionsaufrufs oder Datenabrufs zu speichern, damit spätere Anfragen für dieselbe Eingabe aus einem schnellen temporären Speicher zurückgegeben werden, anstatt die Arbeit erneut zu erledigen. Der erste Aufruf berechnet oder ruft den Wert ab und speichert ihn; jeder weitere passende Aufruf liest die gespeicherte Kopie, was die Latenz reduziert und die Last auf dem entfernt, was den Wert erzeugt hat.

Wann sollte ich lru_cache statt @cache verwenden?

lru_cache(maxsize=N) verwenden, wenn der Satz möglicher Eingaben groß ist und man die Speichernutzung des Caches begrenzen möchte; sobald er voll ist, wird der am längsten nicht genutzte Eintrag verworfen. @cache verwenden, wenn der Eingaberaum klein und begrenzt ist und man jedes Ergebnis für immer behalten möchte, da es nie verwirft. Beide verhalten sich abgesehen von der Größenbeschränkung identisch.

Wie cache ich Daten, die nach einiger Zeit ablaufen?

Einen TTL-Cache aus der cachetools-Bibliothek verwenden. Eine TTLCache(maxsize=N, ttl=seconds) erstellen und mit dem @cached-Dekorator anwenden. Jeder Eintrag ist für die festgelegte Anzahl von Sekunden gültig; danach verfällt er und der nächste Aufruf ruft einen frischen Wert ab. Das ist das richtige Werkzeug für Preise, Kurse und API-Antworten, die sich im Laufe der Zeit aktualisieren.

Kann ich HTTP-Antworten in einem Scraper cachen?

Ja, und das ist einer der wertvollsten Anwendungsfälle für Caching. Die Abruffunktion in einen Cache einschließen, der nach der URL verschlüsselt ist, sodass jede Seite nur einmal innerhalb des Cache-Fensters heruntergeladen wird. Wiederholungen werden aus dem Speicher bedient, was Durchläufe beschleunigt und Anfragen an die Zielseite reduziert. Ein TTL-Cache funktioniert hier gut, damit gecachte Seiten nach einem vernünftigen Intervall aktualisiert werden.

Wann ist Caching die falsche Wahl?

Caching hilft nur, wenn dieselben Eingaben wiederholt vorkommen und das Ergebnis stabil genug ist, dass eine gespeicherte Kopie noch korrekt ist. Erhält eine Funktion bei jedem Aufruf ein anderes Argument, fügt der Cache nur Speicher-Overhead ohne Treffer hinzu. Hat die Funktion wichtige Nebeneffekte, überspringt Caching diese bei Wiederholungsaufrufen, was in der Regel ein Fehler ist. Und wenn sich die Quelldaten ständig ändern, einen kurzen TTL verwenden oder den Cache überspringen, anstatt veraltete Werte zu liefern.

Funktioniert lru_cache mit nicht-hashbaren Argumenten?

Nein. Die Dekoratoren der Standardbibliothek erstellen ihren Cache-Schlüssel aus den Argumenten, daher müssen diese hashbar sein. Listen, Dicts und Sets können nicht direkt verwendet werden. Wenn man einen Aufruf cachen muss, der eines dieser Typen entgegennimmt, muss man es zuerst in eine hashbare Form umwandeln, zum Beispiel eine Liste in ein Tupel umwandeln, bevor man es an die gecachte Funktion übergibt.

Jetzt loslegen

Crawlen Sie jede Website im großen Maßstab, ohne gegen die Infrastruktur zu kämpfen.

Crawlbase übernimmt Proxys, Fingerprints und CAPTCHAs, damit Ihr Team Datenpipelines ausliefert, statt Crawl-Infrastruktur zu pflegen. 1.000 Anfragen kostenlos, keine Karte erforderlich.

Self-Service · Kein Verkaufsgespräch erforderlich · Enterprise-Crawl-Volumen verfügbar