Einen Scraping-API-Fallback zu deinem Scrapy-Projekt hinzufügen
Dein Scrapy-Spider funktioniert. Er zieht täglich Tausende Seiten, parst sauber und kostet dich nichts außer Bandbreite. Und dann füllen sich eines Morgens die Logs mit 403-Antworten oder, schlimmer noch, mit 200-Antworten, die eine Challenge-Seite statt der gewünschten Daten enthalten. Die Website hat eine Anti-Bot-Schicht hinzugefügt, und deine direkten Anfragen kommen nicht mehr durch.
Der Reflex ist, alles über eine Scraping-API zu leiten. Das funktioniert, ist aber verschwenderisch. Die meisten deiner Anfragen wurden nie blockiert, und sie alle über eine kostenpflichtige API zu schicken bedeutet, für Seiten zu zahlen, die du kostenlos hättest abrufen können. Ein besseres Muster ist ein Fallback: erst direkt abrufen und nur dann zur API eskalieren, wenn eine Anfrage tatsächlich fehlschlägt.
Diese Anleitung zeigt dir, wie du diesen Fallback als Downloader-Middleware in Scrapy einbaust - mit ordentlicher Erkennung, Retry-Logik und Fehlerbehandlung. Der Code ist vollständig, und du kannst ihn in ein bestehendes Projekt übernehmen.
Warum ein Fallback statt eines Alles-oder-nichts-Schalters
Eine Scraping-API ist eine kostenpflichtige Ressource. Jede Anfrage darüber kostet einen Credit. Wenn 90 % deiner Zielseiten ungeschützt sind, vervielfacht das Leiten aller Anfragen über die API deine Rechnung ohne jeden Nutzen.
Ein Fallback dreht die Logik um:
- Zuerst direkt. Scrapy sendet seine normale Anfrage. Wenn die Website mit sauberen Daten antwortet, bist du fertig und hast nichts bezahlt.
- Bei Fehlschlag eskalieren. Ist die Antwort blockiert, fehlend oder fehlerhaft, versucht die Middleware dieselbe URL erneut über die API.
- Sauber aufgeben. Nach einer festgelegten Anzahl von API-Versuchen wird die Anfrage verworfen oder protokolliert, damit sie nicht endlos in einer Schleife läuft.
So bleiben deine Credit-Ausgaben proportional dazu, wie schwierig die Website wirklich ist. Es bedeutet auch, dass du einen Spider auf eine gemischte Menge von Domains richten kannst, manche einfach und manche geschützt, ohne deinen Code in zwei Pfade aufzuteilen.
Wo der Fallback in Scrapy hineinpasst
Scrapy verarbeitet jede Anfrage durch eine Kette von Downloader-Middlewares vor und nach dem eigentlichen Download. Das ist der richtige Ort für einen Fallback, denn eine Middleware kann:
- Die Antwort sehen, die Scrapy auf die direkte Anfrage bekommen hat.
- Entscheiden, ob diese Antwort als Blockade zählt.
- Sie durch eine neue Anfrage ersetzen, die über die API geleitet wird.
Du fasst die Parsing-Logik deines Spiders überhaupt nicht an. Der Spider fragt eine URL an und erhält eine funktionierende Antwort. Ob diese Antwort direkt oder über die API kam, ist für ihn unsichtbar.
Eine Blockade erkennen
Der schwierigste Teil eines Fallbacks ist nicht der API-Aufruf. Es ist die Entscheidung, wann er ausgelöst wird. Eine naive Prüfung auf response.status == 403 übersieht den häufigsten modernen Fehlerfall: die weiche Blockade, bei der der Server 200 OK mit einer Challenge oder einer “Bestätige, dass du ein Mensch bist”-Seite statt echtem Inhalt zurückgibt.
Baue deine Erkennung um mehrere Signale herum auf:
- Harte Statuscodes.
403,429und503sind explizite Blockaden oder Rate-Limits. - Verdächtiger Body bei einer
200. Sehr kurze Bodys oder Bodys, die bekannte Challenge-Marker enthalten, sind weiche Blockaden. - Fehlender erwarteter Inhalt. Sollte die Seite ein Produktraster enthalten und der Selektor liefert nichts, behandle es als fehlgeschlagenen Abruf.
Hier ist ein Erkennungshelfer, den du pro Projekt anpassen kannst:
BLOCK_STATUS = {403, 429, 503}
CHALLENGE_MARKERS = (
b"captcha",
b"cf-challenge",
b"just a moment",
b"verify you are human",
)
def looks_blocked(response):
if response.status in BLOCK_STATUS:
return True
body = response.body[:20000].lower()
if len(response.body) < 500:
return True
return any(marker in body for marker in CHALLENGE_MARKERS)
Halte die Marker-Liste kurz und spezifisch. Passt du zu breit ab, schickst du saubere Seiten über die API und verbrennst Credits. Wenn du tiefer verstehen willst, warum eine 200 trotzdem eine Blockade sein kann, sieh dir unsere Anleitung zum Erkennen weicher Blockaden beim Web Scraping an.
Die Fallback-Middleware
Nun die Middleware selbst. Sie beobachtet jede Antwort, und wenn looks_blocked True zurückgibt, baut sie die Anfrage so um, dass sie statt zum Ursprung über die Scraping-API läuft. Sie verfolgt über ein Flag im Meta der Anfrage, wie oft sie eine bestimmte URL bereits erneut versucht hat, damit sie nie in eine Schleife gerät.
Der Fetch-Endpunkt von ScrapeUnblocker nimmt die Ziel-URL als Query-Parameter und einen API-Schlüssel in einem Header entgegen, sodass der Fallback nur eine Frage des Umschreibens der Anfrage-URL und des Austauschs der Header ist.
import logging
from urllib.parse import quote, urlencode
from scrapy.exceptions import IgnoreRequest
from scrapy.http import Request
logger = logging.getLogger(__name__)
API_ENDPOINT = "https://api.scrapeunblocker.com/getPageSource"
class ScrapingApiFallbackMiddleware:
def __init__(self, api_key, max_api_retries):
if not api_key:
raise ValueError("SCRAPEUNBLOCKER_API_KEY is not set")
self.api_key = api_key
self.max_api_retries = max_api_retries
@classmethod
def from_crawler(cls, crawler):
return cls(
api_key=crawler.settings.get("SCRAPEUNBLOCKER_API_KEY"),
max_api_retries=crawler.settings.getint("API_FALLBACK_MAX_RETRIES", 2),
)
def _build_api_request(self, original, attempt):
params = {"url": original.meta.get("origin_url", original.url)}
# Füge hier zusätzliche Optionen hinzu, falls eine Website sie braucht, z. B.:
# params["proxy_country"] = "us"
api_url = f"{API_ENDPOINT}?{urlencode(params, quote_via=quote)}"
return original.replace(
url=api_url,
method="POST",
headers={"x-scrapeunblocker-key": self.api_key},
meta={
**original.meta,
"origin_url": original.meta.get("origin_url", original.url),
"api_attempt": attempt,
"download_slot": "scrapeunblocker-api",
},
dont_filter=True,
)
def process_response(self, request, response, spider):
attempt = request.meta.get("api_attempt", 0)
if not looks_blocked(response):
return response
if attempt >= self.max_api_retries:
logger.warning(
"Giving up on %s after %d API attempts",
request.meta.get("origin_url", request.url),
attempt,
)
raise IgnoreRequest(f"Blocked after {attempt} API retries")
next_attempt = attempt + 1
logger.info(
"Blocked, routing through API (attempt %d): %s",
next_attempt,
request.meta.get("origin_url", request.url),
)
return self._build_api_request(request, next_attempt)
Ein paar Details, die man verstehen sollte:
origin_urlim Meta. Sobald eine Anfrage so umgeschrieben ist, dass sie auf die API zeigt, istrequest.urldie API-URL, nicht die gewünschte Seite. Das Speichern des ursprünglichen Ziels inmeta["origin_url"]erlaubt jedem Retry, den API-Aufruf aus der echten URL wieder aufzubauen.- Zähler
api_attempt. Jede Eskalation erhöht ihn. Erreicht ermax_api_retries, wirft die MiddlewareIgnoreRequestund stoppt. download_slot. Ein gemeinsamer Slot für API-Anfragen erlaubt dir, sie unabhängig von deinem direkten Verkehr zu drosseln (mehr dazu unten).dont_filter=True. Ohne dies würde Scrapys Dupe-Filter den Retry verwerfen, weil er auf eine URL zielt, die der Spider bereits besucht hat.
Einbindung in die Einstellungen
Aktiviere die Middleware und setze deinen Schlüssel und deine Limits in settings.py:
DOWNLOADER_MIDDLEWARES = {
"myproject.middlewares.ScrapingApiFallbackMiddleware": 610,
}
SCRAPEUNBLOCKER_API_KEY = "YOUR_API_KEY"
API_FALLBACK_MAX_RETRIES = 2
# Drossle den API-Verkehr getrennt von den direkten Anfragen.
DOWNLOAD_SLOTS = {
"scrapeunblocker-api": {"concurrency": 4, "delay": 0},
}
Die Prioritätszahl 610 platziert die Middleware direkt nach Scrapys eingebauter RetryMiddleware (die bei 550 sitzt), sodass gewöhnliche transiente Fehler zuerst direkt erneut versucht werden und nur anhaltende Blockaden zur API durchfallen. Lies deinen Schlüssel aus einer Umgebungsvariable, statt ihn fest im Code zu hinterlegen:
import os
SCRAPEUNBLOCKER_API_KEY = os.environ["SCRAPEUNBLOCKER_API_KEY"]
Fehler und Timeouts behandeln
Blockaden sind nicht der einzige Fehlerfall. Die API-Anfrage selbst kann ein Timeout haben oder einen Fehler zurückgeben, und das willst du behandeln, ohne dass der Crawl abstürzt. Füge einen process_exception-Hook zur selben Middleware hinzu, damit auch Netzwerkfehler bei einer direkten Anfrage den Fallback auslösen:
def process_exception(self, request, exception, spider):
attempt = request.meta.get("api_attempt", 0)
if attempt >= self.max_api_retries:
return None # überlasse Scrapy die normale Fehlerbehandlung
logger.info(
"Download error (%s), routing through API: %s",
type(exception).__name__,
request.meta.get("origin_url", request.url),
)
return self._build_api_request(request, attempt + 1)
Kombiniere dies mit Scrapys eigenen Retry- und Timeout-Einstellungen, damit ein langsamer API-Aufruf nicht den gesamten Crawl blockiert:
DOWNLOAD_TIMEOUT = 60
RETRY_ENABLED = True
RETRY_TIMES = 2
Da die API geschützte Seiten mit einem echten Browser rendert, sind ihre Antworten langsamer als ein reiner Abruf. Ein Timeout von 60 Sekunden gibt ihr Raum zum Arbeiten, ohne deinen Spider auf unbestimmte Zeit hängen zu lassen.
Den Fallback testen
Bevor du im großen Maßstab läufst, bestätige, dass sich beide Pfade richtig verhalten:
- Richte den Spider auf eine ungeschützte Seite und prüfe, dass die Logs keine API-Versuche zeigen. Das beweist, dass du keine Credits für einfache Seiten verschwendest.
- Richte ihn auf eine bekannte geschützte Seite und bestätige, dass du die Log-Zeile “routing through API” siehst, gefolgt von einem erfolgreichen Parse. Das beweist, dass die Eskalation funktioniert.
- Erzwinge einen Fehler, indem du
max_api_retriesauf0setzt, und bestätige, dass die Anfrage mit der Warnung “giving up” sauber verworfen wird, statt in einer Schleife zu laufen.
FAQ
Verlangsamt der Fallback meinen Crawl? Nur bei Seiten, die blockiert werden. Direkte Anfragen laufen mit voller Geschwindigkeit. Eskalierte Anfragen sind langsamer, weil die API sie mit einem echten Browser rendert - das ist der Preis dafür, überhaupt an der Blockade vorbeizukommen. Der separate Download-Slot verhindert, dass dieser langsamere Verkehr deine direkten Anfragen drosselt.
Wie vermeide ich, für Seiten zu zahlen, die gar nicht wirklich blockiert waren?
Passe looks_blocked an. Halte die Liste der Challenge-Marker spezifisch für das, was deine Ziele tatsächlich zurückgeben, und protokolliere jede Eskalation während des Testens, um Fehlalarme zu erkennen, bevor sie dich Geld kosten.
Kann ich das mit Scrapys AutoThrottle nutzen? Ja. AutoThrottle passt die Verzögerungen anhand der Antwortlatenz an. Die API-Anfragen in einen eigenen Download-Slot zu legen, verhindert, dass ihre höhere Latenz die Verzögerungsberechnung für deinen direkten Verkehr nach unten zieht.
Was, wenn eine Seite JavaScript-Rendering oder ein bestimmtes Land braucht?
Füge die entsprechenden Parameter zum params-Dictionary in _build_api_request hinzu. Setze zum Beispiel einen proxy_country-Wert, um über eine bestimmte Region zu leiten. Der Rest der Middleware bleibt gleich.
Fazit
Ein Scraping-API-Fallback gibt dir das Beste aus beiden Modellen: kostenlose, schnelle direkte Abrufe für den Großteil deiner Seiten und einen zuverlässigen Eskalationspfad für die Handvoll Domains, die sich wehren. Das Ganze lebt in einer einzigen Downloader-Middleware, sodass deine Spider sauber bleiben und deine Credit-Ausgaben proportional dazu bleiben, wie schwierig jede Website wirklich ist.
Wenn du den Eskalationspfad ausprobieren willst: ScrapeUnblocker übernimmt den Anti-Bot-Bypass und das Browser-Rendering hinter einem einzigen Endpunkt, abgerechnet mit einem Credit pro Anfrage inklusive JavaScript-Rendering. Übernimm die obige Middleware in dein Projekt, richte SCRAPEUNBLOCKER_API_KEY auf deinen Schlüssel, und deine bestehenden Spider laufen weiter, während die schwierigen Seiten leise wieder zu funktionieren beginnen.
ScrapeUnblocker kostenlos testen
Über 95 % Erfolgsquote · ab 0,55 € pro 1.000 Aufrufe · 500 kostenlose Anfragen bei der Registrierung.