Conocimiento

Cómo rotar proxies residenciales en Scrapy con middleware personalizado

Scrapy fija el proxy por meta, pero la trampa del cacheo de Proxy-Authorization rompe la rotación. Un middleware personalizado que rota identidad y reintenta ante bloqueos.

Chris Collins

Chris Collins

8 de agosto de 2026 · 8 min de lectura

Scrapy es el framework al que recurres cuando un scrape supera a un script: maneja la programación, la concurrencia, los reintentos y los pipelines de fábrica. Añadir un proxy residencial es directo, pero Scrapy lo hace distinto de un cliente HTTP simple. El proxy es un ajuste por petición manejado por un downloader middleware, y esa arquitectura tiene una trampa específica alrededor de la autenticación que rompe la rotación en silencio. Entiende el modelo de middleware y todo encaja.

Esta es la entrada de Scrapy junto a proxies residenciales con Python, que cubre requests y httpx. El pipeline de downloader middleware de Scrapy es otra bestia, así que recibe su propio tratamiento aquí.

Todo lo de abajo usa el gateway residencial de Shifter: un endpoint, p.shifter.io:443, con todo el targeting codificado en el nombre de usuario. Cambia el host y las credenciales por otro proveedor; la forma es la misma.

El modelo del gateway en un párrafo

El nombre de usuario del proxy lleva tu autenticación y tu targeting. No cambias de endpoint para cambiar de país o de sesión, cambias la cadena del nombre de usuario:

customer-USERNAME-country-us-sid-abc123-ttl-600

country-us apunta a Estados Unidos, sid fija una sesión sticky, ttl mantiene esa IP durante N segundos. Omite sid/ttl y cada conexión nueva rota. La contraseña se mantiene constante. En Scrapy, ese nombre de usuario se vuelve la cabecera Proxy-Authorization, y rotar la identidad significa cambiarla por petición.

Cómo maneja Scrapy los proxies

Scrapy enruta el proxy de cada petición a través del HttpProxyMiddleware incorporado, que lee request.meta['proxy']. El montaje ingenuo es poner las credenciales en línea en esa URL:

# El one-liner tentador. Funciona, hasta que rotas.
request.meta['proxy'] = 'http://customer-USER-country-us:PASS@p.shifter.io:443'

Esto funciona para una única identidad fija. Se rompe en el momento en que intentas rotar, y la razón es la trampa que vale la pena conocer.

La trampa: Proxy-Authorization se cachea

Cuando HttpProxyMiddleware ve credenciales en la URL del proxy, las codifica en base64 en una cabecera Proxy-Authorization y, crucialmente, cachea esa cabecera en la petición. Si una petición se reintenta o redirige más tarde y su meta['proxy'] cambia a una identidad distinta, el middleware no siempre recomputa la cabecera, así que la petición sale con un Proxy-Authorization obsoleto para el nombre de usuario anterior. En un gateway donde el nombre de usuario lleva tu geo y tu sesión, eso significa que tu rotación en silencio no rota: cambias el nombre de usuario en meta['proxy'], pero la petición aún se autentica como la vieja.

El arreglo es dejar de poner credenciales en la URL del proxy por completo. Fija el host del proxy sin userinfo, y fija la cabecera Proxy-Authorization tú mismo, explícitamente, en cada petición. Para eso es exactamente un middleware personalizado.

Un middleware de rotación personalizado

Pon el host en meta['proxy'] sin credenciales, y computa la cabecera de auth por petición a partir de la identidad que quieres. Como el targeting vive en el nombre de usuario, elegir un país y una sesión es solo construir el nombre de usuario correcto.

middlewares.py
import os
from w3lib.http import basic_auth_header
class ShifterProxyMiddleware:
def __init__(self):
self.user = os.environ['SHIFTER_USER']
self.password = os.environ['SHIFTER_PASS']
self.endpoint = 'http://p.shifter.io:443' # solo host, sin credenciales
def process_request(self, request, spider):
country = request.meta.get('country', 'us')
sid = request.meta.get('sid') # ponlo para una sesión sticky, omítelo para rotar
username = f"{self.user}-country-{country}" + (f"-sid-{sid}-ttl-600" if sid else "")
request.meta['proxy'] = self.endpoint
request.headers['Proxy-Authorization'] = basic_auth_header(username, self.password)

Actívalo, y déjalo correr antes del middleware de proxy incorporado para que la cabecera que fijas sea la que se envía:

settings.py
DOWNLOADER_MIDDLEWARES = {
'myproject.middlewares.ShifterProxyMiddleware': 350, # antes de HttpProxyMiddleware (750)
}

Ahora cada petición lleva su propio Proxy-Authorization recién computado, así que cambiar country o sid en el meta de una petición de verdad cambia la identidad. Fija sid en las peticiones que pertenecen a una unidad lógica de trabajo para que compartan una IP, y déjalo fuera para rotar por conexión (sticky vs rotativo cubre la distinción). Mapear el trabajo a identidades de esta forma es el patrón de reparto de carga en forma de Scrapy.

Rota en el reintento, no solo por programación

El RetryMiddleware de Scrapy ya reintenta timeouts y 5xx, pero por defecto reintenta con la misma identidad, lo cual es inútil si la razón del fallo fue que esa identidad se bloqueó. El movimiento de alto valor es rotar la identidad específicamente cuando una petición falla o vuelve desafiada. En tu middleware, detecta un bloqueo suave o un 403/429 y reprograma la petición con una identidad nueva:

def process_response(self, request, response, spider):
if response.status in (403, 429) or looks_blocked(response):
new = request.copy()
new.meta.pop('sid', None) # descarta la sesión quemada -> IP fresca
new.dont_filter = True
return new # reintenta a través de una identidad nueva
return response

Detectar el bloqueo suave es su propia disciplina, un 200 aún puede ser una página de bloqueo, así que combina esto con las comprobaciones de detectar contenido bloqueado o falso. Tratar una respuesta desafiada como una señal para rotar, en lugar de aceptarla, es lo que mantiene vivo un crawl largo.

Usa las perillas de cortesía de Scrapy

Scrapy te da los controles de límite de ritmo que un scraper hecho a mano tiene que construir, y con una flota de proxies importan más, no menos. Limita la concurrencia por dominio para que un destino no reciba una paliza, añade un retardo, y activa AutoThrottle para adaptarte a las respuestas del sitio:

settings.py
CONCURRENT_REQUESTS = 32
CONCURRENT_REQUESTS_PER_DOMAIN = 8 # tope por destino, el que importa
DOWNLOAD_DELAY = 0.5
AUTOTHROTTLE_ENABLED = True
RETRY_ENABLED = True
RETRY_TIMES = 3

La concurrencia por dominio es la perilla que te impide convertir un pool de proxies en un martillo distribuido. Más paralelismo más allá de la tolerancia de un destino compra bloqueos, no throughput (cómo evitar que te bloqueen y scrapear responsablemente ambos aplican), y AutoThrottle retrocediendo ante respuestas lentas es justo la contención que un crawl sano de larga duración necesita.

Verifica que de verdad estás en el proxy

Apunta un spider a un endpoint que devuelva la IP y comprueba la IP de salida antes de fiarte de una corrida:

def start_requests(self):
yield scrapy.Request('http://ip-api.com/json',
meta={'country': 'us'},
callback=self.parse) # espera una IP residencial de EE. UU.

Tu propia IP significa que el middleware no se aplica, o está ordenado después de HttpProxyMiddleware. Un muro de timeouts significa que la cabecera de auth está mal o falta. Ambos se cubren en la guía de diagnóstico de timeouts.

Preguntas frecuentes

¿Por qué mi rotación de proxy no rota de verdad en Scrapy? Casi seguro la trampa del cacheo de Proxy-Authorization: pusiste credenciales en la URL del proxy, y HttpProxyMiddleware cacheó la cabecera de auth, así que cuando cambias meta['proxy'] en un reintento la petición aún envía las credenciales viejas. Fija el host sin credenciales y computa la cabecera Proxy-Authorization tú mismo en un middleware, por petición.

¿Dónde van los flags de targeting? En el nombre de usuario, que se vuelve el Proxy-Authorization. Un middleware personalizado construye customer-USER-country-<cc>-sid-<id>-ttl-<seg> a partir del meta por petición, así que elegir geo y sesión es solo fijar country y sid en la petición.

¿Cómo le doy a una petición específica una sesión sticky? Fija un sid estable en el meta de esa petición y reúsalo entre las peticiones que van juntas; omite sid para rotar en cada conexión. El middleware lo convierte en el nombre de usuario correcto.

¿Debo rotar en cada petición o en el reintento? Ambos tienen su lugar. Rota por unidad lógica de trabajo para el tráfico normal, y además fuerza una identidad fresca cuando una petición vuelve bloqueada o limitada, para que una IP quemada no se reintente como sí misma.

¿Sigo necesitando DOWNLOAD_DELAY y AutoThrottle con proxies rotativos? Sí. La rotación reparte la carga entre IPs, pero la concurrencia por dominio, el retardo y AutoThrottle te impiden abrumar a un único destino sin importar cuántas IPs tengas. La cortesía y la rotación resuelven problemas distintos.

En resumen

Scrapy más proxies residenciales es potente en cuanto rodeas su única trampa: no pongas credenciales en la URL del proxy, porque el Proxy-Authorization cacheado derrota la rotación en silencio. En su lugar, escribe un pequeño downloader middleware que fije el host en meta['proxy'] y compute la cabecera Proxy-Authorization por petición a partir de la identidad que quieres, rota esa identidad por unidad lógica de trabajo y de nuevo ante cualquier respuesta bloqueada o limitada, y apóyate en la concurrencia por dominio y AutoThrottle de Scrapy para seguir siendo cortés.

Haz eso y el scheduler, los reintentos y los pipelines de Scrapy trabajan con tu capa de proxy en lugar de contra ella. Apunta el crawl al gateway residencial, y recuerda que la calidad del pool decide con qué frecuencia reintentas siquiera (reputación de IP). La página de precios tiene los planes por GB para probarlo contra tus propios destinos.

¿Listo para empezar?

Prueba los proxies residenciales de Shifter, más de 205M IPs, más de 195 países, desde 0,75 $/GB.

Comenzar