Warum Scrapy Proxy Middleware dein Scraping-Projekt überlebenswichtig macht
Wer mit Scrapy im Produktivbetrieb scrapet, kennt das Problem: Nach 200 Requests kommt der erste 403, dann 429, und plötzlich ist die eigene IP gebannt. Die Lösung heißt Scrapy proxy middleware — aber sie richtig in den Downloader zu integrieren, ist mehr als ein http_proxy-Setting in settings.py. Man braucht Rotation, Fehlerbehandlung pro Request, Sticky Sessions für Login-Flows und Monitoring, das Bans erkennt, bevor sie die Pipeline blockieren.
Dieser Guide geht tief in ScraPys Middleware-Architektur, zeigt einen vollständigen Proxy-Rotations-Middleware mit Scrapy residential proxies, vergleicht Community-Lösungen mit einem Custom-Ansatz und deckt Deployment sowie Monitoring ab. Alles code-first, kopierbereit und für den Produktionseinsatz gedacht.
ScraPys Downloader-Middleware-Modell: Wo Proxies andocken
ScraPys Downloader ist eine Pipeline aus Middleware-Klassen, die Requests und Responses auf ihrem Weg durchs System abfangen können. Jede Middleware hat vier Hooks:
process_request— wird aufgerufen, bevor der Request an den Downloader geht. Hier setzt du den Proxy.process_response— wird aufgerufen, nachdem die Response vom Server kommt. Hier erkennst du Bans.process_exception— wird aufgerufen, wenn der Request fehlschlägt (Timeout, Connection-Error). Hier rotierst du den Proxy.from_crawler— Klassenmethode für den Zugriff auf ScraPys Settings und Stats.
Die Middleware-Reihenfolge ist entscheidend. Scrapy verarbeitet process_request nach aufsteigender Priorität (niedrigere Zahl = früher). process_response und process_exception laufen in umgekehrter Reihenfolge. Die integrierte HttpProxyMiddleware hat Priority 560 — unser Custom-Middleware muss davor laufen, um den Proxy zu setzen, oder sie ersetzt die integrierte komplett.
# settings.py — Middleware-Prioritäten verstehen
DOWNLOADER_MIDDLEWARES = {
# Eigene Rotation muss VOR der eingebauten HttpProxyMiddleware laufen
'myproject.middlewares.ProxyRotationMiddleware': 550,
'myproject.middlewares.ProxyRetryMiddleware': 580,
# Eingebaute Middleware deaktivieren, wenn wir eigene Proxy-Logik haben
'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware': None,
}Eigene Proxy-Rotation-Middleware für Residential Proxies
Der Kern einer guten Scrapy proxy rotation ist: Jeder Request bekommt einen Proxy zugewiesen, der Proxy wird bei Fehlern automatisch gewechselt, und die Zuordnung bleibt für Sticky Sessions erhalten. Hier ist ein vollständiges Middleware-Klasse:
# middlewares.py
import random
import time
import logging
from urllib.parse import urlparse
from scrapy import signals
from scrapy.exceptions import IgnoreRequest
from scrapy.utils.request import fingerprint
logger = logging.getLogger(__name__)
class ProxyRotationMiddleware:
"""Scrapy Downloader Middleware für Proxy-Rotation mit Residential Proxies.
Unterstützt:
- Round-Robin und Random-Rotation
- Sticky Sessions via Request-Meta
- Geo-Targeting über country-Flag im Username
- Automatisches Retry mit neuem Proxy bei Fehlern
""
def __init__(self, proxy_list, strategy='round_robin'):
self.proxy_list = proxy_list
self.strategy = strategy
self.index = 0
self.stats = {} # proxy_url -> {success, fail, bans}
@classmethod
def from_crawler(cls, crawler):
username = crawler.settings.get('PROXYHAT_USERNAME')
password = crawler.settings.get('PROXYHAT_PASSWORD')
strategy = crawler.settings.get('PROXY_STRATEGY', 'round_robin')
# Proxy-Pool generieren — verschiedene Geo-Targets
countries = crawler.settings.getlist('PROXY_COUNTRIES', ['US', 'DE', 'GB'])
proxy_list = []
for country in countries:
proxy_url = (
f'http://{username}-country-{country}:{password}'
f'@gate.proxyhat.com:8080'
)
proxy_list.append(proxy_url)
# Fallback: einzelner Proxy ohne Geo-Target
if not proxy_list:
proxy_list.append(
f'http://{username}:{password}@gate.proxyhat.com:8080'
)
middleware = cls(proxy_list, strategy)
crawler.signals.connect(
middleware.spider_closed, signal=signals.spider_closed
)
return middleware
def _next_proxy(self):
if self.strategy == 'random':
return random.choice(self.proxy_list)
# Round-Robin
proxy = self.proxy_list[self.index % len(self.proxy_list)]
self.index += 1
return proxy
def process_request(self, request, spider):
# Sticky Session: Wenn Meta bereits einen Proxy hat, beibehalten
if request.meta.get('proxy'):
return None
# Request-spezifisches Geo-Target
country = request.meta.get('proxy_country')
if country:
username = spider.settings.get('PROXYHAT_USERNAME')
password = spider.settings.get('PROXYHAT_PASSWORD')
proxy = (
f'http://{username}-country-{country}:{password}'
f'@gate.proxyhat.com:8080'
)
else:
proxy = self._next_proxy()
request.meta['proxy'] = proxy
request.meta['proxy_assigned_at'] = time.time()
logger.debug(
f'Proxy zugewiesen: {self._mask_proxy(proxy)} '
f'für {request.url}'
)
return None
def process_response(self, request, response, spider):
proxy = request.meta.get('proxy', '')
# Ban-Erkennung
if response.status in (403, 429, 503):
self._record(proxy, 'ban')
logger.warning(
f'Ban erkannt: {response.status} '
f'via {self._mask_proxy(proxy)}'
)
# Retry mit neuem Proxy
return self._retry_with_new_proxy(request, spider)
if response.status == 200:
self._record(proxy, 'success')
return response
def process_exception(self, request, exception, spider):
proxy = request.meta.get('proxy', '')
self._record(proxy, 'fail')
logger.warning(
f'Proxy-Fehler: {type(exception).__name__} '
f'via {self._mask_proxy(proxy)}'
)
return self._retry_with_new_proxy(request, spider)
def _retry_with_new_proxy(self, request, spider):
retries = request.meta.get('proxy_retry_count', 0)
max_retries = spider.settings.getint('PROXY_MAX_RETRIES', 3)
if retries >= max_retries:
logger.error(f'Max Retries erreicht für {request.url}')
raise IgnoreRequest(f'Max proxy retries: {request.url}')
# Neuen Proxy zuweisen
new_proxy = self._next_proxy()
request.meta['proxy'] = new_proxy
request.meta['proxy_retry_count'] = retries + 1
request.dont_filter = True # Scrapy erlaubt Duplicate-Request
logger.info(
f'Retry #{retries + 1} mit neuem Proxy für {request.url}'
)
return request
def _record(self, proxy, result_type):
if proxy not in self.stats:
self.stats[proxy] = {'success': 0, 'fail': 0, 'ban': 0}
self.stats[proxy][result_type] += 1
@staticmethod
def _mask_proxy(url):
parsed = urlparse(url)
return f'{parsed.scheme}://***:***@{parsed.hostname}:{parsed.port}'
def spider_closed(self, spider):
logger.info('=== Proxy-Statistiken ===')
for proxy, stats in self.stats.items():
total = sum(stats.values())
success_rate = (
stats['success'] / total * 100 if total > 0 else 0
)
logger.info(
f'{self._mask_proxy(proxy)}: '
f'{success_rate:.1f}% Success ({stats})'
)Dazugehörige settings.py-Konfiguration:
# settings.py
PROXYHAT_USERNAME = 'your_user'
PROXYHAT_PASSWORD = 'your_pass'
PROXY_STRATEGY = 'round_robin' # oder 'random'
PROXY_COUNTRIES = ['US', 'DE', 'GB', 'FR']
PROXY_MAX_RETRIES = 3
DOWNLOADER_MIDDLEWARES = {
'myproject.middlewares.ProxyRotationMiddleware': 550,
'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware': None,
}
# Timeout-Settings für Proxy-Betrieb
DOWNLOAD_TIMEOUT = 30
CONCURRENT_REQUESTS = 32
CONCURRENT_REQUESTS_PER_DOMAIN = 8
RETRY_ENABLED = False # Wir machen eigenes Retry in der Middlewarescrapy-rotating-proxies vs. Custom Middleware: Was passt besser?
Die Community-Lösung scrapy-rotating-proxies ist beliebt, aber sie hat Limitierungen, die im Produktionseinsatz schmerzen. Hier der Vergleich:
| Kriterium | scrapy-rotating-proxies | Custom Middleware |
|---|---|---|
| Einrichtung | 3 Zeilen in settings.py | ~150 Zeilen Middleware-Klasse |
| Proxy-Quelle | Statische Liste in Settings | Dynamisch generiert (API, Datei, Geo-Target) |
| Sticky Sessions | Über rotating_proxies_key Meta | Voll customisierbar |
| Ban-Erkennung | Status-Codes konfigurierbar | Status-Codes + Content-Heuristiken |
| Residential Proxies | Kein natives Geo-Targeting | Geo-Flag direkt im Username |
| Statistiken | Basic (Scrapy Stats) | Custom-Metriken pro Proxy |
| Wartung | Letztes Release 2018 | Eigener Code = eigene Wartung |
| Retry-Logik | Einfaches Retry mit nächstem Proxy | Retry + Backoff + Fallback-Pool |
Fazit: Für einfache Projekte mit einer statischen Proxy-Liste ist scrapy-rotating-proxies ausreichend. Sobald du Scrapy residential proxies mit Geo-Targeting, per-Request-Fehlerbehandlung oder Content-basierter Ban-Erkennung brauchst, ist eine Custom Middleware die bessere Wahl. Die Code-Komplexität ist überschaubar, und du hast volle Kontrolle.
Per-Request Proxy-Fehlerbehandlung: Retry mit Rotation
ScraPys integriertes RetryMiddleware weiß nichts von Proxies. Es wiederholt den gleichen Request mit dem gleichen Proxy — genau das, was du nicht willst, wenn der Proxy gebannt ist. Die Lösung: Eigenes Retry, das den Proxy bei jedem Versuch wechselt.
Das haben wir bereits im ProxyRotationMiddleware oben implementiert. Hier noch ein ergänzender Ansatz, der Content-basierte Ban-Erkennung hinzufügt:
# middlewares.py — Ergänzung zur Ban-Erkennung
class BanDetectionMiddleware:
"""Erkennt Bans anhand von Response-Content, nicht nur Status-Codes."""
BAN_SIGNATURES = [
b'access denied',
b'captcha',
b'cloudflare',
b'rate limit',
b'please verify you are a human',
]
def process_response(self, request, response, spider):
# Status-Code-basierte Bans
if response.status in (403, 429, 503):
logger.info(f'Status-Ban: {response.status} für {request.url}')
return self._mark_for_retry(request, response)
# Content-basierte Bans (200-Status, aber CAPTCHA-Seite)
if response.status == 200:
body_lower = response.body.lower()
for sig in self.BAN_SIGNATURES:
if sig in body_lower:
logger.info(
f'Content-Ban: "{sig.decode()}" in {request.url}'
)
return self._mark_for_retry(request, response)
return response
def _mark_for_retry(self, request, response):
retries = request.meta.get('ban_retry_count', 0)
if retries >= 3:
return response # Aufgeben, Response durchlassen
request.meta['ban_retry_count'] = retries + 1
request.meta['proxy'] = None # Proxy zurücksetzen
request.dont_filter = True
return request # Scrapy macht neuen Download</pre><p>Wichtig: Deaktiviere das integrierte Retry, damit es nicht gegen deine Rotations-Logik arbeitet:</p><pre><code># settings.py
RETRY_ENABLED = False
DOWNLOADER_MIDDLEWARES = {
'myproject.middlewares.ProxyRotationMiddleware': 550,
'myproject.middlewares.BanDetectionMiddleware': 555,
'scrapy.downloadermiddlewares.retry.RetryMiddleware': None,
'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware': None,
}JS-Heavy Sites: scrapy-splash und scrapy-playwright mit Proxies
Viele moderne Seiten rendern Inhalte per JavaScript. Scrapy allein sieht nur den leeren Shell-HTML. Zwei Lösungsansätze:
scrapy-splash (leichtgewichtig)
scrapy-splash nutzt einen externen Splash-Docker-Container. Der Proxy wird an Splash weitergereicht:
# settings.py
SPLASH_URL = 'http://splash:8050'
DOWNLOADER_MIDDLEWARES = {
'scrapy_splash.SplashDeduplicateMiddleware': 790,
'myproject.middlewares.ProxyRotationMiddleware': 550,
}
# Im Spider — Proxy an Splash übergeben
import scrapy
from scrapy_splash import SplashRequest
class JsSpider(scrapy.Spider):
name = 'js_spider'
def start_requests(self):
yield SplashRequest(
url='https://example.com/data',
callback=self.parse,
args={
'wait': 2,
'proxy': 'http://user-country-US:pass@gate.proxyhat.com:8080',
},
endpoint='render.html',
)scrapy-playwright (modern, mächtig)
scrapy-playwright steuert echte Chromium-Browser und unterstützt native Proxy-Konfiguration:
# settings.py
DOWNLOAD_HANDLERS = {
'http': 'scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler',
'https': 'scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler',
}
TWISTED_REACTOR = 'twisted.internet.asyncioreactor.AsyncioSelectorReactor'
# Im Spider
import scrapy
class PlaywrightSpider(scrapy.Spider):
name = 'pw_spider'
def start_requests(self):
yield scrapy.Request(
url='https://example.com/data',
meta={
'playwright': True,
'playwright_include_page': False,
'playwright_context_kwargs': {
'proxy': {
'server': 'http://gate.proxyhat.com:8080',
'username': 'user-country-US',
'password': 'pass',
}
}
}
)Tipp: Bei Playwright läuft der Proxy auf Browser-Ebene, nicht auf Scrapy-Ebene. Deine ProxyRotationMiddleware greift hier nicht — du musst den Proxy im playwright_context_kwargs setzen. Schreibe eine separate Middleware, die request.meta['playwright_context_kwargs'] manipuliert.
Deployment: Scrapyd, Docker und Headless-Fleets
Option 1: Scrapyd (klassisch)
Scrapyd ist der Standard für Scrapy-Deployment. Ein Daemon läuft auf dem Server, Spider werden per API deployt und gestartet.
# scrapyd.conf
[scrapyd]
bind_address = 0.0.0.0
http_port = 6800
max_proc = 4
# Deploy
crapyd-deploy production -p myproject
# Spider starten
curl http://localhost:6800/schedule.json -d \
'project=myproject&spider=my_spider'Proxy-Konfiguration landet in den Umgebungsvariablen des Servers — niemals hardcodiert in settings.py:
# settings.py — Umgebungsvariablen nutzen
import os
PROXYHAT_USERNAME = os.environ.get('PROXYHAT_USERNAME', '')
PROXYHAT_PASSWORD = os.environ.get('PROXYHAT_PASSWORD', '')Option 2: Docker + Cron (flexibel, reproduzierbar)
Für größere Fleets ist Docker die bessere Wahl. Jeder Container ist isoliert, skaliert horizontal und lässt sich mit Kubernetes oder Docker Compose orchestrieren:
# Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["scrapy", "crawl", "my_spider"]# docker-compose.yml — Horizontale Skalierung
version: '3.8'
services:
scraper:
build: .
environment:
- PROXYHAT_USERNAME=${PROXYHAT_USERNAME}
- PROXYHAT_PASSWORD=${PROXYHAT_PASSWORD}
deploy:
replicas: 4
restart_policy:
condition: on-failure
delay: 30s
logging:
driver: json-file
options:
max-size: "100m"
max-file: "3"Starten mit docker-compose up --scale scraper=8 — 8 parallele Container, jeder mit eigenem Proxy-Rotations-Pool.
Option 3: ScrapeOps / Zyte (Managed)
Wenn du keine Infrastruktur betreiben willst, bieten ScrapeOps und Zyte verwaltete Scrapy-Deployments. Der Nachteil: Du bist an deren Proxy-Infrastruktur gebunden und kannst Scrapy residential proxies von ProxyHat nicht direkt nutzen. Für maximale Kontrolle bleibt Docker die beste Wahl.
Monitoring: Success-Rates, Ban-Erkennung und Metriken
Ein Proxy-Pool ohne Monitoring ist ein Blindflug. Du musst wissen: Welche Proxies funktionieren? Wo liegen Bans? Wann muss ich rotieren?
Scrapy-Stats pro Proxy
Unsere ProxyRotationMiddleware oben sammelt bereits Statistiken. Erweitere sie mit ScraPys integriertem Stats-Collector:
# In der Middleware — Scrapy-Stats integrieren
def process_response(self, request, response, spider):
proxy = request.meta.get('proxy', 'direct')
country = request.meta.get('proxy_country', 'unknown')
# Scrapy-Stats
self.crawler.stats.inc_value(f'proxy/request_total')
self.crawler.stats.inc_value(f'proxy/country/{country}')
if response.status == 200:
self.crawler.stats.inc_value(f'proxy/success')
elif response.status in (403, 429, 503):
self.crawler.stats.inc_value(f'proxy/banned')
self.crawler.stats.inc_value(f'proxy/banned_country/{country}')
else:
self.crawler.stats.inc_value(f'proxy/other_{response.status}')
return responseExterne Überwachung mit Prometheus
Für Produktionsscraping empfiehlt sich die Integration mit Prometheus. Scrapy liefert die Stats per Telnet-Console oder per stats.py Extension. Alternativ: Ein einfacher HTTP-Endpoint in der Spider:
# extensions.py — Stats per HTTP bereitstellen
from twisted.web import server, resource
from scrapy import signals
import json
class StatsEndpoint:
def __init__(self, crawler):
self.crawler = crawler
self.port = crawler.settings.getint('STATS_PORT', 9300)
@classmethod
def from_crawler(cls, crawler):
ext = cls(crawler)
crawler.signals.connect(ext.start, signal=signals.spider_opened)
return ext
def start(self):
class StatsResource(resource.Resource):
isLeaf = True
def __init__(self, stats):
self.stats = stats
resource.Resource.__init__(self)
def render_GET(self, request):
request.setHeader(b'content-type', b'application/json')
return json.dumps(dict(self.stats.get_stats()),
default=str).encode()
root = StatsResource(self.crawler.stats)
site = server.Site(root)
from twisted.internet import reactor
reactor.listenTCP(self.port, site)Ban-Erkennungs-Heuristiken
Überwache diese Signale kontinuierlich:
- Success-Rate pro Country-Target: Fällt unter 80%? Rotiere das Country-Target.
- Antwortzeit-P90: Steigt die Latenz plötzlich? Der Provider drosselt möglicherweise.
- CAPTCHA-Rate: Mehr als 5% CAPTCHAs? Der Zielserver hat dein Verhalten erkannt.
- Verbindungsfehler-Rate: Timeouts über 10%? Proxy-Pool-Qualität prüfen.
Key Takeaways
1. Ersetze ScraPys integrierte
HttpProxyMiddlewaredurch eine Custom-Middleware, die Rotation, Retry und Geo-Targeting steuert.2. Scrapy residential proxies mit Geo-Targeting im Username (z.B.
user-country-DE) sind die flexibelste Lösung für produktives Scraping.3. Deaktiviere das integrierte
RetryMiddleware— es wiederholt mit demselben Proxy. Eigenes Retry + Proxy-Rotation in einer Middleware ist die bessere Architektur.4. Für JS-Heavy Sites:
scrapy-playwrightmit Proxy implaywright_context_kwargsist moderner als Splash.5. Monitoring ist kein Nice-to-Have — ohne per-Proxy-Statistiken fliegst du blind. Integriere Scrapy Stats und externe Metriken von Tag 1 an.
6. Docker +
docker-composemit Umgebungsvariablen für Credentials ist die robusteste Deployment-Strategie für skalierende Fleets.
Nächste Schritte
Wenn du Scrapy proxy middleware in Produktion bringst, starte mit der ProxyRotationMiddleware oben, passe die Geo-Targets an deine Use-Cases an und deploye mit Docker. Bei ProxyHat bekommst du Residential, Mobile und Datacenter-Proxies mit einem einzigen Gateway — gate.proxyhat.com:8080 für HTTP, :1080 für SOCKS5. Die Username-Flags machen Geo-Targeting und Sticky Sessions trivial.
Schau dir unsere Preise und verfügbaren Standorte an, oder lies mehr über Web-Scraping mit Proxies.






