Middleware de Proxy en Scrapy: Rotación de Proxies Sin Bloqueos
Scrapy ha incluido soporte para proxies desde la versión 0.8, pero la documentación nunca te dice cómo rotar, reintentar y sobrevivir a los bloqueos a gran escala. Aquí está la anatomía del middleware, el código y las configuraciones que deciden si tu rastreo termina.
Scrapy ha incluido un middleware de proxy desde la versión 0.8, así que '¿Scrapy soporta proxies?' se resolvió hace quince años. Lo que la documentación nunca termina de armar es el panorama de producción: cómo el middleware de proxy integrado de Scrapy realmente procesa las credenciales, cuándo establecer proxies por solicitud versus globalmente, y cómo rotar y recuperarse cuando un objetivo comienza a bloquear salidas a mitad del rastreo. Esta guía recorre toda la cadena: el HttpProxyMiddleware integrado, meta por solicitud, un middleware de rotación personalizado con manejo de bloqueos y las configuraciones que deciden si un rastreo de 100k páginas termina o muere a las 3am.
Cómo funciona el middleware de proxy integrado de Scrapy
scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware está habilitado por defecto con prioridad 750 en DOWNLOADER_MIDDLEWARES_BASE. Leer su código fuente (aproximadamente 100 líneas en Scrapy 2.17) te dice todo lo que necesitas para depurar:
- Al iniciar, llama a
urllib.request.getproxies(), por lo que las variables de entornohttp_proxy/https_proxyconfiguran un spider sin código.no_proxyse respeta solo para esquemas http y https. - Por solicitud,
request.meta['proxy']siempre gana sobre el entorno. Establécelo enNonepara forzar una conexión directa para esa solicitud. - Las credenciales incrustadas en la URL del proxy (
http://user:pass@host:port) se eliminan, codifican en Base64 y se envían como un encabezadoProxy-Authorization: Basic. La codificación predeterminada es latin-1 y se puede configurar a través deHTTPPROXY_AUTH_ENCODING. - El middleware descodifica las credenciales antes de codificarlas, por lo que debes codificar en URL caracteres especiales como
@en contraseñas para que pasen correctamente. - Si un reintento cambia el proxy, el middleware elimina el encabezado de autenticación obsoleto en lugar de filtrarlo al nuevo proxy, una corrección de higiene de credenciales en Scrapy moderno, y una buena razón para no fijar versiones antiguas.
Consecuencia práctica: casi nunca necesitas un paquete de proxy de terceros. Cualquier cosa que coloque una URL de proxy válida en request.meta['proxy'] antes de la prioridad 750 obtiene autenticación y manejo de encabezados gratis.
Proxies por solicitud con meta
La integración más ligera es establecer meta directamente en el spider, útil cuando solo algunas solicitudes necesitan un proxy, o diferentes objetivos necesitan diferentes países de salida:
import scrapy
class PricesSpider(scrapy.Spider):
name = "prices"
def start_requests(self):
yield scrapy.Request(
"https://example.com/product/1",
meta={"proxy": "http://USER:PASS@gate.quantumproxies.io:PORT"},
)
El meta por solicitud brilla cuando la elección del proxy en sí está impulsada por datos: enrutar páginas de productos alemanas a través de un nombre de usuario dirigido a Alemania, enviar descargas de imágenes a través de salidas de centros de datos económicos mientras el HTML va residencial, o escalar una URL obstinada a una sesión más pegajosa en su segundo intento. Dado que el meta sobrevive a reintentos y redirecciones, la decisión que tomas al construir la solicitud la sigue durante todo el ciclo de descarga. Sin embargo, establecer meta en miles de solicitudes a mano no escala, para eso son los middlewares de descarga.
La configuración de producción más simple: un gateway rotativo
Con proxies rotativos detrás de un único endpoint de gateway, la rotación ocurre del lado del servidor: cada solicitud a través de la misma URL sale de una IP diferente en un pool residencial de más de 90M que abarca más de 200 países. Tu middleware se reduce a tres líneas, y no hay lista que verificar, podar o actualizar:
# middlewares.py
class RotatingGatewayMiddleware:
PROXY = "http://USER:PASS@gate.quantumproxies.io:PORT"
def process_request(self, request, spider):
request.meta["proxy"] = self.PROXY
# settings.py
DOWNLOADER_MIDDLEWARES = {
"myproject.middlewares.RotatingGatewayMiddleware": 350,
}
La prioridad 350 importa: tu middleware debe ejecutarse antes del integrado en 750 para que HttpProxyMiddleware aún pueda convertir las credenciales en el encabezado de autenticación. Este emparejamiento —rotación de gateway más la maquinaria de reintento estándar de Scrapy— es la mayor fiabilidad por línea de código, porque cada reintento viaja automáticamente a través de una nueva IP de salida.

Un middleware de proxy rotativo personalizado con manejo de bloqueos
Si manejas tu propia lista de proxies —direcciones ISP estáticas o un pool mixto— el patrón clásico (popularizado por el paquete scrapy-proxies, que recomendaba RETRY_TIMES = 10 porque los proxies de lista gratuita fallan tan a menudo) es: elegir aleatoriamente por solicitud, desalojar en señales de bloqueo, reencolar la solicitud:
import random
class ProxyListMiddleware:
BAN_CODES = {403, 429}
def __init__(self, proxies):
self.proxies = list(proxies)
@classmethod
def from_crawler(cls, crawler):
return cls(crawler.settings.getlist("PROXY_LIST"))
def process_request(self, request, spider):
if self.proxies and "proxy" not in request.meta:
request.meta["proxy"] = random.choice(self.proxies)
def process_response(self, request, response, spider):
if response.status in self.BAN_CODES:
bad = request.meta.get("proxy")
if bad in self.proxies and len(self.proxies) > 1:
self.proxies.remove(bad)
spider.logger.warning("Evicted %s (%d left)", bad, len(self.proxies))
return request.replace(dont_filter=True) # re-queue on a new proxy
return response
Observa todo lo que este middleware debe hacer que un gateway hace gratis: rastrear la salud del pool, desalojar IPs quemadas, reencolar solicitudes. También se reduce bajo presión —un pool de 20 IPs estáticas puede evaporarse en minutos en un objetivo agresivo. El libro de jugadas más amplio contra bloqueos (ritmo, encabezados, disciplina de sesión) está en nuestra lista de verificación para evitar bloqueos de IP.
Configuraciones de Scrapy que deciden el resultado
El middleware coloca el proxy; las configuraciones deciden si el rastreo se comporta. Estas son las más importantes:
# settings.py — a sane baseline for proxied crawls
RETRY_TIMES = 5
RETRY_HTTP_CODES = [429, 403, 500, 502, 503, 504, 408]
CONCURRENT_REQUESTS = 32
CONCURRENT_REQUESTS_PER_DOMAIN = 8
DOWNLOAD_TIMEOUT = 30
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_TARGET_CONCURRENCY = 4.0
- RETRY_HTTP_CODES — agrega 429 y 403 para que los bloqueos suaves desencadenen un reintento (y, con rotación, una nueva IP) en lugar de un elemento fallido.
- RETRY_TIMES — 3-5 es adecuado para un pool de calidad; necesitar 10 es una señal de que el pool en sí no es saludable.
- CONCURRENT_REQUESTS_PER_DOMAIN — el verdadero control de cortesía. La rotación distribuye la carga a través de IPs, pero el objetivo aún ve el volumen total.
- DOWNLOAD_TIMEOUT — los 180 segundos por defecto permiten que una salida lenta ocupe un espacio de concurrencia durante tres minutos. 30 es suficiente.
- AUTOTHROTTLE — adapta el ritmo a la latencia observada; se combina bien con la rotación en objetivos con límite de velocidad. ¿Aún ahogado en 429s? Nuestra guía sobre límites de velocidad cubre los presupuestos de concurrencia en profundidad.

Detección de bloqueos más allá de los códigos de estado
Los objetivos sofisticados no envían 403s honestos. Sirven 200s que contienen una página CAPTCHA, una cuadrícula de productos vacía o una plantilla despojada. Los spiders robustos verifican el contenido, no solo el estado —busca un elemento que solo existe en la página real y reencola cuando falta. Si las páginas regresan estructuralmente vacías incluso a través de buenos proxies, probablemente el contenido se renderiza del lado del cliente; consulta por qué los scrapers obtienen páginas vacías. Y cuando un dominio derrota el HTTP simple sin importar la IP, entrega ese dominio a una Scraper API que renderiza JavaScript y devuelve HTML limpio o markdown —Scrapy lo consume como cualquier otra respuesta, y mantienes tu pipeline.
def parse(self, response):
if not response.css("div.product-grid"):
# 200 OK but the real content is missing: soft ban or JS wall
yield response.request.replace(dont_filter=True)
return
for product in response.css("div.product-grid article"):
yield {"name": product.css("h2::text").get()}
Preguntas frecuentes
¿Scrapy soporta proxies de forma nativa?
Sí. HttpProxyMiddleware se envía habilitado con prioridad 750: lee las variables de entorno http_proxy / https_proxy y respeta request.meta['proxy'] por solicitud, incluidas las credenciales user:pass@, que convierte en un encabezado Proxy-Authorization. Solo escribes código para decidir qué proxy obtiene cada solicitud.
¿Cómo configuro un proxy para una sola solicitud en Scrapy?
Pásalo en el meta de la solicitud: scrapy.Request(url, meta={'proxy': 'http://user:pass@host:port'}). Meta siempre anula los proxies a nivel de entorno, y establecer el valor en None fuerza que esa solicitud vaya directa —útil para mezclar tráfico con y sin proxy en un solo spider.
¿Cómo roto proxies en Scrapy?
O bien escribe un middleware de descarga que elija un proxy por solicitud de una lista y desaloje los bloqueados, o apunta cada solicitud a un endpoint de gateway rotativo que asigne una nueva IP de salida del lado del servidor. El enfoque de gateway necesita un middleware de tres líneas, sin verificaciones de salud, y convierte cada reintento de Scrapy en una rotación gratis.
¿Por qué mi proxy de Scrapy devuelve 407?
El proxy rechazó la autenticación. Verifica que las credenciales estén dentro de la URL del proxy en meta, que los caracteres especiales estén codificados en URL, y que tu IP de servidor esté en la lista blanca si tu plan usa autenticación por IP. Si las credenciales contienen caracteres no ASCII, establece HTTPPROXY_AUTH_ENCODING para que coincida con tu proveedor.
El patrón a recordar: establece meta['proxy'] temprano, deja que el middleware integrado en 750 haga la autenticación, agrega 429 y 403 a tus códigos de reintento, y prefiere la rotación del lado del servidor sobre el cuidado de listas. Si partes de tu rastreo necesitan JavaScript, sopesa el costo de los navegadores sin cabeza versus las solicitudes HTTP antes de optar por uno —la mayoría de los proyectos de Scrapy necesitan mejores IPs, no un navegador.