Volver al blog

Configuración de proxy en el marco Scrapy: guía completa con ejemplos de código

Guía completa para la integración de proxies en Scrapy: desde la configuración básica hasta métodos avanzados de rotación de direcciones IP con ejemplos de código funcional.

📅14 de febrero de 2026
```html

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ón
  • http://user:pass@proxy.example.com:8080 — con usuario/contraseña
  • socks5://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.

```