O parser no Scrapy falha por timeout, o pool de proxies é consumido muito mais rápido do que o esperado, e os logs estão cheios de respostas 407 e 403 — uma cena familiar? Em 90% dos casos, o problema não está nos próprios proxies, mas na forma como o DownloaderMiddleware foi escrito. Vamos analisar cinco dos erros mais comuns no middleware que transformam tráfego caro em requisições inúteis e mostrar como corrigi-los com código.
Erro 1: rotação primitiva sem considerar o estado do proxy
A construção mais comum que pode ser encontrada em tutoriais é random.choice(PROXY_LIST) dentro de process_request. O problema é que essa rotação não sabe qual proxy acabou de ser banido e qual ainda está ativo. Como resultado, o parser continua enviando requisições através de um IP já bloqueado, recebe 403/429, faz retry — e novamente escolhe o mesmo endereço, pois a seleção é aleatória e não exclui os nós "ruins".
A abordagem correta é manter o estado de cada proxy: número de requisições bem-sucedidas, número de erros, tempo do último uso. Aqui está uma versão mínima funcional:
import random
import time
class ProxyPool:
def __init__(self, proxies):
self.proxies = {p: {"fails": 0, "last_used": 0, "banned_until": 0} for p in proxies}
def get_proxy(self):
now = time.time()
available = [
p for p, state in self.proxies.items()
if state["banned_until"] < now
]
if not available:
# se todos estão banidos, reiniciamos o ban mais "antigo"
available = list(self.proxies.keys())
return random.choice(available)
def mark_fail(self, proxy, cooldown=300):
self.proxies[proxy]["fails"] += 1
self.proxies[proxy]["banned_until"] = time.time() + cooldown
def mark_success(self, proxy):
self.proxies[proxy]["fails"] = 0
self.proxies[proxy]["last_used"] = time.time()
Esse pool exclui IPs banidos durante o tempo de "cooldown" e os retorna ao uso posteriormente. Isso já reduz o consumo de tráfego drasticamente, pois você não está atacando o mesmo nó bloqueado repetidamente.
Erro 2: tratamento incorreto de retry e códigos de status
O segundo erro típico é usar o RetryMiddleware padrão do Scrapy sem modificação. Por padrão, ele tenta novamente a requisição no mesmo proxy que falhou, a menos que você tenha inserido explicitamente a troca de proxy em process_exception. Como resultado, obtemos a clássica situação: 3 retries, 3 bans, a requisição ainda falha, e o tráfego já foi gasto.
O segundo ponto é que nem todos os códigos de status precisam ser retriados da mesma forma. 429 (Too Many Requests) requer uma pausa e troca de IP, 403 geralmente significa ban de um proxy específico (necessita de troca imediata), enquanto 5xx — frequentemente é um problema temporário do lado do servidor, podendo ser retriado no mesmo proxy. Se você tratar tudo da mesma forma, o middleware ou queima os proxies de forma muito agressiva ou espera muito tempo onde era necessário trocar o IP imediatamente.
class SmartRetryMiddleware:
def __init__(self, pool):
self.pool = pool
def process_response(self, request, response, spider):
proxy = request.meta.get("proxy")
if response.status in (403, 407):
if proxy:
self.pool.mark_fail(proxy, cooldown=600)
new_request = request.copy()
new_request.meta["proxy"] = self.pool.get_proxy()
new_request.dont_filter = True
return new_request
if response.status == 429:
if proxy:
self.pool.mark_fail(proxy, cooldown=120)
new_request = request.copy()
new_request.meta["proxy"] = self.pool.get_proxy()
new_request.dont_filter = True
return new_request
if proxy:
self.pool.mark_success(proxy)
return response
Aqui é importante diferenciar o cooldown por tipo de erro: ban rígido (403/407) — pausa longa, limite de taxa (429) — pausa curta. Isso economiza dezenas de porcentagens de tráfego em longas execuções.
Erro 3: ausência de sessões sticky para sites com autenticação
Se o parser está trabalhando com um site que possui login, carrinho, paginação com manutenção de estado ou captcha com verificação por IP — a troca de proxies a cada requisição quebra a sessão. O site vê que a requisição nº 1 veio de um IP, enquanto a requisição nº 2 (dentro da mesma sessão de cookies) veio de outro, e isso aciona a proteção contra bots instantaneamente, mesmo que ambos os IPs sejam "limpos".
A solução é vincular um proxy a uma sessão lógica (por exemplo, a uma conta específica ou a uma cadeia de requisições dentro de um mesmo domínio) por um tempo fixo, em vez de trocá-lo a cada requisição. Isso é chamado de sessão sticky.
class StickySessionMiddleware:
def __init__(self, pool, ttl=600):
self.pool = pool
self.ttl = ttl
self.sessions = {} # session_id -> (proxy, expires_at)
def process_request(self, request, spider):
session_id = request.meta.get("session_id")
if not session_id:
return
now = time.time()
session = self.sessions.get(session_id)
if session and session[1] > now:
request.meta["proxy"] = session[0]
else:
proxy = self.pool.get_proxy()
self.sessions[session_id] = (proxy, now + self.ttl)
request.meta["proxy"] = proxy
Para tarefas que exigem estabilidade de IP durante toda a sessão — autenticação, trabalho com painel de controle, formulários de múltiplas etapas — os proxies residenciais com suporte a sessões são os mais adequados: eles permitem manter o mesmo IP de saída por vários minutos ou horas, e depois trocá-lo de forma controlada, em vez de aleatoriamente a cada requisição.
Erro 4: transmissão incorreta da autenticação do proxy
O quarto erro é técnico, mas aparece em quase todos os segundos projetos. Os desenvolvedores transmitem o login e a senha do proxy diretamente na URL do tipo http://user:pass@ip:port através de request.meta["proxy"]. Isso funciona na maioria dos casos, mas ao usar proxies através de um túnel HTTPS ou ao trabalhar com alguns provedores, esse método de autenticação é processado incorretamente pelo padrão HttpProxyMiddleware, e as requisições falham com 407 Proxy Authentication Required, mesmo que as credenciais estejam corretas.
Um método mais confiável é transmitir o cabeçalho Proxy-Authorization explicitamente, codificado em base64:
import base64
class ProxyAuthMiddleware:
def process_request(self, request, spider):
proxy = request.meta.get("proxy")
if not proxy:
return
# proxy sem credenciais na URL
request.meta["proxy"] = proxy
user = request.meta.get("proxy_user")
password = request.meta.get("proxy_pass")
if user and password:
credentials = f"{user}:{password}"
encoded = base64.b64encode(credentials.encode()).decode()
request.headers["Proxy-Authorization"] = f"Basic {encoded}"
Essa abordagem funciona de forma mais estável ao escalar para centenas de requisições paralelas e não depende de como uma versão específica da biblioteca analisa URLs com credenciais embutidas. Isso é especialmente crítico ao trabalhar com proxies móveis, onde a autenticação muitas vezes está ligada a uma lista de IPs permitidos ou a uma verificação rigorosa de cabeçalhos.
Erro 5: falta de monitoramento e registro de bans
O último e, possivelmente, o erro mais caro em termos de consequências é a falta de registro de quais proxies estão sendo banidos, com que frequência e em quais domínios. Sem esses dados, é impossível entender o que exatamente está consumindo o tráfego: se o pool de proxies se esgotou em um site específico ou se o problema está no próprio parser (requisições muito frequentes, falta de delays, cabeçalhos suspeitos).
O conjunto mínimo de métricas que vale a pena registrar no middleware:
- Número de requisições para cada proxy durante a sessão de parsing
- Número e códigos de erros (403, 407, 429, 5xx) para cada proxy
- Tempo de vida do proxy até o primeiro ban
- Domínios onde os bans ocorrem com mais frequência
import logging
import json
logger = logging.getLogger("proxy_stats")
class ProxyStatsMiddleware:
def __init__(self):
self.stats = {}
def process_response(self, request, response, spider):
proxy = request.meta.get("proxy", "unknown")
domain = request.url.split("/")[2]
key = f"{proxy}|{domain}"
entry = self.stats.setdefault(key, {"requests": 0, "errors": 0})
entry["requests"] += 1
if response.status in (403, 407, 429):
entry["errors"] += 1
if entry["requests"] % 50 == 0:
logger.info(json.dumps(self.stats))
return response
Sem essas estatísticas, quaisquer tentativas de "otimizar" o middleware se resumem a adivinhações. Com isso, você vê claramente: se, por exemplo, um domínio específico banir 80% do pool de proxies nas primeiras 10 requisições — o problema não está nos proxies, mas no padrão das requisições (sem rotação de User-Agent, frequência muito alta, ausência de delays entre requisições).
Exemplo funcional de middleware completo
Vamos juntar tudo no settings.py — a ordem do middleware é crítica, pois depende da ordem em que as verificações são aplicadas:
DOWNLOADER_MIDDLEWARES = {
"myproject.middlewares.ProxyAuthMiddleware": 350,
"myproject.middlewares.StickySessionMiddleware": 400,
"myproject.middlewares.SmartRetryMiddleware": 550,
"myproject.middlewares.ProxyStatsMiddleware": 900,
}
RETRY_ENABLED = False # desativamos o retry padrão, usamos o nosso
DOWNLOAD_TIMEOUT = 15
CONCURRENT_REQUESTS_PER_DOMAIN = 8
Preste atenção em RETRY_ENABLED = False — isso é fundamental, caso contrário, o mecanismo de retry embutido do Scrapy entrará em conflito com sua lógica de troca de proxies, e as requisições começarão a ser duplicadas ou retriadas duas vezes. Também é importante limitar CONCURRENT_REQUESTS_PER_DOMAIN — uma paralelidade muito alta em um único domínio, mesmo com rotação de proxies, parece suspeita para sistemas anti-bots.
Qual tipo de proxy escolher para Scrapy
O middleware resolve metade do problema, a outra metade é a escolha correta do próprio pool de proxies para a tarefa. Abaixo está uma comparação para cenários típicos de parsing.
| Tipo de proxy | Quando usar | Prós | Contras |
|---|---|---|---|
| Proxies de datacenter | Parsing em massa de páginas abertas sem proteção anti-bots rigorosa | Alta velocidade, baixo custo de tráfego | Fácil de detectar, frequentemente em blacklist |
| Proxies residenciais | Parsing de marketplaces, sites com proteção JS, autenticação | IPs reais, baixo percentual de bans, suporte a sessões sticky | Velocidade inferior em comparação com DC |
| Proxies móveis | Parsing de versões móveis de sites e APIs com proteção anti-bots rigorosa | Máxima confiança por parte dos sites-alvo | Custo de tráfego mais alto |
Regra prática: para parsing de páginas estáticas sem proteção forte, proxies de datacenter baratos em conjunto com um middleware bem elaborado deste artigo são adequados. Se o site utiliza Cloudflare, PerimeterX, DataDome ou sistemas semelhantes — os IPs de datacenter serão banidos quase imediatamente, e aqui já é mais vantajoso passar diretamente para um pool residencial, economizando tempo na depuração do middleware.
Checklist antes de colocar o parser em produção
- O middleware exclui proxies banidos durante o cooldown, em vez de escolhê-los aleatoriamente
- A lógica de retry diferencia tipos de erros (403/407 vs 429 vs 5xx) com diferentes cooldowns
- Para tarefas de sessão, é utilizada a vinculação sticky do proxy, em vez de troca a cada requisição
- A autenticação do proxy é transmitida através do cabeçalho Proxy-Authorization, e não apenas pela URL
- Estatísticas de bans por domínios e proxies são mantidas para diagnóstico de problemas
- O RetryMiddleware padrão do Scrapy está desativado para não entrar em conflito com a lógica personalizada
- A concorrência é limitada a valores razoáveis, e não definida no máximo
Conclusão
A maioria dos problemas com o consumo de tráfego de proxy no Scrapy é resolvida não pela compra de um pool maior de IPs, mas pela correção da lógica do middleware: separação de erros por tipos, sessões sticky para cenários complexos, autenticação correta e monitoramento constante de bans. O código deste artigo pode ser usado como base e adaptado para um projeto específico — a estrutura permanece funcional tanto para pequenos parsers quanto para instalações distribuídas do Scrapy-Cluster.
Se o middleware já está configurado corretamente, mas os bans ainda ocorrem com muita frequência — provavelmente, o problema está na qualidade do próprio pool de IPs. Para parsing de sites com proteção anti-bots avançada, vale a pena experimentar proxies residenciais com suporte a sessões — eles reduzem significativamente a porcentagem de ativação da proteção em comparação com endereços de datacenter, mantendo a mesma lógica de middleware.