# Middleware de proxy Scrapy para proxies residenciales rotativos

> Middleware de proxy rotativo para Scrapy: IPs residenciales rotativas y fijas por solicitud, ajustes AutoThrottle y retry, scrapy-playwright. Código con sintaxis verificada.

- URL: https://crawlrelay.com/es/docs/scrapy/
- Última actualización: 2026-10-13
- Publisher: CrawlRelay (https://crawlrelay.com)

Un middleware downloader de 30 líneas que elige país y sesión por petición, más ajustes que mantienen un crawl respetuoso: robots.txt, concurrencia por dominio, AutoThrottle y reintentos.

**Respuesta rápida — ¿Cómo uso proxies rotativos con Scrapy?**

Pon `request.meta["proxy"]` en `http://USERNAME:PASSWORD@gw.crawlrelay.com:7777` en un middleware pequeño que corre antes del `HttpProxyMiddleware` de Scrapy; Scrapy envía las credenciales por ti. Con proxies residenciales **CrawlRelay**, país y sesión fija van en el usuario, así un middleware maneja ambos. El tráfico cuesta 0,50 US$/GB desde 50 GB. Se paga en crypto, sin KYC.

## Configuración rápida de Scrapy

| Atributo | Valor |
| --- | --- |
| Dónde va el proxy | `request.meta["proxy"]`, configurado por tu middleware |
| Orden del middleware | Tu middleware debajo de 750, prioridad del `HttpProxyMiddleware` integrado |
| Credenciales | En la URL del proxy; Scrapy lo convierte en una cabecera `Proxy-Authorization` |
| SOCKS5 | No con el manejador HTTP/1.1 por defecto; Scrapy 2.17+ soporta SOCKS vía HttpxDownloadHandler opcional. El puerto HTTP 7777 es lo más simple |
| Rotación | Nueva salida por conexión nueva; un ID de sesión único por solicitud normalmente da IP diferente |
| Cortesía | `ROBOTSTXT_OBEY`, `CONCURRENT_REQUESTS_PER_DOMAIN`, `AUTOTHROTTLE_ENABLED` |
| Reintentos | 429 y 5xx están en el `RETRY_HTTP_CODES` por defecto; Retry-After no se lee por defecto |

## ¿Cómo escribo el middleware de proxy?

Pon la clase en `middlewares.py`. Construye el usuario de las configuraciones y tres claves opcionales `meta`: `proxy_country`, `proxy_session` y `proxy_ttl`. Una solicitud sin `proxy_session` rota; con `proxy_session="new"` crea sesión nueva, por tanto IP salida distinta; con ID de sesión fijo comparte la salida con otras solicitudes del mismo flujo. El middleware recalcula el proxy en cada pasada, incluyendo retries, y no modifica proxies definidos por otro código. Si falta `CRAWLRELAY_USER`, el crawl se detiene al iniciar en vez de correr desde tu propia IP.

*middlewares.py*

```python
import uuid
from urllib.parse import quote


class CrawlRelayProxyMiddleware:
    """Sets request.meta["proxy"]; Scrapy's HttpProxyMiddleware then sends the credentials."""

    def __init__(self, user, password, country):
        self.user, self.password, self.country = user, password, country

    @classmethod
    def from_crawler(cls, crawler):
        s = crawler.settings
        if not s.get("CRAWLRELAY_USER"):
            # Fail loudly: NotConfigured would only disable the middleware and crawl from your own IP
            raise ValueError("CRAWLRELAY_USER is not set: refusing to crawl without the proxy")
        return cls(s.get("CRAWLRELAY_USER"), s.get("CRAWLRELAY_PASSWORD", ""), s.get("CRAWLRELAY_COUNTRY", "us"))

    def process_request(self, request, spider=None):
        if "proxy" in request.meta and not request.meta.get("_crawlrelay"):
            return None  # a proxy set by other code: leave it alone
        parts = [self.user, "country", request.meta.get("proxy_country", self.country)]
        session = request.meta.get("proxy_session")
        if session == "new":  # a fresh session for this request (and for each of its retries)
            session = uuid.uuid4().hex[:12]
        if session:
            parts += ["session", session, "ttl", str(request.meta.get("proxy_ttl", 10))]
        username = "-".join(parts)
        request.meta["proxy"] = "http://%s:%s@gw.crawlrelay.com:7777" % (
            quote(username, safe=""), quote(self.password, safe=""))
        request.meta["_crawlrelay"] = True
        return None
```

## ¿Qué configuraciones de Scrapy mantienen un crawl proxy rápido y respetuoso?

Los proxies distribuyen tus solicitudes en muchas IPs, pero el objetivo ve tu tasa total. Estas configuraciones mantienen cada dominio a un ritmo sostenible y dejan que AutoThrottle desacelere solo si las respuestas se vuelven lentas. Mantén `ROBOTSTXT_OBEY = True`, que ya configuran los nuevos proyectos Scrapy.

*settings.py (extracto)*

```python
import os

BOT_NAME = "myproject"
ROBOTSTXT_OBEY = True
USER_AGENT = "ExampleCrawler/1.0 (+https://example.com/crawler-info)"

CRAWLRELAY_USER = os.environ.get("CRAWLRELAY_USER")  # e.g. the username from your dashboard
CRAWLRELAY_PASSWORD = os.environ.get("CRAWLRELAY_PASSWORD")
CRAWLRELAY_COUNTRY = "us"

DOWNLOADER_MIDDLEWARES = {
    "myproject.middlewares.CrawlRelayProxyMiddleware": 350,  # runs before HttpProxyMiddleware (750)
}

CONCURRENT_REQUESTS = 32
CONCURRENT_REQUESTS_PER_DOMAIN = 4
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_START_DELAY = 1.0
AUTOTHROTTLE_MAX_DELAY = 30.0
AUTOTHROTTLE_TARGET_CONCURRENCY = 2.0

RETRY_ENABLED = True
RETRY_TIMES = 3
RETRY_HTTP_CODES = [429, 500, 502, 503, 504, 522, 524, 408]
DOWNLOAD_TIMEOUT = 45
```

## ¿Cómo mantengo una IP para paginación y rotar para las páginas de ítems?

Pasa un `proxy_session` fijo en las solicitudes de un flujo que dependa de la página anterior, y ninguno en páginas independientes. El spider a continuación corre en books.toscrape.com, un sitio sandbox publicado para práctica de scraping.

*spiders/catalog.py*

```python
import scrapy


class CatalogSpider(scrapy.Spider):
    name = "catalog"
    start_urls = ["https://books.toscrape.com/"]

    def parse(self, response):
        for href in response.css("article.product_pod h3 a::attr(href)").getall():
            # Product pages are independent: rotating exits
            yield response.follow(href, callback=self.parse_item)
        next_page = response.css("li.next a::attr(href)").get()
        if next_page:
            # The listing flow keeps one sticky exit for 30 minutes
            yield response.follow(next_page, callback=self.parse,
                                  meta={"proxy_session": "listing1", "proxy_ttl": 30})

    def parse_item(self, response):
        yield {
            "title": response.css("div.product_main h1::text").get(),
            "price": response.css("p.price_color::text").get(),
            "url": response.url,
        }
```

## ¿Scrapy reutiliza conexiones y eso detiene la rotación?

Sí, Scrapy mantiene conexiones activas y las reutiliza por proxy y host objetivo, así que solicitudes consecutivas en una conexión comparten una salida. Para la mayoría de los crawls esto está bien: Scrapy abre hasta `CONCURRENT_REQUESTS_PER_DOMAIN` conexiones por sitio, y cada conexión nueva generalmente obtiene una IP residencial diferente.

Cuando una solicitud debe salir por una IP nueva, por ejemplo un reintento tras un 502](https://crawlrelay.com/es/docs/errors/502-bad-gateway/) de una salida fallida, reenvíala con `response.request.replace(meta={**response.meta, "proxy_session": "new"}, dont_filter=True)`. El middleware crea un nuevo nombre de usuario de sesión, un usuario distinto significa una pool de conexiones distinta, por lo que Scrapy abre una conexión nueva y la gateway normalmente asigna una salida diferente.

## ¿Cómo respeto Retry-After en respuestas 429 en Scrapy?

El `RetryMiddleware` incorporado de Scrapy reintenta el 429 pero no espera el retraso `Retry-After`. AutoThrottle reduce la velocidad del dominio con el tiempo, lo cual suele ser suficiente. Para APIs que envían valores `Retry-After` largos, agrega un middleware pequeño que lea el encabezado y programe el reintento con retraso, o reduce `CONCURRENT_REQUESTS_PER_DOMAIN` para ese dominio. La página [429](https://crawlrelay.com/es/docs/errors/429-too-many-requests/) explica qué significa ese encabezado.

## ¿Cómo uso el proxy con scrapy-playwright?

Para páginas que requieren JavaScript, scrapy-playwright pasa las opciones de lanzamiento de Playwright a través de una configuración. Da las credenciales como campos separados: Chromium ignora credenciales incluidas dentro de la URL del servidor. Usa un contexto por sesión sticky si renderizas flujos sin login que abarcan varias páginas.

*settings.py para scrapy-playwright*

```python
import os

DOWNLOAD_HANDLERS = {
    "http": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler",
    "https": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler",
}
TWISTED_REACTOR = "twisted.internet.asyncioreactor.AsyncioSelectorReactor"

PLAYWRIGHT_LAUNCH_OPTIONS = {
    "proxy": {
        "server": "http://gw.crawlrelay.com:7777",
        "username": os.environ.get("CRAWLRELAY_USER", "USERNAME") + "-country-us",
        "password": os.environ.get("CRAWLRELAY_PASSWORD", "PASSWORD"),
    },
}
```

## Scrapy y proxies: preguntas

### ¿Necesito scrapy-rotating-proxies o una lista de proxies?

No. Esos paquetes rotan una lista de direcciones de proxies individuales y bloquean las que fallan. Con un gateway residencial, la rotación ocurre detrás de una dirección: cada conexión nueva generalmente obtiene una IP residencial diferente. Solo necesitas un middleware que configure `request.meta["proxy"]`.

### ¿Por qué Scrapy muestra TunnelError con mi proxy?

`TunnelError: Could not open CONNECT tunnel` significa que el proxy rechazó o falló la solicitud CONNECT para un sitio HTTPS. El estado en el mensaje te dice por qué: 407 es por credenciales, 502 o 504 es por la salida o el objetivo. Mira las páginas [407](https://crawlrelay.com/es/docs/errors/407-proxy-authentication-required/) y [502](https://crawlrelay.com/es/docs/errors/502-bad-gateway/).

### ¿Cuántas solicitudes concurrentes por dominio debo usar?

Empieza con 2 a 4 por dominio y deja que AutoThrottle ajuste. Aumenta solo cuando la proporción de respuestas 429, 403 y 5xx se mantenga estable y observa los tiempos de respuesta del sitio. Sitios grandes toleran más que sitios pequeños; un sitio pequeño puede necesitar un retraso entre solicitudes y una sola conexión.

### ¿Debo desactivar ROBOTSTXT_OBEY para obtener más datos?

No. robots.txt es la forma en que un sitio indica qué rutas pueden consultar clientes automatizados, y el formato se estandarizó con RFC 9309 en 2022. Ignorarlo incrementa tu riesgo legal y ético y viola nuestra [Política de Uso Aceptable](https://crawlrelay.com/es/acceptable-use/) cuando se usa para acceder a contenido excluido por el sitio.

### ¿Cuánto tráfico usa un crawl de Scrapy?

Scrapy descarga solo HTML, así que las páginas suelen costar decenas de kilobytes cada una, comprimidas. Revisa el valor `downloader/response_bytes` en las estadísticas que Scrapy imprime al final de la ejecución y divídelo por el número de respuestas. La [guía de ancho de banda](https://crawlrelay.com/es/guides/bandwidth-calculator/) convierte eso en un costo.

**Conecta IPs residenciales a tu proyecto Scrapy** 5,94 US$ para 10 GB para probar, 0,50 US$/GB de 50 GB. [Ver precios](https://crawlrelay.com/es/pricing/) · [Guía de concurrencia](https://crawlrelay.com/es/docs/concurrency/)

---

CrawlRelay: CrawlRelay es un servicio de proxies residenciales rotativos y proxies móviles 4G/5G para desarrolladores, ideal para scrapers y pipelines de datos de IA: tráfico prepago sin vencimiento desde 0,50 US$/GB, pagado con criptomonedas, sin subir documentos de identidad. Sin KYC — sin ID, sin selfie, sin documentos. Paga con criptomonedas.
