Guia Completo de Scrapy Proxy Middleware: Rotação de Proxies Residenciais em Produção

Aprenda a integrar proxies residenciais no Scrapy via middleware customizado, lidar com falhas por requisição, escalar com Splash/Playwright e monitorar taxas de banimento — com código funcional pronto para produção.

Guia Completo de Scrapy Proxy Middleware: Rotação de Proxies Residenciais em Produção

Por que integrar proxies residenciais no Scrapy demanda um middleware próprio

Se você já rodou um spider Scrapy contra um site protegido por Cloudflare ou PerimeterX, conhece o padrão: as primeiras 50 requisições funcionam, depois tudo vira 403. O problema não é o Scrapy — é que seu IP datacenter foi classificado como bot. Proxies residenciais resolvem isso distribuindo suas requisições por IPs reais de ISPs, mas integrá-los de forma robusta exige entender o pipeline de middlewares do framework.

O Scrapy não rotaciona IPs por padrão. Ele suporta um único proxy via HTTPPROXY_AUTH_ENCODING, o que é inútil para scraping em escala. A solução é construir um Scrapy proxy middleware que atribui um proxy diferente a cada request (ou a cada sessão), trata falhas individuais e se integra ao sistema de retries do framework.

O modelo de downloader middleware do Scrapy e onde proxies entram

O Scrapy processa cada request através de uma pilha de downloader middlewares, ordenados por prioridade numérica (menor = executado primeiro). O fluxo é:

  1. Request phase: middlewares processam o request antes de ele chegar ao downloader.
  2. O downloader executa o request HTTP.
  3. Response phase: middlewares processam a resposta antes de ela voltar ao spider.
  4. Exception phase: se o download falha, middlewares podem capturar a exceção.

É na request phase que injetamos o proxy. O middleware recebe o objeto Request e pode modificar request.meta['proxy'] antes do download. Já na response/exception phase, detectamos bans e falhas para decidir se retry com outro IP faz sentido.

A prioridade padrão do HttpProxyMiddleware nativo é 560. Nosso middleware customizado deve ter prioridade mais alta (número menor) para que execute antes:

DOWNLOADER_MIDDLEWARES = {
    'myproject.middlewares.ProxyHatRotatingMiddleware': 100,
    'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware': 560,
}
Importante: não desative o HttpProxyMiddleware nativo — ele ainda processa o header Proxy-Authorization. Nosso middleware apenas define request.meta['proxy']; o middleware nativo cuida da autenticação.

Implementando um middleware de rotação de proxies residenciais

Vamos construir um middleware completo que: (1) alterna IPs residenciais a cada request por padrão; (2) suporta sessões sticky quando o spider precisa; (3) permite geo-targeting via flags no request.

Código completo do ProxyHatRotatingMiddleware

import random
import hashlib
from scrapy import signals
from scrapy.downloadermiddlewares.httpproxy import HttpProxyMiddleware


class ProxyHatRotatingMiddleware(HttpProxyMiddleware):
    """
    Middleware de rotação de proxies residenciais ProxyHat.

    Suporta:
      - Rotação per-request (padrão)
      - Sessões sticky via request.meta['proxy_session']
      - Geo-targeting via request.meta['proxy_country'] e 'proxy_city'
    """

    def __init__(self, proxy_user, proxy_pass, proxy_host, proxy_port,
                 proxy_mode='rotate', auth_encoding='latin-1'):
        super().__init__(auth_encoding=auth_encoding)
        self.proxy_user = proxy_user
        self.proxy_pass = proxy_pass
        self.proxy_host = proxy_host
        self.proxy_port = proxy_port
        self.proxy_mode = proxy_mode
        self._sticky_sessions = {}

    @classmethod
    def from_crawler(cls, crawler):
        s = crawler.settings
        return cls(
            proxy_user=s.get('PROXYHAT_USERNAME'),
            proxy_pass=s.get('PROXYHAT_PASSWORD'),
            proxy_host=s.get('PROXYHAT_HOST', 'gate.proxyhat.com'),
            proxy_port=s.get('PROXYHAT_PORT', 8080),
            proxy_mode=s.get('PROXYHAT_MODE', 'rotate'),
        )

    def _build_proxy_url(self, request):
        """Constrói a URL do proxy com base nas flags do request."""
        user = self.proxy_user
        country = request.meta.get('proxy_country')
        city = request.meta.get('proxy_city')
        session = request.meta.get('proxy_session')

        # Adiciona flags de geo-targeting no username
        if country:
            user = f'{user}-country-{country}'
        if city:
            user = f'{user}-city-{city.lower()}'

        # Sessão sticky: hash determinístico para manter o mesmo IP
        if session:
            session_hash = hashlib.md5(session.encode()).hexdigest()[:8]
            user = f'{user}-session-{session_hash}'

        return f'http://{user}:{self.proxy_pass}@{self.proxy_host}:{self.proxy_port}'

    def process_request(self, request, spider):
        # Ignora requests que já têm proxy definido
        if 'proxy' in request.meta and request.meta.get('proxy'):
            return

        # Verifica se o spider desabilitou proxy para este request
        if request.meta.get('dont_proxy'):
            request.meta['proxy'] = None
            return

        proxy_url = self._build_proxy_url(request)
        request.meta['proxy'] = proxy_url

        # Loga para debugging
        country = request.meta.get('proxy_country', 'any')
        session = request.meta.get('proxy_session', 'none')
        spider.logger.debug(
            f'ProxyHat: {request.url[:60]} | country={country} session={session}'
        )

Configuração no settings.py:

# settings.py
PROXYHAT_USERNAME = 'myuser'
PROXYHAT_PASSWORD = 'mypass'
PROXYHAT_HOST = 'gate.proxyhat.com'
PROXYHAT_PORT = 8080
PROXYHAT_MODE = 'rotate'

DOWNLOADER_MIDDLEWARES = {
    'myproject.middlewares.ProxyHatRotatingMiddleware': 100,
    'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware': 560,
}

CONCURRENT_REQUESTS = 32
DOWNLOAD_TIMEOUT = 30

E no spider, você controla o comportamento por request:

class MySpider(scrapy.Spider):
    name = 'example'

    def start_requests(self):
        # Rotação per-request (padrão)
        yield scrapy.Request('https://example.com/data')

        # Sessão sticky — mesmo IP por 10 minutos
        yield scrapy.Request(
            'https://example.com/login',
            meta={'proxy_session': 'user-42', 'proxy_country': 'US'}
        )

        # Geo-targeting para DE com IP de Berlin
        yield scrapy.Request(
            'https://example.com/prices',
            meta={'proxy_country': 'DE', 'proxy_city': 'berlin'}
        )

        # Sem proxy
        yield scrapy.Request(
            'https://api.example.com/public',
            meta={'dont_proxy': True}
        )

scrapy-rotating-proxies vs. middleware próprio: quando usar cada um

A biblioteca scrapy-rotating-proxies foi a solução go-to por anos, mas tem limitações sérias para proxies residenciais modernos. Veja a comparação:

Característica scrapy-rotating-proxies Middleware próprio (ProxyHat)
Tipo de proxy suportado Lista estática de IPs Residencial, mobile, datacenter via gateway
Rotação Round-robin ou random Per-request ou sticky session
Geo-targeting Não suportado País e cidade por request
Detecção de ban Verifica página de ban configurada Integração com RetryMiddleware
Manutenção Parcialmente abandonado Você controla
Complexidade inicial Baixa Média

Use scrapy-rotating-proxies se você tem uma lista fixa de proxies datacenter e não precisa de geo-targeting. Para proxies residenciais com gateway rotativo, um middleware próprio é mais simples e mais poderoso — como vimos acima, são ~80 linhas de código.

Retry com rotação de proxy: tratando falhas por request

Quando um proxy falha (timeout, 403, 503), o RetryMiddleware nativo do Scrapy tenta novamente — mas com o mesmo IP. Precisamos de um middleware de retry que troque o proxy a cada tentativa.

from scrapy.downloadermiddlewares.retry import RetryMiddleware
from scrapy.utils.response import response_status_message

class ProxyHatRetryMiddleware(RetryMiddleware):
    """
    Retry middleware que limpa o proxy do request antes de retry,
    forçando o ProxyHatRotatingMiddleware a atribuir um novo IP.
    """

    def __init__(self, settings):
        super().__init__(settings)
        self.ban_status_codes = set(
            settings.getlist('PROXYHAT_BAN_STATUS_CODES', [403, 429, 503])
        )

    def process_response(self, request, response, spider):
        # Se o status indica ban, força retry com novo proxy
        if response.status in self.ban_status_codes:
            reason = response_status_message(response.status)
            # Remove o proxy atual para que o próximo request receba um novo
            request.meta.pop('proxy', None)
            request.meta.pop('proxy_session', None)
            spider.logger.info(
                f'Ban detectado ({response.status}) em {request.url[:60]}, '
                f'retry com novo proxy'
            )
            return self._retry(request, reason, spider) or response

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

    def process_exception(self, request, exception, spider):
        # Timeout ou erro de conexão — tenta com novo proxy
        if isinstance(exception, (self._get_twisted_exceptions())):
            request.meta.pop('proxy', None)
            spider.logger.debug(
                f'Exceção {type(exception).__name__} em {request.url[:60]}, '
                f'retry com novo proxy'
            )
        return super().process_exception(request, exception, spider)

Configuração:

RETRY_ENABLED = True
RETRY_TIMES = 3
RETRY_HTTP_CODES = [500, 502, 503, 504, 408, 429, 403]
PROXYHAT_BAN_STATUS_CODES = [403, 429, 503]

DOWNLOADER_MIDDLEWARES = {
    'myproject.middlewares.ProxyHatRotatingMiddleware': 100,
    'myproject.middlewares.ProxyHatRetryMiddleware': 550,
    'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware': 560,
    'scrapy.downloadermiddlewares.retry.RetryMiddleware': None,
}

A ordem importa: nosso retry middleware (550) roda antes do HttpProxyMiddleware (560). Quando ele limpa request.meta['proxy'] e devolve o request ao scheduler, o ProxyHatRotatingMiddleware (100) atribui um novo IP na próxima tentativa.

Sites com JavaScript: scrapy-splash e scrapy-playwright com proxies

Sites renderizados por JavaScript (SPAs, conteúdo dinâmico) exigem um browser headless. Dois approaches principais no ecossistema Scrapy:

scrapy-splash

O Splash é um servidor de renderização leve baseado em Qt. A integração com proxies funciona passando o proxy via splash_args:

import scrapy
from scrapy_splash import SplashRequest

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

    def start_requests(self):
        yield SplashRequest(
            'https://spa-example.com/products',
            self.parse,
            args={
                'wait': 3,
                'proxy': 'http://user-country-US:pass@gate.proxyhat.com:8080',
            },
            meta={'proxy_country': 'US'}
        )

scrapy-playwright

O scrapy-playwright usa Chromium real e é mais adequado para sites com anti-bot avançado. Proxies são configurados no settings:

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

TWISTED_REACTOR = 'twisted.internet.asyncioreactor.AsyncioSelectorReactor'

PLAYWRIGHT_LAUNCH_OPTIONS = {
    'proxy': {
        'server': 'http://gate.proxyhat.com:8080',
        'username': 'user-country-US',
        'password': 'pass',
    },
    'headless': True,
}

Para rotação dinâmica com Playwright, você precisa criar um novo context por request com credenciais diferentes:

class PlaywrightRotatingSpider(scrapy.Spider):
    name = 'pw_rotating'

    def start_requests(self):
        countries = ['US', 'DE', 'GB', 'FR']
        for country in countries:
            yield scrapy.Request(
                f'https://example.com/prices?cc={country}',
                meta={
                    'playwright': True,
                    'playwright_include_page': True,
                    'proxy_country': country,
                },
                dont_filter=True,
            )

    async def parse(self, response):
        page = response.meta.get('playwright_page')
        if page:
            await page.close()
        # ... processa response
Dica: Com Playwright, cada browser context pode ter seu próprio proxy. Isso permite sessões sticky naturais — um context por sessão de usuário. O custo é maior uso de memória (~150MB por context), então planeje a concorrência.

Deploy em produção: Scrapyd, Docker e além

Opção 1 — Scrapyd + scrapyd-web

O Scrapyd é um daemon que gerencia spiders como serviços. Com scrapyd-web, você ganha uma UI para agendamento e monitoramento. Funciona bem para projetos pequenos a médios.

# Deploy para Scrapyd
scrapyd-deploy

# Agendar spider via API
curl http://localhost:6800/schedule.json -d \
  'project=myproject&spider=example&PROXYHAT_MODE=rotate'

Opção 2 — Docker + cron (simples e robusto)

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
ENTRYPOINT ["scrapy", "crawl"]

E no cron do host:

# /etc/cron.d/scrapy-prices
0 6 * * * root docker run --rm myproject:latest prices \
  -s PROXYHAT_USERNAME=$USER \
  -s PROXYHAT_PASSWORD=$PASS

Opção 3 — ScrapeOps ou Zyte Scrapy Cloud

Plataformas gerenciadas que incluem auto-scaling, proxy rotation e monitoramento. O custo é maior, mas eliminam overhead operacional. Ideal para equipes sem DevOps dedicado.

d>Gratuito (self-hosted)
Abordagem Custo Complexidade Ideal para
Scrapyd Gratuito (self-hosted) Média 1–5 spiders, VPS única
Docker + cron Baixa Spiders agendados, infra simples
ScrapeOps / Zyte $49+/mês Baixa Equipes sem DevOps

Monitoramento: taxas de sucesso, ban detection e observabilidade

Em produção, você precisa saber quais IPs estão sendo banidos e qual a taxa de sucesso por país. Adicione signal handlers e stats ao seu middleware:

from scrapy import signals

class ProxyHatStatsMiddleware:
    """Coleta estatísticas de uso de proxy por país e status code."""

    def __init__(self, stats):
        self.stats = stats

    @classmethod
    def from_crawler(cls, crawler):
        o = cls(crawler.stats)
        crawler.signals.connect(o.spider_closed, signal=signals.spider_closed)
        return o

    def process_response(self, request, response, spider):
        country = request.meta.get('proxy_country', 'unknown')
        status = response.status

        # Incrementa contadores
        self.stats.inc_value(f'proxy/response_count/{country}')
        self.stats.inc_value(f'proxy/status/{country}/{status}')

        if status in (403, 429, 503):
            self.stats.inc_value(f'proxy/banned/{country}')
            spider.logger.warning(
                f'BAN: country={country} status={status} url={request.url[:80]}'
            )

        if status >= 400:
            self.stats.inc_value(f'proxy/error/{country}')

        return response

    def spider_closed(self, spider, reason):
        stats = self.stats.get_stats()
        spider.logger.info('=== Proxy Stats ===')
        for key, value in sorted(stats.items()):
            if key.startswith('proxy/'):
                spider.logger.info(f'  {key}: {value}')

Para monitoramento contínuo, exporte as stats do Scrapy para Prometheus ou Datadog via scrapy-prometheus ou um pipeline customizado. Métricas essenciais:

  • Taxa de sucesso por país: proxy/status/{country}/200 / proxy/response_count/{country}
  • Taxa de ban por país: proxy/banned/{country} / proxy/response_count/{country}
  • Latência mediana por país: coletada via response.meta.get('download_latency')
  • Retry rate: downloader/response_status_count/403 vs total
Padrão de alerta: se a taxa de bans de um país excede 15%, ajuste o geo-targeting ou reduza a concorrência. Bans acima de 30% indicam que o target bloqueou o range de IPs — troque para mobile proxies ou reduza drasticamente o ritmo.

Padrões de escala: concorrência, rate limiting e fleet management

Proxies residenciais são um recurso finito. O Scrapy dispara requests em paralelo via CONCURRENT_REQUESTS, e sem controle você pode esgotar o pool rapidamente.

Estratégia 1 — Rate limiting por domínio

CONCURRENT_REQUESTS_PER_DOMAIN = 4
DOWNLOAD_DELAY = 1.0
RANDOMIZE_DOWNLOAD_DELAY = True

Isso garante no máximo 4 requests simultâneos por domínio com delay de 1s ± 0.5s. Para 10 domínios, são ~40 requests concorrentes — perfeito para um pool residencial.

Estratégia 2 — AutoThrottle

O AutoThrottle ajusta automaticamente a concorrência com base na latência:

AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_TARGET_CONCURRENCY = 4.0
AUTOTHROTTLE_MAX_CONCURRENCY = 16

Estratégia 3 — Fleet de containers

Para escala massiva (>100k requests/hora), rode múltiplos containers Scrapy com partições de URLs e IPs de países diferentes. Um orchestrador simples:

# docker-compose.yml
services:
  scraper-us:
    image: myproject:latest
    command: crawl prices -s PROXYHAT_USERNAME=user-country-US
    deploy: { replicas: 3 }
  scraper-de:
    image: myproject:latest
    command: crawl prices -s PROXYHAT_USERNAME=user-country-DE
    deploy: { replicas: 2 }

Key Takeaways

  • Middleware é o lugar certo para proxy rotation no Scrapy — não hacks no spider. Prioridade 100 garante que roda antes do HttpProxyMiddleware nativo.
  • Use o gateway do ProxyHat para rotação automática. Não gerencie listas de IPs — passe flags no username (-country-US, -session-abc).
  • Retry com novo proxy é essencial. Substitua o RetryMiddleware nativo por um customizado que limpa request.meta['proxy'] antes de cada retry.
  • Para JS-heavy sites, scrapy-playwright com um proxy por browser context é mais robusto que scrapy-splash.
  • Monitore por país: taxas de ban acima de 15% exigem ajuste de geo-targeting ou tipo de proxy.
  • scrapy-rotating-proxies é útil para listas estáticas, mas não suporta geo-targeting ou sticky sessions via gateway — middleware próprio é mais adequado para proxies residenciais.

Para começar com proxies residenciais de alta qualidade, confira os locations disponíveis e os planos do ProxyHat. E para aprofundar em scraping com Scrapy, veja nosso guia de web scraping em produção.

Pronto para começar?

Acesse mais de 50M de IPs residenciais em mais de 148 países com filtragem por IA.

Ver preçosProxies residenciais
← Voltar ao Blog