Scrapy Proxy Middleware: Residential Proxies richtig integrieren

Ein code-first Deep-Dive in ScraPys Downloader-Middleware-Modell: Eigenes Proxy-Rotation-Middleware, Retry-Logik, JS-Rendering und Monitoring für Produktionsscraping mit Residential Proxies.

Scrapy Proxy Middleware: Residential Proxies richtig integrieren

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 Middleware

scrapy-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:

Kriteriumscrapy-rotating-proxiesCustom Middleware
Einrichtung3 Zeilen in settings.py~150 Zeilen Middleware-Klasse
Proxy-QuelleStatische Liste in SettingsDynamisch generiert (API, Datei, Geo-Target)
Sticky SessionsÜber rotating_proxies_key MetaVoll customisierbar
Ban-ErkennungStatus-Codes konfigurierbarStatus-Codes + Content-Heuristiken
Residential ProxiesKein natives Geo-TargetingGeo-Flag direkt im Username
StatistikenBasic (Scrapy Stats)Custom-Metriken pro Proxy
WartungLetztes Release 2018Eigener Code = eigene Wartung
Retry-LogikEinfaches Retry mit nächstem ProxyRetry + 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 response

Externe Ü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 HttpProxyMiddleware durch 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-playwright mit Proxy im playwright_context_kwargs ist 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-compose mit 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.

Bereit loszulegen?

Zugang zu über 50 Mio. Residential-IPs in über 148 Ländern mit KI-gesteuerter Filterung.

Preise ansehenResidential Proxies
← Zurück zum Blog