Scrapy Proxy Middleware: Guida Completa alla Rotazione dei Proxy Residenziali

Scopri come integrare i proxy residenziali in Scrapy tramite middleware personalizzati, gestire i retry con rotazione IP e monitorare i tassi di successo per produzione.

Scrapy Proxy Middleware: Guida Completa alla Rotazione dei Proxy Residenziali

Perché il Proxy Middleware in Scrapy è Fondamentale

Se hai mai lanciato uno spider Scrapy contro un sito protetto da Cloudflare o PerimeterX, sai cosa succede: le prime 50 richieste funzionano, poi arriva il ban di massa. Il problema non è Scrapy — è che il tuo spider usa un singolo IP per centinaia di richieste al minuto. I server target vedono questo traffico per quello che è: automatizzato.

La soluzione non è semplicemente «aggiungere un proxy». La soluzione è integrare la rotazione dei proxy nel middleware layer di Scrapy, dove puoi controllare quale IP usare per ogni richiesta, come gestire i fallimenti e quando scartare un proxy bannato. Questa guida ti porta attraverso l'intero stack — dall'architettura del middleware al deployment in produzione.

L'Architettura del Downloader Middleware in Scrapy

Scrapy processa ogni richiesta attraverso una catena di downloader middleware prima che raggiunga il server remoto, e ogni risposta attraversa la stessa catena al ritorno. Questo è il punto di estensione naturale per i proxy.

La catena del middleware è definita in settings.py tramite un dizionario ordinato dove il valore numerico determina la priorità — valori più bassi vengono eseguiti prima:

DOWNLOADER_MIDDLEWARES = {
    'myproject.middlewares.ProxyRotationMiddleware': 543,
    'scrapy.downloadermiddlewares.retry.RetryMiddleware': 550,
    'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware': 560,
}

I middleware chiave per i proxy sono:

  • HttpProxyMiddleware (560) — legge http_proxy dall'ambiente o dal meta proxy della richiesta e imposta l'header appropriato.
  • RetryMiddleware (550) — ritenta le richieste fallite, ma di default non ruota il proxy.
  • Il tuo middleware personalizzato — si inserisce nella catena per assegnare proxy, gestire ban e coordinare i retry.

Il flusso è: Request → process_request(middleware) → Downloader → Response → process_response(middleware). Se un middleware solleva IgnoreRequest, la richiesta viene scartata. Se ritorna None, il prossimo middleware nella catena continua.

Il meta proxy — il punto di aggancio

Scrapy utilizza request.meta['proxy'] come URL del proxy per quella specifica richiesta. Qualsiasi middleware può impostare questo valore prima che HttpProxyMiddleware lo legga:

# In un middleware personalizzato
def process_request(self, request, spider):
    request.meta['proxy'] = 'http://user-country-US:pass@gate.proxyhat.com:8080'
    return None  # passa al prossimo middleware

Questo è il meccanismo fondamentale. Tutto il resto — rotazione, sticky sessions, geo-targeting — si costruisce sopra questo hook.

Implementare un ProxyRotationMiddleware Personalizzato

Il middleware built-in HttpProxyMiddleware applica un proxy statico. Per la rotazione residenziale hai bisogno di un middleware che selezioni un IP diverso per ogni richiesta, supporti sessioni sticky e gestisca il geo-targeting. Ecco un'implementazione completa:

import random
import time
from collections import defaultdict
from scrapy import signals
from scrapy.exceptions import IgnoreRequest
from scrapy.utils.request import fingerprint


class ProxyRotationMiddleware:
    """Rotazione proxy residenziali con ban tracking e sticky session."""

    def __init__(self, proxy_url, max_retries=3, sticky_timeout=600):
        self.proxy_url = proxy_url  # es. http://USERNAME:PASSWORD@gate.proxyhat.com:8080
        self.max_retries = max_retries
        self.sticky_timeout = sticky_timeout
        # session_id → timestamp di creazione
        self.sessions = {}
        # proxy_fingerprint → contatore ban
        self.ban_counts = defaultdict(int)
        # request_fingerprint → session_id per retry
        self.request_session = {}

    @classmethod
    def from_crawler(cls, crawler):
        proxy_url = crawler.settings.get(
            'PROXYHAT_URL',
            'http://USERNAME:PASSWORD@gate.proxyhat.com:8080'
        )
        max_retries = crawler.settings.get('PROXY_MAX_RETRIES', 3)
        sticky_timeout = crawler.settings.get('PROXY_STICKY_TIMEOUT', 600)
        middleware = cls(proxy_url, max_retries, sticky_timeout)
        crawler.signals.connect(middleware.spider_opened, signal=signals.spider_opened)
        return middleware

    def spider_opened(self, spider):
        spider.logger.info('ProxyRotationMiddleware attivo con %s', self.proxy_url.split('@')[-1])

    def _build_proxy_url(self, request):
        """Costruisce URL proxy con geo-targeting e sessione."""
        fp = fingerprint(request)
        # Se la richiesta ha già una sessione assegnata (retry), riutilizzala
        if fp in self.request_session:
            session_id = self.request_session[fp]
            # Controlla se la sessione è scaduta
            if time.time() - self.sessions.get(session_id, 0) < self.sticky_timeout:
                return self._inject_session(self.proxy_url, session_id, request)

        # Nuova sessione
        session_id = f'sess-{random.randint(100000, 999999)}'
        self.sessions[session_id] = time.time()
        self.request_session[fp] = session_id
        return self._inject_session(self.proxy_url, session_id, request)

    def _inject_session(self, base_url, session_id, request):
        """Inietta sessione e geo-targeting nell'username del proxy."""
        # base_url: http://user:pass@gate.proxyhat.com:8080
        country = request.meta.get('proxy_country', 'US')
        # Risultato: http://user-country-US-session-sess-123:pass@gate.proxyhat.com:8080
        parts = base_url.split('://')
        protocol = parts[0]
        rest = parts[1]
        credentials, host = rest.split('@')
        user, password = credentials.split(':')
        new_user = f'{user}-country-{country}-session-{session_id}'
        return f'{protocol}://{new_user}:{password}@{host}'

    def process_request(self, request, spider):
        proxy = self._build_proxy_url(request)
        request.meta['proxy'] = proxy
        request.meta['proxy_country'] = request.meta.get('proxy_country', 'US')
        # Traccia il tentativo corrente
        retry_times = request.meta.get('retry_times', 0)
        request.meta['retry_times'] = retry_times
        return None

    def process_response(self, request, response, spider):
        # Rileva ban — status 403, 429, o CAPTCHA pages
        if response.status in (403, 429):
            spider.logger.warning('Ban rilevato: %s status=%d', response.url, response.status)
            self.ban_counts[fingerprint(request)] += 1
            # Se non abbiamo superato il max retry, ritenta con nuovo proxy
            if request.meta.get('retry_times', 0) < self.max_retries:
                request.meta['retry_times'] = request.meta.get('retry_times', 0) + 1
                # Forza nuova sessione rimuovendo il mapping
                fp = fingerprint(request)
                if fp in self.request_session:
                    del self.request_session[fp]
                request.dont_filter = True
                return request  # ritenta
            else:
                spider.logger.error('Max retry raggiunto per %s', request.url)
        return response

    def process_exception(self, request, exception, spider):
        # Timeout, connection errors — ritenta con nuovo proxy
        if request.meta.get('retry_times', 0) < self.max_retries:
            request.meta['retry_times'] = request.meta.get('retry_times', 0) + 1
            fp = fingerprint(request)
            if fp in self.request_session:
                del self.request_session[fp]
            request.dont_filter = True
            spider.logger.info('Retry per eccezione: %s → %s', request.url, exception)
            return request
        return None

Questo middleware fa tre cose fondamentali: assegna un proxy diverso per ogni richiesta usando le flag di sessione di ProxyHat, mantiene sessioni sticky per un timeout configurabile, e ruota automaticamente il proxy quando rileva un ban (403/429) o un'eccezione di connessione.

Configurazione in settings.py:

# settings.py
PROXYHAT_URL = 'http://USERNAME:PASSWORD@gate.proxyhat.com:8080'
PROXY_MAX_RETRIES = 3
PROXY_STICKY_TIMEOUT = 600  # 10 minuti in secondi

DOWNLOADER_MIDDLEWARES = {
    'myproject.middlewares.ProxyRotationMiddleware': 543,
    'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware': 560,
}

# Disabilita il RetryMiddleware di default — il nostro gestisce i retry
RETRY_ENABLED = False

# Aumenta la concorrenza per sfruttare il pool residenziale
CONCURRENT_REQUESTS = 32
CONCURRENT_REQUESTS_PER_DOMAIN = 8
DOWNLOAD_TIMEOUT = 30

Nota importante: disabilitiamo il RetryMiddleware di Scrapy perché il nostro middleware gestisce i retry con rotazione proxy. Se entrambi fossero attivi, ogni richiesta bannata verrebbe ritentata due volte — una dal nostro middleware e una dal RetryMiddleware di default.

scrapy-rotating-proxies vs Middleware Personalizzato

La community offre scrapy-rotating-proxies, un pacchetto che gestisce rotazione e ban detection. Vale la pena considerarne i pro e i contro:

Caratteristicascrapy-rotating-proxiesMiddleware Personalizzato
ConfigurazioneLista di proxy in settings.pyURL singolo con rotazione lato provider
Ban detectionBasata su status code configurabiliPersonalizzabile per sito (403, 429, CAPTCHA)
Sticky sessionsNon supportate nativamenteSupporto completo via flag session
Geo-targetingManuale — devi filtrare i proxy per paesePer-request via flag country
Pool managementGestisci tu il pool di IPDelegato al provider (ProxyHat)
Proxy residenzialiNessun supporto specificoOttimizzato per proxy residenziali
ManutenzionePacchetto terze parti, aggiornamenti rariControlli tu il codice

Quando usare scrapy-rotating-proxies: hai una lista di proxy datacenter gratuiti o acquistati e vuoi rotazione rapida senza scrivere codice.

Quando usare un middleware personalizzato: usi proxy residenziali con sessioni sticky, geo-targeting per-request, e vuoi pieno controllo sulla logica di retry. Per la maggior parte dei casi di produzione con proxy a pagamento, il middleware personalizzato è la scelta migliore.

Gestire i Retry con Rotazione Proxy

Il pattern più comune di fallimento: una richiesta riceve status 403, il RetryMiddleware di Scrapy ritenta la stessa richiesta con lo stesso IP, e riceve di nuovo 403. È un ciclo inutile.

Il nostro middleware risolve questo problema rimuovendo il mapping della sessione quando rileva un ban, forzando la richiesta successiva a ottenere un nuovo IP. Ma c'è di più — puoi combinare questo con il RetryMiddleware di Scrapy per avere una strategia a due livelli:

# middlewares.py — RetryMiddleware personalizzato che ruota il proxy
from scrapy.downloadermiddlewares.retry import RetryMiddleware as BaseRetry
from scrapy.utils.request import fingerprint

class ProxyAwareRetryMiddleware(BaseRetry):
    """Retry che invalida la sessione proxy prima di ritentare."""

    def __init__(self, settings):
        super().__init__(settings)
        self.max_retry_times = settings.getint('RETRY_TIMES', 3)

    def process_response(self, request, response, spider):
        if request.meta.get('dont_retry', False):
            return response

        # Se è un ban, invalida la sessione proxy
        if response.status in (403, 429, 503):
            fp = fingerprint(request)
            # Segnala al ProxyRotationMiddleware di usare un nuovo IP
            if fp in spider.proxy_middleware.request_session if hasattr(spider, 'proxy_middleware') else {}:
                del spider.proxy_middleware.request_session[fp]
            spider.logger.info('Ban rilevato, ruoto proxy per %s', request.url)

        return super().process_response(request, response, spider)

    def process_exception(self, request, exception, spider):
        # Timeout e connection errors — ruota proxy
        fp = fingerprint(request)
        if hasattr(spider, 'proxy_middleware') and fp in spider.proxy_middleware.request_session:
            del spider.proxy_middleware.request_session[fp]
        return super().process_exception(request, exception, spider)

Con questo approccio, ogni retry usa automaticamente un IP diverso. Configuralo così:

DOWNLOADER_MIDDLEWARES = {
    'myproject.middlewares.ProxyRotationMiddleware': 543,
    'myproject.middlewares.ProxyAwareRetryMiddleware': 550,
}

# NON disabilitare RETRY_ENABLED qui — il nostro retry middleware lo usa
RETRY_TIMES = 3
RETRY_HTTP_CODES = [403, 429, 500, 502, 503, 504]

Siti JS-Heavy: scrapy-splash e scrapy-playwright con Proxy

I siti moderni spesso richiedono esecuzione JavaScript per renderizzare il contenuto. Scrapy gestisce questo scenario tramite scrapy-splash (per Splash, un renderer headless) o scrapy-playwright (per Playwright, che controlla browser reali).

Configurare scrapy-playwright con proxy residenziali

scrapy-playwright è diventato lo standard de facto per il rendering JavaScript in Scrapy. Ecco come integrare i proxy residenziali:

# settings.py
DOWNLOAD_HANDLERS = {
    'http': 'scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler',
    'https': 'scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler',
}

TWISTED_REACTOR = 'twisted.internet.asyncioreactor.AsyncioSelectorReactor'

PLAYWRIGHT_BROWSER_TYPE = 'chromium'

# Il proxy per Playwright va configurato per-request nel meta
PLAYWRIGHT_LAUNCH_OPTIONS = {
    'headless': True,
}

Nello spider, assegni il proxy per ogni richiesta tramite il meta playwright:

import scrapy
import random

class JSSpider(scrapy.Spider):
    name = 'js_spider'

    def start_requests(self):
        urls = ['https://example.com/data']
        for url in urls:
            session_id = f'sess-{random.randint(100000, 999999)}'
            yield scrapy.Request(
                url=url,
                callback=self.parse,
                meta={
                    'playwright': True,
                    'playwright_include_page': False,
                    'playwright_proxy': {
                        'server': 'http://gate.proxyhat.com:8080',
                        'username': f'USERNAME-country-US-session-{session_id}',
                        'password': 'PASSWORD',
                    },
                    'proxy_country': 'US',
                },
            )

    def parse(self, response):
        for item in response.css('.data-item'):
            yield {
                'title': item.css('h2::text').get(),
                'price': item.css('.price::text').get(),
            }

Per scrapy-splash, il pattern è simile ma passi il proxy attraverso i parametri di Splash:

# Con scrapy-splash
yield scrapy.Request(
    url=target_url,
    callback=self.parse,
    meta={
        'splash': {
            'args': {
                'render': 1,
                'proxy': 'http://gate.proxyhat.com:8080',
            },
            'endpoint': 'render.html',
        }
    }
)

Attenzione: con scrapy-splash, il proxy è configurato lato server Splash, non per-request. Se hai bisogno di rotazione per-request, devi usare un pool di istanze Splash o passare a scrapy-playwright che supporta proxy per-request nativamente.

Deployment: Scrapyd, Docker, e Oltre

Opzione 1: Docker + cron — semplice ed efficace

Per la maggior parte dei casi d'uso, un container Docker con cron è sufficiente:

# Dockerfile
FROM python:3.11-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .

# cron job per eseguire lo spider ogni ora
RUN echo "0 * * * * cd /app && scrapy crawl my_spider >> /var/log/spider.log 2>&1" | crontab -
CMD ["crontab", "-l", "&&", "tail", "-f", "/var/log/spider.log"]

Esegui con docker run -d --name spider my-spider-image. Per scaling orizzontale, usa Docker Compose con multiple istanze, ognuna con un diverso PROXYHAT_URL per distribuire il carico.

Opzione 2: Scrapyd — gestione centralizzata

Scrapyd offre un'API per deployare e schedulare spider. È utile se hai multipli spider che girano su diverse macchine:

# Deploy su Scrapyd
scrapyd-deploy

# Schedula un job
curl http://localhost:6800/schedule.json -d project=myproject -d spider=my_spider

Opzione 3: ScrapeOps — monitoraggio avanzato

ScrapeOps fornisce un dashboard per monitorare job, success rate e log. Si integra bene con Scrapy tramite il loro scrapeops-scrapy-proxy-sdk, ma se usi ProxyHat come provider, il nostro middleware personalizzato è più flessibile.

Monitoraggio: Success Rate, Ban Detection, e Per-IP Stats

In produzione, devi sapere quali IP funzionano e quali sono bannati. Ecco un'estensione Scrapy che raccoglie statistiche per paese e tipo di risposta:

# extensions.py
from scrapy import signals
from collections import defaultdict


class ProxyStatsExtension:
    """Raccoglie statistiche per-request sui proxy."""

    def __init__(self, stats):
        self.stats = stats
        self.country_stats = defaultdict(lambda: {'success': 0, 'ban': 0, 'error': 0})

    @classmethod
    def from_crawler(cls, crawler):
        ext = cls(crawler.stats)
        crawler.signals.connect(ext.request_scheduled, signal=signals.request_scheduled)
        crawler.signals.connect(ext.response_received, signal=signals.response_received)
        crawler.signals.connect(ext.request_dropped, signal=signals.request_dropped)
        return ext

    def request_scheduled(self, request, spider):
        country = request.meta.get('proxy_country', 'unknown')
        self.stats.inc_value(f'proxy/request/{country}')

    def response_received(self, response, spider):
        country = response.meta.get('proxy_country', 'unknown')
        if response.status >= 400:
            self.country_stats[country]['ban'] += 1
            self.stats.inc_value(f'proxy/ban/{country}')
        else:
            self.country_stats[country]['success'] += 1
            self.stats.inc_value(f'proxy/success/{country}')

    def request_dropped(self, request, spider):
        country = request.meta.get('proxy_country', 'unknown')
        self.country_stats[country]['error'] += 1
        self.stats.inc_value(f'proxy/error/{country}')

Abilita l'estensione in settings.py:

EXTENSIONS = {
    'myproject.extensions.ProxyStatsExtension': 500,
}

Con queste stats puoi monitorare il tasso di successo per paese — se i proxy US hanno un success rate del 95% ma quelli DE solo del 60%, sai dove ottimizzare.

Ban detection avanzata

Non tutti i ban restituiscono 403. Alcuni siti restituiscono 200 con una pagina CAPTCHA, o redirect a una pagina di verifica. Aggiungi questi pattern al tuo middleware:

# Nel process_response del ProxyRotationMiddleware
def _is_ban(self, response):
    """Rileva ban che non sono ovvi status code."""
    # Status code ban
    if response.status in (403, 429, 503):
        return True
    # CAPTCHA pages
    captcha_signals = ['captcha', 'cf-challenge', 'recaptcha', 'hcaptcha']
    body_lower = response.text[:5000].lower()
    if any(sig in body_lower for sig in captcha_signals):
        return True
    # Redirect a pagine di verifica
    if response.status == 200 and '/verify' in response.url:
        return True
    return False

Pattern di Scaling per Produzione

Quando passi da uno spider che gira su un laptop a un sistema che processa milioni di pagine al giorno, i pattern di scaling diventano critici:

Concorrenza e rate limiting

Con proxy residenziali, puoi aumentare la concorrenza significativamente perché ogni richiesta usa un IP diverso:

CONCURRENT_REQUESTS = 64
CONCURRENT_REQUESTS_PER_DOMAIN = 16
DOWNLOAD_DELAY = 0.5  # ancora utile per non saturare la banda

# AutoThrottle per adattarsi alla velocità del sito
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_START_DELAY = 0.5
AUTOTHROTTLE_MAX_DELAY = 5
AUTOTHROTTLE_TARGET_CONCURRENCY = 16

Container fleet con Docker Compose

Per scaling orizzontale, esegui multiple istanze dello spider con diverse configurazioni geo:

# docker-compose.yml
version: '3.8'
services:
  spider-us:
    build: .
    environment:
      - PROXY_COUNTRY=US
      - PROXYHAT_URL=http://user-country-US:pass@gate.proxyhat.com:8080
    command: scrapy crawl my_spider
  spider-de:
    build: .
    environment:
      - PROXY_COUNTRY=DE
      - PROXYHAT_URL=http://user-country-DE:pass@gate.proxyhat.com:8080
    command: scrapy crawl my_spider
  spider-uk:
    build: .
    environment:
      - PROXY_COUNTRY=UK
      - PROXYHAT_URL=http://user-country-GB:pass@gate.proxyhat.com:8080
    command: scrapy crawl my_spider

Item pipelines e deduplicazione

Con multiple istanze che girano in parallelo, hai bisogno di deduplicazione a livello di item. Usa un DuplicateFilter basato su Redis:

# pipelines.py
import redis

class RedisDuplicateFilterPipeline:
    def __init__(self, redis_url):
        self.redis = redis.from_url(redis_url)
        self.ttl = 86400  # 24 ore

    @classmethod
    def from_crawler(cls, crawler):
        return cls(crawler.settings.get('REDIS_URL', 'redis://localhost:6379'))

    def process_item(self, item, spider):
        item_id = item.get('url') or item.get('id')
        if self.redis.setnx(f'dedup:{item_id}', 1):
            self.redis.expire(f'dedup:{item_id}', self.ttl)
            return item
        raise DropItem(f'Duplicato: {item_id}')

Considerazioni Etiche e Legali

L'uso di proxy per web scraping solleva questioni importanti:

  • Rispetta robots.txt — usa ROBOTSTXT_OBEY = True a meno che tu non abbia una buona ragione per non farlo.
  • Rate limiting responsabile — anche con proxy residenziali, non bombardare un server. Usa DOWNLOAD_DELAY e AutoThrottle.
  • GDPR e CCPA — se raccogli dati personali, assicurati di essere in conformità. I proxy non ti esentano dalle obbligazioni legali.
  • ToS dei siti — molti siti proibiscono lo scraping nei loro Termini di Servizio. Valuta i rischi legali.

Per approfondire le best practice per lo scraping etico, consulta la nostra guida su web scraping responsabile.

Key Takeaways

1. Il middleware è il punto di estensione corretto. Non hackerare il proxy URL nei tuoi spider — implementa un middleware che centralizza la logica di rotazione.

2. Rotazione per-request, non per-spider. Usa request.meta['proxy'] per assegnare un IP diverso per ogni richiesta, non un proxy globale.

3. I retry devono ruotare il proxy. Ritentare con lo stesso IP bannato è inutile. Invalida la sessione prima di ogni retry.

4. I proxy residenziali richiedono sessioni sticky. Per login, carrelli e paginazioni, mantieni la stessa sessione IP per un timeout configurabile.

5. Monitora i tassi di successo per paese. Se i proxy di un paese hanno un success rate basso, investiga — potrebbe essere un problema di targeting o un blocco regionale.

6. scrapy-playwright > scrapy-splash per proxy per-request. Playwright supporta proxy per-request nativamente, Splash no.

Prossimi Passi

Ora hai tutte le componenti per un sistema Scrapy con proxy residenziali di livello production. Il prossimo passo è configurare il tuo account ProxyHat e testare il middleware contro il tuo sito target.

Se hai domande sull'integrazione, il team ProxyHat è disponibile per supporto tecnico direttamente dalla dashboard.

Pronto per iniziare?

Accedi a oltre 50M di IP residenziali in oltre 148 paesi con filtraggio AI.

Vedi i prezziProxy residenziali
← Torna al Blog