Erweiterte Funktionen¶
Einführung¶
Voraussetzungen
- Sie haben die Seite Erste Schritte gelesen und wissen, wie Sie einen einfachen Spider erstellen und starten.
Diese Seite erklärt die erweiterten Funktionen des Spider-Systems: parallele Anfragen steuern, pausieren und fortsetzen, Streaming, Lebenszyklus-Hooks, Statistiken und Protokollierung.
Parallele Anfragen steuern¶
Mit diesen Klassenattributen steuert das Spider-System, wie viele Anfragen es in welcher Zeit sendet:
| Attribut | Standard | Beschreibung |
|---|---|---|
concurrent_requests |
4 |
Höchste Anzahl gleichzeitig verarbeiteter Anfragen |
concurrent_requests_per_domain |
0 |
Höchste Anzahl paralleler Anfragen pro Domain (0 = keine Grenze pro Domain) |
download_delay |
0.0 |
Wartezeit vor jeder Anfrage in Sekunden |
robots_txt_obey |
False |
robots.txt-Regeln beachten (Disallow, Crawl-delay, Request-rate) |
class PoliteSpider(Spider):
name = "polite"
start_urls = ["https://example.com"]
# Be gentle with the server
concurrent_requests = 4
concurrent_requests_per_domain = 2
download_delay = 1.0 # Wait 1 second between requests
async def parse(self, response: Response):
yield {"title": response.css("title::text").get("")}
Wenn concurrent_requests_per_domain gesetzt ist, erhält jede Domain neben der globalen Grenze eine eigene Grenze für parallele Anfragen. So können Sie mehrere Domains gleichzeitig abrufen und insgesamt viele Anfragen verarbeiten, ohne einzelne Domains zu stark zu belasten.
Tip
Der Parameter download_delay fügt vor jeder Anfrage eine feste Wartezeit ein, unabhängig von der Domain. Damit begrenzen Sie die Anfragerate auf einfache Weise.
AutoThrottle¶
Ein fester download_delay ist eine Schätzung. Ist er zu niedrig, werden Sie gesperrt. Ist er zu hoch, dauert der Crawl die ganze Nacht. AutoThrottle misst stattdessen, wie schnell die Website antwortet, und passt die Wartezeit für jede Domain an. Bei schnellen Servern geht es schneller, bei langsamen oder sperrenden Servern langsamer.
| Attribut | Standard | Beschreibung |
|---|---|---|
autothrottle_enabled |
False |
Automatische Anpassung der Wartezeit einschalten |
autothrottle_start_delay |
5.0 |
Wartezeit vor der ersten Anfrage an eine Domain |
autothrottle_max_delay |
60.0 |
Höchste erlaubte Wartezeit |
autothrottle_target_concurrency |
None |
Angestrebte Anzahl paralleler Anfragen pro Domain, siehe unten |
autothrottle_block_backoff |
True |
Wartezeit einer Domain bei jeder Sperre verdoppeln |
class AdaptiveSpider(Spider):
name = "adaptive"
start_urls = ["https://example.com"]
concurrent_requests = 8
concurrent_requests_per_domain = 1
autothrottle_enabled = True
autothrottle_start_delay = 2.0
autothrottle_max_delay = 30.0
async def parse(self, response: Response):
yield {"title": response.css("title::text").get("")}
Nach jeder Antwort nähert sich die Wartezeit der Antwortzeit des Servers an. Antwortet eine Website in 0,5 Sekunden, stellt sich eine Wartezeit von ungefähr 0,5 Sekunden ein. Das entspricht etwa einer laufenden Anfrage. Die Zielanzahl paralleler Anfragen pro Domain wird in dieser Reihenfolge bestimmt: autothrottle_target_concurrency, wenn gesetzt, sonst concurrent_requests_per_domain, sonst 1. Meist müssen Sie also keinen eigenen Zielwert setzen. Die bereits konfigurierte Grenze pro Domain wird verwendet und die Wartezeit durch diesen Wert geteilt. Steigt die Antwortzeit stark an, wirkt sich das sofort aus und wird nicht erst gemittelt. Der Spider wird daher sofort langsamer, wenn der Server Probleme hat, und beschleunigt nur schrittweise.
Bei Sperren langsamer werden¶
Die Antwortzeit allein erkennt keine Anfragelimits. Eine Antwort mit 429, 403 oder einer CAPTCHA-Seite kommt oft schneller zurück als der eigentliche Inhalt. Nur nach Antwortzeit betrachtet wäre das ein Grund, schneller zu werden. Stattdessen verdoppelt jede fehlerhafte Antwort die Wartezeit der Domain. Dazu zählen Statuscodes außerhalb von 2xx und Antworten, die is_blocked() als gesperrt einstuft:
Solange die Website Anfragen ablehnt, wird der Spider weiter langsamer. Sobald wieder erfolgreiche Antworten kommen, senkt die normale Mittelwertbildung die Wartezeit schrittweise auf die tatsächliche Antwortzeit. Der Spider erholt sich selbst, ohne dass Sie ihn neu starten müssen.
Wenn die Website bei 429 oder 503 über den Header Retry-After eine genaue Wartezeit nennt, wird dieser Wert statt der Verdopplung verwendet. Sowohl die Zahlenform (Retry-After: 120) als auch das HTTP-Datumsformat werden unterstützt.
Mit autothrottle_block_backoff = False schalten Sie dieses Verhalten aus. Dann wird nur die Antwortzeit berücksichtigt. Eine Sperre verhindert lediglich, dass der Spider schneller wird.
Die endgültige Wartezeit pro Domain finden Sie in den Statistiken:
Hinweise:
download_delayund Wartezeiten aus robots.txt bilden die Untergrenze. Doch auch diese wird durchautothrottle_max_delaybegrenzt. Setzen Sie das Maximum mindestens so hoch wie die konfigurierte oder von robots.txt geforderte Wartezeit.concurrent_requests_per_domainhat zwei Aufgaben: Es begrenzt laufende Anfragen pro Domain und dient als Zielwert für AutoThrottle. Meist ist deshalb kein eigener Zielwert nötig. Wir empfehlen, diese Grenze zu setzen. Eine wartende Anfrage belegt nämlich einen Platz innerhalb der globalen Grenze. Ohne Grenze pro Domain können wartende Anfragen die Plätze belegen, die alle Domains gemeinsam nutzen.- Die gemessene Antwortzeit umfasst den gesamten Abruf, einschließlich interner Wiederholungen und bei Browser-Sitzungen auch der Zeit zum Rendern der Seite.
autothrottle_max_delaybegrenzt alle Wartezeiten, auchRetry-After. Fordert eine Website eine längere Pause, erhöhen Sieautothrottle_max_delay, um sie einzuhalten.- Die ermittelten Wartezeiten werden nicht in Checkpoints gespeichert. Nach dem Pausieren und Fortsetzen beginnt jede Domain wieder mit
autothrottle_start_delay.
uvloop verwenden¶
Die Methode start() nimmt den Parameter use_uvloop an. Damit verwendet sie, falls verfügbar, die schnellere Event-Loop-Implementierung uvloop/winloop:
Das kann Crawls mit vielen Ein- und Ausgabevorgängen beschleunigen. Sie müssen uvloop unter Linux/macOS oder winloop unter Windows getrennt installieren.
Pausieren und fortsetzen¶
Der Spider kann durch Checkpoints geordnet pausiert und fortgesetzt werden. Übergeben Sie dafür dem Konstruktor ein Verzeichnis als crawldir:
spider = MySpider(crawldir="crawl_data/my_spider")
result = spider.start()
if result.paused:
print("Crawl was paused. Run again to resume.")
else:
print("Crawl completed!")
Funktionsweise¶
- Pausieren: Drücken Sie während des Crawls
Ctrl+C. Der Spider wartet auf alle laufenden Anfragen, speichert offene Anfragen und Fingerprints bereits besuchter Anfragen in einem Checkpoint und beendet sich. - Sofort stoppen: Drücken Sie
Ctrl+Cein zweites Mal, um ohne Warten auf laufende Aufgaben zu stoppen. - Fortsetzen: Starten Sie den Spider erneut mit demselben
crawldir. Er erkennt den Checkpoint, stellt die Warteschlange und die Menge der bekannten Fingerprints wieder her und setzt den Crawl fort. Dabei wirdstart_requests()übersprungen. - Aufräumen: Wird ein Crawl normal beendet und nicht pausiert, werden die Checkpoint-Dateien automatisch gelöscht.
Auch während des Crawls werden regelmäßig Checkpoints gespeichert, standardmäßig alle 5 Minuten.
So ändern Sie den Abstand:
# Save checkpoint every 2 minutes
spider = MySpider(crawldir="crawl_data/my_spider", interval=120.0)
Beim Speichern eines Checkpoints wird eine temporäre Datei verwendet. Der vorherige Checkpoint wird erst ersetzt, wenn das Schreiben erfolgreich war.
Checkpoint-Daten
Wenn Sie crawldir aktivieren, müssen sich die Werte in offenen Anfragen, einschließlich meta und Anfrageargumenten, mit Pythons Modul pickle speichern lassen. Kann ein Wert nicht gespeichert werden, schlägt das Speichern des Checkpoints fehl und der Crawl stoppt. Ein Selector kann in einem Callback verwendet werden, lässt sich aber nicht auf diese Weise speichern.
Speichern Sie extrahierte Werte wie Zeichenfolgen oder Zahlen statt Selector-Objekten. Auch Listen und Dictionaries dürfen nur Werte enthalten, die sich speichern lassen:
Tip
Bei start() wird der Spider durch Ctrl+C auch ohne aktiviertes Checkpoint-System geordnet beendet. Ein weiterer Tastendruck, bevor er fertig ist, beendet ihn sofort.
Erkennen, ob ein Crawl fortgesetzt wird¶
Der Hook on_start() erhält das Flag resuming:
async def on_start(self, resuming: bool = False):
if resuming:
self.logger.info("Resuming from checkpoint!")
else:
self.logger.info("Starting fresh crawl")
Entwicklungsmodus¶
Wenn Sie die parse()-Logik eines Spiders überarbeiten, sind neue Anfragen an die Zielserver bei jedem Lauf langsam und unnötig. Im Entwicklungsmodus werden alle Antworten beim ersten Lauf auf der Festplatte gespeichert und später von dort gelesen. So können Sie Ihre Selektoren ändern und den Spider beliebig oft neu starten, ohne diese Seiten erneut abzurufen.
Setzen Sie dafür development_mode = True in Ihrem Spider:
class MySpider(Spider):
name = "my_spider"
start_urls = ["https://example.com"]
development_mode = True
async def parse(self, response: Response):
yield {"title": response.css("title::text").get("")}
Der erste Lauf ruft die Seiten normal ab und speichert jede Antwort auf der Festplatte. Bei späteren Läufen werden dieselben Anfragen aus dem Cache beantwortet. Der Seitenabruf entfällt. Bei robots_txt_obey=True werden die robots.txt-Regeln vor dem Lesen aus dem Cache geprüft.
Speicherort des Caches¶
Standardmäßig werden Antworten unter .scrapling_cache/{spider.name}/ gespeichert. Der Pfad ist relativ zum aktuellen Arbeitsverzeichnis, also dort, wo Sie den Spider gestartet haben, nicht dort, wo das Spider-Skript liegt. Mit development_cache_dir ändern Sie den Speicherort:
class MySpider(Spider):
name = "my_spider"
start_urls = ["https://example.com"]
development_mode = True
development_cache_dir = "/tmp/my_spider_cache"
Funktionsweise¶
- Cache-Schlüssel: Jede Antwort wird über den Fingerprint der Anfrage zugeordnet. Ändern Sie Attribute, die den Fingerprint beeinflussen (
fp_include_kwargs,fp_include_headers,fp_keep_fragments), wird die Seite erneut abgerufen. - Speicherformat: Pro Antwort wird eine JSON-Datei mit dem Namen
{fingerprint_hex}.jsonerstellt. Der Body ist mit Base64 kodiert, damit Binärdaten exakt erhalten bleiben. Schreibvorgänge sind atomar: temporäre Datei und Umbenennen. - Wiederverwendung: Bei einem Cache-Treffer überspringt die Engine den Seitenabruf samt
download_delay, Begrenzung der Anfragerate und Wiederholungen überis_blocked(). Die gespeicherte Antwort geht direkt an Ihren Callback. - Statistiken: Anfragen aus dem Cache zählen weiterhin für
requests_count,response_bytesund die Zähler pro Statuscode. Die Statistik sieht also wie bei einem normalen Crawl aus. Die zusätzlichen Zählercache_hitsundcache_misseszeigen, wie oft der Cache eine Antwort liefern konnte.
Cache leeren¶
Cache-Einträge laufen nicht automatisch ab. Löschen Sie für einen neuen Crawl das Cache-Verzeichnis oder rufen Sie die Methode clear() des Managers direkt auf.
Warning
Der Entwicklungsmodus ist nur für die Entwicklung gedacht. Gespeicherte Antworten laufen nie ab. Beim Lesen aus dem Cache werden Anfrageraten und Wiederholungen gesperrter Anfragen übersprungen. Setzen Sie einen Spider nicht mit development_mode = True im Produktivbetrieb ein.
Streaming¶
Verwenden Sie stream() statt start() für lang laufende Spider oder Anwendungen, die sofort auf extrahierte Datensätze zugreifen müssen:
import anyio
async def main():
spider = MySpider()
async for item in spider.stream():
print(f"Got item: {item}")
# Access real-time stats
print(f"Items so far: {spider.stats.items_scraped}")
print(f"Requests made: {spider.stats.requests_count}")
anyio.run(main)
Die wichtigsten Unterschiede zu start():
stream()muss aus einem asynchronen Kontext aufgerufen werden.- Datensätze werden einzeln direkt nach der Extraktion geliefert, statt in einer Liste gesammelt zu werden.
- Während des Durchlaufs erhalten Sie über
spider.statsaktuelle Statistiken.
Abstract
Alle über spider.stats verfügbaren Statistiken werden weiter unten erklärt.
Sie können Streaming auch mit Checkpoints verwenden. So lassen sich Oberflächen mit aktuellen Daten erstellen, in denen Crawls pausiert und fortgesetzt werden können.
import anyio
async def main():
spider = MySpider(crawldir="crawl_data/my_spider")
async for item in spider.stream():
print(f"Got item: {item}")
# Access real-time stats
print(f"Items so far: {spider.stats.items_scraped}")
print(f"Requests made: {spider.stats.requests_count}")
anyio.run(main)
spider.pause() können Sie den Spider im Beispiel auch aus dem Code heraus stoppen. Ohne aktiviertes Checkpoint-System wird der Crawl einfach beendet.
Lebenszyklus-Hooks¶
Der Spider bietet mehrere Hooks, die Sie überschreiben können. Damit ergänzen Sie eigenes Verhalten in verschiedenen Phasen des Crawls:
on_start¶
Wird vor Beginn des Crawls aufgerufen. Verwenden Sie ihn für Vorbereitungen, etwa das Laden von Daten oder Einrichten von Ressourcen:
async def on_start(self, resuming: bool = False):
self.logger.info("Spider starting up")
# Load seed URLs from a database, initialize counters, etc.
on_close¶
Wird nach dem Crawl aufgerufen, unabhängig davon, ob dieser abgeschlossen oder pausiert wurde. Verwenden Sie ihn zum Aufräumen:
async def on_close(self):
self.logger.info("Spider shutting down")
# Close database connections, flush buffers, etc.
on_error¶
Wird aufgerufen, wenn eine Anfrage mit einer Ausnahme fehlschlägt. Verwenden Sie ihn, um Fehler zu erfassen oder eine eigene Fehlerbehandlung einzubauen:
async def on_error(self, request: Request, error: Exception):
self.logger.error(f"Failed: {request.url} - {error}")
# Log to error tracker, save failed URL for later, etc.
on_scraped_item¶
Wird für jeden extrahierten Datensatz aufgerufen, bevor er zu den Ergebnissen hinzugefügt wird. Geben Sie den Datensatz unverändert oder geändert zurück, um ihn zu behalten. Geben Sie None zurück, um ihn zu verwerfen:
async def on_scraped_item(self, item: dict) -> dict | None:
# Drop items without a title
if not item.get("title"):
return None
# Modify items (e.g., add timestamps)
item["scraped_at"] = "2026-01-01"
return item
Tip
Mit diesem Hook können Sie Datensätze auch durch eigene Pipelines leiten und aus den Spider-Ergebnissen entfernen.
start_requests¶
Überschreiben Sie start_requests(), um die ersten Anfragen selbst zu erstellen, statt start_urls zu verwenden:
async def start_requests(self):
# POST request to log in first
yield Request(
"https://example.com/login",
method="POST",
data={"user": "admin", "pass": "secret"},
callback=self.after_login,
)
async def after_login(self, response: Response):
# Now crawl the authenticated pages
yield response.follow("/dashboard", callback=self.parse)
Ergebnisse und Statistiken¶
Das von start() zurückgegebene CrawlResult enthält sowohl die extrahierten Datensätze als auch ausführliche Statistiken:
result = MySpider().start()
# Items
print(f"Total items: {len(result.items)}")
result.items.to_json("output.json", indent=True)
# Did the crawl complete?
print(f"Completed: {result.completed}")
print(f"Paused: {result.paused}")
# Statistics
stats = result.stats
print(f"Requests: {stats.requests_count}")
print(f"Failed: {stats.failed_requests_count}")
print(f"Blocked: {stats.blocked_requests_count}")
print(f"Offsite filtered: {stats.offsite_requests_count}")
print(f"Robots.txt disallowed: {stats.robots_disallowed_count}")
print(f"Cache hits: {stats.cache_hits}")
print(f"Cache misses: {stats.cache_misses}")
print(f"Items scraped: {stats.items_scraped}")
print(f"Items dropped: {stats.items_dropped}")
print(f"Response bytes: {stats.response_bytes}")
print(f"Duration: {stats.elapsed_seconds:.1f}s")
print(f"Speed: {stats.requests_per_second:.1f} req/s")
Ausführliche Statistiken¶
Das Objekt CrawlStats erfasst detaillierte Angaben:
stats = result.stats
# Status code distribution
print(stats.response_status_count)
# {'status_200': 150, 'status_404': 3, 'status_403': 1}
# Bytes downloaded per domain
print(stats.domains_response_bytes)
# {'example.com': 1234567, 'api.example.com': 45678}
# Requests per session
print(stats.sessions_requests_count)
# {'http': 120, 'stealth': 34}
# Proxies used during the crawl
print(stats.proxies)
# ['http://proxy1:8080', 'http://proxy2:8080']
# Log level counts
print(stats.log_levels_counter)
# {'debug': 200, 'info': 50, 'warning': 3, 'error': 1, 'critical': 0}
# Timing information
print(stats.start_time) # Unix timestamp when crawl started
print(stats.end_time) # Unix timestamp when crawl finished
print(stats.download_delay) # The download delay used (seconds)
# Concurrency settings used
print(stats.concurrent_requests) # Global concurrency limit
print(stats.concurrent_requests_per_domain) # Per-domain concurrency limit
# AutoThrottle
print(stats.autothrottle_enabled) # Whether the adaptive delay was on
print(stats.autothrottle_delays) # Final delay per domain, {'example.com': 0.62}
# Custom stats (set by your spider code)
print(stats.custom_stats)
# {'login_attempts': 3, 'pages_with_errors': 5}
# Export everything as a dict
print(stats.to_dict())
Protokollierung¶
Der Spider hat einen eingebauten Logger, auf den Sie über self.logger zugreifen. Er verwendet bereits den Namen des Spiders und bietet diese Einstellungen:
| Attribut | Standard | Beschreibung |
|---|---|---|
logging_level |
logging.DEBUG |
Niedrigste protokollierte Meldungsstufe |
logging_format |
"[%(asctime)s]:({spider_name}) %(levelname)s: %(message)s" |
Format der Protokollmeldungen |
logging_date_format |
"%Y-%m-%d %H:%M:%S" |
Datumsformat in Protokollmeldungen |
log_file |
None |
Pfad einer Protokolldatei zusätzlich zur Konsolenausgabe |
log_file_encoding |
None |
Kodierung der Protokolldatei; None verwendet die Standardkodierung von Python |
import logging
class MySpider(Spider):
name = "my_spider"
start_urls = ["https://example.com"]
logging_level = logging.INFO
log_file = "logs/my_spider.log"
log_file_encoding = "utf-8"
async def parse(self, response: Response):
self.logger.info(f"Processing {response.url}")
yield {"title": response.css("title::text").get("")}
Das Verzeichnis der Protokolldatei wird bei Bedarf automatisch erstellt. Die Ausgabe in Konsole und Datei verwendet dasselbe Format.
Neue Protokolleinträge werden angehängt. Wenn die Datei bereits existiert, verwenden Sie ihre Kodierung.