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 é:
- Request phase: middlewares processam o request antes de ele chegar ao downloader.
- O downloader executa o request HTTP.
- Response phase: middlewares processam a resposta antes de ela voltar ao spider.
- 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 headerProxy-Authorization. Nosso middleware apenas definerequest.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.
| Abordagem | Custo | Complexidade | Ideal para |
|---|---|---|---|
| Scrapyd | Gratuito (self-hosted) | Média | 1–5 spiders, VPS única |
| Docker + cron | d>Gratuito (self-hosted)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/403vs 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.






