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:

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.

Diagrama de flujo de la cadena de middleware de proxy de Scrapy: solicitud del spider, middleware personalizado establece meta proxy, HttpProxyMiddleware agrega autenticación en prioridad 750, descargador sale a través de gateway rotativo
Tu middleware solo necesita establecer meta['proxy']; el integrado en prioridad 750 maneja las credenciales, y los reintentos reingresan a la cadena en una nueva IP.

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
Comparación de un middleware de lista de proxies DIY en Scrapy versus un gateway residencial rotativo para rotación de proxies
Una lista DIY significa verificaciones de salud, lógica de desalojo y IPs obsoletas. Un gateway es un endpoint que rota del lado del servidor —los reintentos aterrizan en IPs frescas por defecto.

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.

Conecta proxies rotativos a tu proyecto de Scrapy