Scrapy es uno de los frameworks de Python más potentes para web scraping, pero sin la configuración adecuada de proxies, tus scrapers recibirán bloqueos en cuestión de minutos. En esta guía, mostraré todas las formas de integrar proxies en Scrapy: desde la configuración más simple hasta métodos avanzados de rotación de direcciones IP con manejo automático de errores.
El material se basa en la experiencia real de scraping en grandes plataformas de comercio electrónico y sitios protegidos. Obtendrás ejemplos de código listos para usar en tus proyectos.
Por qué Scrapy sin proxies recibe bloqueos
Los sitios modernos utilizan protección en múltiples niveles contra el scraping. Incluso si has configurado el User-Agent y los retrasos entre solicitudes, tu dirección IP revela la automatización por varios signos:
- Frecuencia de solicitudes: una IP realiza 100+ solicitudes por minuto — un claro signo de un bot
- Patrones de comportamiento: navegación secuencial de páginas sin transiciones aleatorias
- Falta de JavaScript: Scrapy no ejecuta JS, lo que es fácil de detectar
- Geolocalización: acceso desde un centro de datos en lugar de una red doméstica
El resultado es un ban por IP durante varias horas o días. Los marketplaces (Amazon, Wildberries, Ozon), redes sociales y sitios con Cloudflare utilizan una protección especialmente agresiva. Los proxies resuelven este problema distribuyendo las solicitudes entre múltiples direcciones IP.
Importante: Incluso con proxies, es necesario respetar los límites de tasa. La velocidad recomendada: 1-3 solicitudes por segundo por IP. Para scraping de alta velocidad, utiliza un pool de 50+ proxies con rotación.
Configuración básica de proxy en Scrapy
La forma más sencilla es especificar el proxy directamente en la configuración del spider. Este método es adecuado para pruebas o scraping de pequeños volúmenes de datos con un solo servidor proxy.
Método 1: A través de meta en Request
import scrapy
class MySpider(scrapy.Spider):
name = 'example'
start_urls = ['https://example.com']
def start_requests(self):
proxy = 'http://username:password@proxy.example.com:8080'
for url in self.start_urls:
yield scrapy.Request(
url=url,
callback=self.parse,
meta={'proxy': proxy}
)
def parse(self, response):
# Tu lógica de scraping
self.log(f'Scraped {response.url} via {response.meta["proxy"]}')
El formato del proxy depende del protocolo y del método de autenticación:
http://proxy.example.com:8080— sin autenticaciónhttp://user:pass@proxy.example.com:8080— con usuario/contraseñasocks5://user:pass@proxy.example.com:1080— proxy SOCKS5
Método 2: Configuración global en settings.py
# settings.py
# Proxy HTTP para todas las solicitudes
HTTPPROXY_ENABLED = True
HTTPPROXY_AUTH_ENCODING = 'utf-8'
# Configuración a través de variables de entorno
HTTP_PROXY = 'http://username:password@proxy.example.com:8080'
HTTPS_PROXY = 'http://username:password@proxy.example.com:8080'
Este método es conveniente para pruebas rápidas, pero no es adecuado para producción: no hay rotación de IP, si el proxy falla, todo el scraper se detiene, y no es posible usar diferentes proxies para diferentes sitios.
Creación de un Proxy Middleware personalizado
Para el scraping en producción, se necesita un middleware propio que gestione el pool de proxies, maneje errores y rote automáticamente las IP. Aquí hay una implementación básica:
# middlewares.py
import random
from scrapy import signals
from scrapy.exceptions import NotConfigured
class RandomProxyMiddleware:
def __init__(self, proxy_list):
self.proxy_list = proxy_list
@classmethod
def from_crawler(cls, crawler):
# Cargamos la lista de proxies desde la configuración
proxy_list = crawler.settings.getlist('PROXY_LIST')
if not proxy_list:
raise NotConfigured('PROXY_LIST no configurado')
return cls(proxy_list)
def process_request(self, request, spider):
# Elegimos un proxy aleatorio del pool
proxy = random.choice(self.proxy_list)
request.meta['proxy'] = proxy
spider.logger.info(f'Usando proxy: {proxy}')
def process_exception(self, request, exception, spider):
# En caso de error, probamos con otro proxy
proxy = random.choice(self.proxy_list)
request.meta['proxy'] = proxy
spider.logger.warning(
f'Error de proxy, cambiando a: {proxy}'
)
return request
Ahora configuramos el uso del middleware en settings.py:
# settings.py
# Lista de proxies (se puede cargar desde un archivo o API)
PROXY_LIST = [
'http://user1:pass1@proxy1.example.com:8080',
'http://user2:pass2@proxy2.example.com:8080',
'http://user3:pass3@proxy3.example.com:8080',
# ... añade 50+ proxies para una rotación efectiva
]
# Conectamos el middleware
DOWNLOADER_MIDDLEWARES = {
'myproject.middlewares.RandomProxyMiddleware': 350,
'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware': 400,
}
# Reintentos en caso de errores
RETRY_TIMES = 3
RETRY_HTTP_CODES = [500, 502, 503, 504, 408, 429]
Rotación de proxies: tres métodos efectivos
La selección aleatoria de proxies (como en el ejemplo anterior) es el método más simple, pero no el más efectivo. Veamos tres estrategias de rotación para diferentes escenarios.
Método 1: Round-robin (rotación secuencial)
Los proxies se eligen en círculo. Adecuado para distribuir la carga de manera uniforme:
class RoundRobinProxyMiddleware:
def __init__(self, proxy_list):
self.proxy_list = proxy_list
self.current_index = 0
@classmethod
def from_crawler(cls, crawler):
proxy_list = crawler.settings.getlist('PROXY_LIST')
return cls(proxy_list)
def process_request(self, request, spider):
# Tomamos el siguiente proxy en círculo
proxy = self.proxy_list[self.current_index]
self.current_index = (self.current_index + 1) % len(self.proxy_list)
request.meta['proxy'] = proxy
Método 2: Rotación inteligente con blacklist
Seguimos los proxies problemáticos y los excluimos temporalmente de la rotación:
import time
from collections import defaultdict
class SmartProxyMiddleware:
def __init__(self, proxy_list):
self.proxy_list = proxy_list
self.proxy_errors = defaultdict(int)
self.blacklist = set()
self.blacklist_timeout = 300 # 5 minutos
self.blacklist_time = {}
@classmethod
def from_crawler(cls, crawler):
proxy_list = crawler.settings.getlist('PROXY_LIST')
return cls(proxy_list)
def get_working_proxies(self):
# Eliminamos de la blacklist los proxies cuyo tiempo de espera ha expirado
current_time = time.time()
expired = [
proxy for proxy, ban_time in self.blacklist_time.items()
if current_time - ban_time > self.blacklist_timeout
]
for proxy in expired:
self.blacklist.discard(proxy)
self.proxy_errors[proxy] = 0
# Devolvemos los proxies funcionales
return [p for p in self.proxy_list if p not in self.blacklist]
def process_request(self, request, spider):
working_proxies = self.get_working_proxies()
if not working_proxies:
spider.logger.error('¡Todos los proxies están en la blacklist!')
return
proxy = random.choice(working_proxies)
request.meta['proxy'] = proxy
def process_response(self, request, response, spider):
# Si recibimos un bloqueo — añadimos a la blacklist
if response.status in [403, 429, 503]:
proxy = request.meta.get('proxy')
self.proxy_errors[proxy] += 1
if self.proxy_errors[proxy] >= 3:
self.blacklist.add(proxy)
self.blacklist_time[proxy] = time.time()
spider.logger.warning(
f'Proxy {proxy} en la blacklist por {self.blacklist_timeout}s'
)
return response
Método 3: Rotación a través de la API del proveedor
Muchos proveedores de proxies (incluyendo proxies residenciales) ofrecen un endpoint rotativo: una URL que cambia automáticamente la IP en cada solicitud:
# settings.py
# Endpoint único con rotación automática
ROTATING_PROXY = 'http://username:password@rotating.proxy.com:8080'
# Middleware simple
class RotatingProxyMiddleware:
def __init__(self, proxy):
self.proxy = proxy
@classmethod
def from_crawler(cls, crawler):
proxy = crawler.settings.get('ROTATING_PROXY')
return cls(proxy)
def process_request(self, request, spider):
# Una URL, pero cada solicitud se realiza con una nueva IP
request.meta['proxy'] = self.proxy
Este es el método más conveniente para producción: no es necesario gestionar un pool de proxies, el proveedor se encarga de la calidad de las IP y reemplaza las problemáticas. Funciona especialmente bien con proxies residenciales, donde el pool de IP puede alcanzar millones de direcciones.
Autenticación: usuario/contraseña vs lista blanca de IP
Los proveedores de proxies ofrecen dos métodos de autenticación. La elección afecta la velocidad de conexión y la facilidad de configuración.
Autenticación User:Pass
El usuario y la contraseña se envían en la URL del proxy. Scrapy los convierte automáticamente en el encabezado HTTP Proxy-Authorization:
proxy = 'http://username:password@proxy.example.com:8080'
request.meta['proxy'] = proxy
# Scrapy añadirá automáticamente el encabezado:
# Proxy-Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
Ventajas: funciona desde cualquier IP, fácil de cambiar proxies en el código.
Desventajas: pequeño overhead en cada solicitud (~50-100ms), credenciales en texto claro en el código.
Autenticación IP Whitelist
Agregas la IP de tu servidor a la lista blanca del proveedor, no se requiere autenticación:
proxy = 'http://proxy.example.com:8080' # sin usuario/contraseña
request.meta['proxy'] = proxy
Ventajas: más rápido en 50-100ms, más seguro (sin credenciales en el código).
Desventajas: solo funciona desde ciertas IP, es necesario actualizar la lista blanca al cambiar de servidor.
Recomendación para producción:
Utiliza la lista blanca de IP para scraping desde servidores dedicados (AWS, Google Cloud, Hetzner). Para desarrollo y pruebas desde una máquina local — autenticación usuario:contraseña.
Manejo de errores y cambio automático de IP
Incluso con proxies de calidad, habrá errores: timeouts, rechazos de conexión, bloqueos. Un manejo adecuado de errores es crítico para el funcionamiento estable del scraper.
Manejo de códigos de estado HTTP
class ProxyMiddleware:
def process_response(self, request, response, spider):
# Códigos en los que se debe cambiar el proxy y reintentar
ban_codes = [403, 407, 429, 503]
if response.status in ban_codes:
proxy = request.meta.get('proxy')
spider.logger.warning(
f'Obtenido {response.status} de {proxy}, reintentando...'
)
# Marcamos para reintentar con un nuevo proxy
request.meta['dont_retry'] = False
request.meta['proxy'] = self.get_new_proxy()
return request
return response
Manejo de excepciones de red
from twisted.internet.error import TimeoutError, ConnectionRefusedError
from scrapy.exceptions import IgnoreRequest
class ProxyMiddleware:
def process_exception(self, request, exception, spider):
# Errores de conexión al proxy
proxy_errors = (
TimeoutError,
ConnectionRefusedError,
ConnectionLost,
)
if isinstance(exception, proxy_errors):
proxy = request.meta.get('proxy')
spider.logger.error(
f'Fallo de conexión en el proxy {proxy}: {exception}'
)
# Cambiamos el proxy y probamos de nuevo
request.meta['proxy'] = self.get_new_proxy()
return request
# Para otros errores utilizamos el manejo estándar
return None
Detección de bloqueos por contenido
Algunos sitios devuelven HTTP 200, pero muestran un captcha o una página de bloqueo:
class ProxyMiddleware:
def process_response(self, request, response, spider):
# Indicadores de bloqueo en el contenido
ban_indicators = [
'captcha',
'acceso denegado',
'bloqueado',
'tráfico inusual',
'verificación de robot',
]
body_text = response.text.lower()
if any(indicator in body_text for indicator in ban_indicators):
spider.logger.warning(
f'Página de bloqueo detectada desde {request.meta.get("proxy")}'
)
# Cambiamos el proxy y repetimos
request.meta['proxy'] = self.get_new_proxy()
return request
return response
Qué tipo de proxy elegir para Scrapy
La elección del tipo de proxy depende del sitio objetivo, el presupuesto y la velocidad de scraping requerida. Aquí hay una comparación de las principales opciones:
| Tipo de proxy | Velocidad | Costo | Cuándo usar |
|---|---|---|---|
| Proxy de centro de datos | Alta (50-200ms) | Baja ($1-3/IP) | Sitios simples sin protección, API, herramientas internas |
| Proxies residenciales | Media (300-800ms) | Media ($5-15/GB) | E-commerce, redes sociales, sitios con Cloudflare, geotargeting |
| Proxies móviles | Baja (500-1500ms) | Alta ($50-150/IP) | Aplicaciones móviles, Instagram, TikTok, máxima protección |
Recomendaciones para la elección
Para scraping de marketplaces (Amazon, Wildberries, Ozon, AliExpress) — solo proxies residenciales. Estos sitios banean agresivamente los centros de datos. Se necesita rotación y geotargeting (por ejemplo, IPs rusas para Wildberries).
Para scraping de sitios de noticias, blogs, foros — son adecuados los proxies de centro de datos. La protección es mínima, la velocidad y el bajo costo del tráfico son importantes.
Para scraping de sitios con Cloudflare — los proxies residenciales son obligatorios. Los centros de datos son detectados casi instantáneamente por Cloudflare. Añade a Scrapy la biblioteca cloudscraper para eludir los desafíos de JS.
Para scraping de Google Search, herramientas SEO — proxies residenciales con geotargeting. Google muestra diferentes resultados para diferentes países y ciudades.
Consejo: Comienza con un pool de 10 proxies residenciales para pruebas. Si recibes bloqueos, aumenta el pool a 50-100 IPs. Para scraping de alta velocidad (1000+ solicitudes/minuto), utiliza un endpoint rotativo con un pool de 10,000+ IPs.
Técnicas avanzadas: sesiones y IPs fijas
Al hacer scraping de ciertos sitios, es necesario mantener una IP durante toda la sesión (autenticación, carrito de compras, formularios de múltiples pasos). Aquí se explica cómo implementar sesiones fijas en Scrapy.
IP fija para un dominio
from urllib.parse import urlparse
class StickyProxyMiddleware:
def __init__(self, proxy_list):
self.proxy_list = proxy_list
# Diccionario: dominio -> proxy
self.domain_proxy_map = {}
@classmethod
def from_crawler(cls, crawler):
proxy_list = crawler.settings.getlist('PROXY_LIST')
return cls(proxy_list)
def process_request(self, request, spider):
# Extraemos el dominio de la URL
domain = urlparse(request.url).netloc
# Si ya hay un proxy para este dominio — lo usamos
if domain in self.domain_proxy_map:
proxy = self.domain_proxy_map[domain]
else:
# De lo contrario, elegimos uno nuevo y lo recordamos
proxy = random.choice(self.proxy_list)
self.domain_proxy_map[domain] = proxy
spider.logger.info(f'Asignado {proxy} a {domain}')
request.meta['proxy'] = proxy
IP fija con tiempo de sesión
Una variante más avanzada: el proxy se vincula al dominio durante un tiempo determinado (por ejemplo, 10 minutos), luego se cambia:
import time
from urllib.parse import urlparse
class SessionProxyMiddleware:
def __init__(self, proxy_list, session_timeout=600):
self.proxy_list = proxy_list
self.session_timeout = session_timeout # 10 minutos
# Diccionario: dominio -> (proxy, tiempo de creación)
self.sessions = {}
@classmethod
def from_crawler(cls, crawler):
proxy_list = crawler.settings.getlist('PROXY_LIST')
timeout = crawler.settings.getint('PROXY_SESSION_TIMEOUT', 600)
return cls(proxy_list, timeout)
def get_proxy_for_domain(self, domain):
current_time = time.time()
# Verificamos si hay una sesión activa
if domain in self.sessions:
proxy, created_at = self.sessions[domain]
# Si la sesión no ha expirado — usamos el mismo proxy
if current_time - created_at < self.session_timeout:
return proxy
# Creamos una nueva sesión con un nuevo proxy
new_proxy = random.choice(self.proxy_list)
self.sessions[domain] = (new_proxy, current_time)
return new_proxy
def process_request(self, request, spider):
domain = urlparse(request.url).netloc
proxy = self.get_proxy_for_domain(domain)
request.meta['proxy'] = proxy
Integración con Cookie Middleware
Para sesiones completas, es necesario sincronizar proxies y cookies. Scrapy almacena cookies por separado para cada dominio, pero al cambiar de proxy, es necesario limpiar las cookies:
# settings.py
# Activamos el middleware de cookies
COOKIES_ENABLED = True
COOKIES_DEBUG = False
# Middleware para sincronizar proxies y cookies
class ProxyCookieMiddleware:
def process_request(self, request, spider):
# Obtenemos el proxy actual
current_proxy = request.meta.get('proxy')
# Si el proxy ha cambiado — limpiamos las cookies
previous_proxy = request.meta.get('previous_proxy')
if previous_proxy and previous_proxy != current_proxy:
# Limpiamos las cookies para este dominio
jar = spider.crawler.engine.downloader.middleware.middlewares[0].jars
domain = urlparse(request.url).netloc
if domain in jar:
jar[domain].clear()
spider.logger.info(f'Cookies limpiadas para {domain}')
request.meta['previous_proxy'] = current_proxy
Conclusión
La configuración adecuada de proxies en Scrapy es la base para un scraping estable sin bloqueos. Hemos revisado todos los aspectos clave: desde la integración básica hasta técnicas avanzadas de rotación y gestión de sesiones.
Principales conclusiones:
- Para producción, utiliza un middleware personalizado con rotación inteligente y blacklist de IPs problemáticas
- Maneja todos los tipos de errores: códigos HTTP, excepciones de red, bloqueos por contenido
- Elige el tipo de proxy según la tarea: centros de datos para sitios simples, residenciales para protegidos
- Para sitios con autenticación, utiliza sesiones fijas vinculando el proxy al dominio
- Comienza con un pool de 10-50 proxies, escala a medida que aumenta la carga
Si planeas hacer scraping de sitios protegidos (marketplaces, redes sociales, sitios con Cloudflare), recomiendo utilizar proxies residenciales — proporcionan la máxima anonimidad y el mínimo riesgo de bloqueos. Para scraping de alta velocidad, elige proveedores con endpoint rotativo y un pool de 10,000 direcciones IP.
Todos los ejemplos de código de este artículo han sido probados en Scrapy 2.x y están listos para su uso en producción. Adáptalos a tus necesidades y escálalos a medida que crezca tu proyecto.