# 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 entorno `http_proxy` / `https_proxy` configuran un spider sin código. `no_proxy` se respeta solo para esquemas http y https.
- Por solicitud, `request.meta['proxy']` siempre gana sobre el entorno. Establécelo en `None` para 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 encabezado `Proxy-Authorization: Basic`. La codificación predeterminada es latin-1 y se puede configurar a través de `HTTPPROXY_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:

```python
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@residentialpremium.quantumproxies.io:8080"},
        )
```

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](https://quantumproxies.io/rotating-proxies) 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:

```python
# middlewares.py
class RotatingGatewayMiddleware:
    PROXY = "http://USER:PASS@residentialpremium.quantumproxies.io:8080"

    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.

![Una solicitud de Scrapy recorre la cadena de downloader middlewares: el spider la emite, tu middleware con prioridad 350 fija meta'proxy' en el gateway, el HttpProxyMiddleware integrado en 750 convierte las credenciales en un encabezado Proxy-Authorization y el descargador sale por el gateway rotativo; los reintentos obtienen una IP nueva.](https://res.cloudinary.com/dmqliab0t/image/upload/f_auto,q_auto,w_1200/v1785335142/blog/generated/qp-b9-002-diag1.png)
*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:

```python
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](https://quantumproxies.io/blog/avoid-ip-ban-web-scraping).

## 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:

```python
# 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](https://quantumproxies.io/blog/fix-429-too-many-requests-scraping) cubre los presupuestos de concurrencia en profundidad.

![Dos configuraciones de rotación en Scrapy lado a lado: una lista de proxies DIY que se reduce a medida que se expulsan las IPs bloqueadas y se vuelven a encolar las solicitudes, y un gateway rotativo que es un único endpoint que rota en el servidor. La tarjeta: una lista implica comprobaciones de estado y expulsiones, 20 IPs estáticas pueden evaporarse en minutos, un gateway pone cada reintento en una IP nueva.](https://res.cloudinary.com/dmqliab0t/image/upload/f_auto,q_auto,w_1200/v1785335142/blog/generated/qp-b9-002-diag2.png)
*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](https://quantumproxies.io/blog/scraper-returns-empty-page-javascript). Y cuando un dominio derrota el HTTP simple sin importar la IP, entrega ese dominio a una [Scraper API](https://quantumproxies.io/scraper-api) que renderiza JavaScript y devuelve HTML limpio o markdown —Scrapy lo consume como cualquier otra respuesta, y mantienes tu pipeline.

```python
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](https://quantumproxies.io/blog/headless-browser-vs-http-requests-cost) 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](https://quantumproxies.io/rotating-proxies)
