Scrapy sigue siendo el caballo de batalla para los crawls en producción en 2026 — y sigue siendo el framework donde la configuración de proxy más confunde a la gente, porque hay cuatro lugares distintos donde conectar un proxy y tres de ellos están mal para la mayoría de los proyectos. Esta guía te da el correcto: un pequeño middleware personalizado con enrutamiento por petición, sesiones sticky, detección de baneos y un comportamiento de reintento sensato.
Si estás con requests/httpx/aiohttp a secas, consulta Cómo rotar proxies en Python. Este artículo es específico de Scrapy.
Para una puerta de enlace residencial rotativa, la configuración mínima viable es una línea por petición — sin middleware:
def start_requests(self):
for url in self.urls:
yield scrapy.Request(
url,
meta={"proxy": "http://USERNAME:[email protected]:913"},
)
El HttpProxyMiddleware integrado de Scrapy lee request.meta["proxy"] y gestiona la autenticación a partir de la URL. La puerta de enlace rota la IP de salida por ti. Si eso es todo lo que necesitas, detente aquí. El resto de esta guía es para cuando necesitas control: sesiones sticky, enrutamiento por país, rotación consciente de baneos y ajuste de concurrencia.
Coloca esto en middlewares.py. Asigna sesiones sticky por dominio, rota ante baneos y etiqueta cada petición para que puedas depurar qué sesión obtuvo qué:
import random
import string
GATEWAY = "us.jibaoproxy.com:913"
USERNAME = "USERNAME" # move to settings.py / env in real projects
PASSWORD = "PASSWORD"
def _new_session(n=8):
return "".join(random.choices(string.ascii_lowercase + string.digits, k=n))
class JibaoProxyMiddleware:
"""Sticky session per domain; rotate session on ban."""
def __init__(self):
self.sessions = {} # domain -> session id
def _proxy_url(self, session_id):
user = f"{USERNAME}-session-{session_id}"
return f"http://{user}:{PASSWORD}@{GATEWAY}"
def process_request(self, request, spider):
domain = request.url.split("/")[2]
session = self.sessions.setdefault(domain, _new_session())
request.meta["proxy"] = self._proxy_url(session)
request.meta["proxy_session"] = session
def rotate(self, domain):
"""Call when a session is burned."""
self.sessions[domain] = _new_session()
Y un middleware de descarga complementario que detecta baneos y reintenta en una sesión nueva:
from scrapy.downloadermiddlewares.retry import RetryMiddleware
from scrapy.utils.response import response_status_message
BAN_CODES = {403, 429}
BAN_MARKERS = (b"captcha", b"access denied", b"unusual traffic")
class BanAwareRetryMiddleware(RetryMiddleware):
def process_response(self, request, response, spider):
banned = (
response.status in BAN_CODES
or any(m in response.body[:2048].lower() for m in BAN_MARKERS)
)
if banned:
domain = request.url.split("/")[2]
proxy_mw = spider.crawler.engine.downloader.middleware.middlewares
for mw in proxy_mw:
if hasattr(mw, "rotate"):
mw.rotate(domain) # burn the session
reason = response_status_message(response.status)
return self._retry(request, reason, spider) or response
return super().process_response(request, response, spider)
Conecta ambos en settings.py:
DOWNLOADER_MIDDLEWARES = {
"myproject.middlewares.JibaoProxyMiddleware": 350,
"scrapy.downloadermiddlewares.retry.RetryMiddleware": None, # replace stock retry
"myproject.middlewares.BanAwareRetryMiddleware": 550,
}
RETRY_TIMES = 2
La prioridad importa: el middleware de proxy debe ejecutarse antes que el HttpProxyMiddleware de Scrapy (750), así que cualquier valor por debajo de 750 funciona; 350 lo mantiene temprano y predecible.
| Tipo de crawl | Modo | Implementación |
|---|---|---|
| Recolección de páginas sin estado | Rotativo | Usuario simple, la puerta de enlace rota por petición |
| Login + crawl tras autenticación | Sticky por cuenta | -session-{account_id}, nunca rotar a mitad del login |
| Listados con mucha paginación | Sticky por dominio, rotar ante baneo | El middleware de arriba |
| Precios específicos por geografía | Rotativo + fijación de país | Parámetros estilo USERNAME-country-de |
Tratamiento más profundo de este compromiso: Sesiones de proxy sticky vs rotativas.
Los valores por defecto de Scrapy están ajustados para un crawling cortés de una sola IP. Detrás de un pool rotativo puedes presionar mucho más — pero los límites por dominio aún importan porque el objetivo ve el comportamiento agregado:
# settings.py - sane starting point behind a residential pool
CONCURRENT_REQUESTS = 64
CONCURRENT_REQUESTS_PER_DOMAIN = 8 # what the target experiences
DOWNLOAD_DELAY = 0.25 # jitter applied per slot
RANDOMIZE_DOWNLOAD_DELAY = True # 0.5x-1.5x the delay
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_TARGET_CONCURRENCY = 6.0
DOWNLOAD_TIMEOUT = 30
ROBOTSTXT_OBEY = True
Sube CONCURRENT_REQUESTS_PER_DOMAIN solo después de observar tu tasa de 403 al nivel actual durante unos pocos miles de peticiones. Pasar de 8 → 32 porque "de todos modos los proxies rotan" es como la gente quema GB en reintentos.
IMAGES_STORE para que descarguen a través de un proxy de centro de datos o una conexión directa si al CDN no le importa — el multimedia es la mayor parte de tus GB y rara vez está protegido.HTTPCACHE_ENABLED = True mientras iteras sobre los parsers reproduce las respuestas desde disco en lugar de volver a descargarlas a través del pool. Este único ajuste suele reducir a la mitad la factura de ancho de banda de un proyecto.COMPRESSION_ENABLED = True (por defecto) — el HTML con gzip es 5–10 veces más pequeño en el cable.407 Proxy Authentication RequiredLas credenciales no llegaron al proxy. Ponlas en la URL (http://user:pass@host:port) dentro de meta["proxy"] — Scrapy las parsea y establece Proxy-Authorization por ti. Configurar la cabecera manualmente y usar credenciales en la URL provoca rarezas de doble autenticación; elige una.
TunnelError: Could not open CONNECT tunnelCasi siempre un host/puerto mal escrito, o un objetivo HTTPS a través de un endpoint que no permite CONNECT en ese puerto. Verifica primero con curl -x fuera de Scrapy.
Tu sesión sticky sobrevivió a su bienvenida, o tu tasa por dominio está demasiado caliente. El middleware consciente de baneos de arriba gestiona el primer caso; baja CONCURRENT_REQUESTS_PER_DOMAIN para el segundo. Si es un objetivo que verifica JA4, el propio stack TLS de Scrapy puede ser la señal delatora — consulta JA3/JA4 explicados para saber por qué ningún proxy arregla eso.
meta["proxy"] con la URL de la puerta de enlace es toda la configuración.Sesiones residenciales rotativas y sticky en una sola puerta de enlace. 500MB de tráfico gratis para crawlear.
Comenzar prueba gratuitaLos nuevos usuarios reciben 500MB al registrarse, más un bono en la primera recarga. Oferta por tiempo limitado.