Conectar un proxy residencial a un script de Python es trabajo de cinco minutos una vez que has visto cómo se hace. La fricción nunca es el código, son los detalles que nadie anota: cómo las credenciales codifican el targeting, cuándo rotar frente a fijar una IP, cómo manejar los errores específicos del proxy, y las pequeñas diferencias de librería entre requests, httpx y Scrapy.
Esta es la guía que ojalá hubiera tenido. Ejemplos para copiar y pegar que de verdad corren, para las tres librerías que probablemente usas, más las partes que convierten un snippet que funciona en un scraper que sobrevive a producción.
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. Si estás en otro proveedor la forma es la misma; cambia el host y el formato de credenciales.
Lo único que tienes que entender primero
En un gateway residencial, 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. Un nombre de usuario se ve así:
customer-USERNAME-country-us-sid-abc123-ttl-600Léelo de izquierda a derecha: id de cuenta, luego flags. country-us apunta a EE. UU. sid-abc123 fija una sesión sticky. ttl-600 mantiene esa IP durante 600 segundos. Quita sid/ttl y cada petición rota a una IP nueva. La contraseña es constante. Ese es todo el modelo mental, el resto es solo enchufar esta cadena en el hueco de proxy de cada librería.
Mantén las credenciales en variables de entorno, nunca hardcodeadas:
import osUSER = os.environ["SHIFTER_USER"] # tu nombre de usuario de cuentaPASS = os.environ["SHIFTER_PASS"]GATEWAY = "p.shifter.io:443"requests: la versión de 6 líneas
requests toma un dict proxies. Ambas claves http y https apuntan a la misma URL de proxy http://, eso es correcto, requests tuneliza HTTPS a través de ella vía CONNECT.
import os, requests
USER = os.environ["SHIFTER_USER"]PASS = os.environ["SHIFTER_PASS"]GATEWAY = "p.shifter.io:443"
def proxy(country="us"): url = f"http://{USER}-country-{country}:{PASS}@{GATEWAY}" return {"http": url, "https": url}
r = requests.get("https://api.ipify.org?format=json", proxies=proxy("us"), timeout=30)print(r.json()) # {'ip': '<una IP residencial de EE. UU.>'}Córrelo dos veces y obtendrás dos IPs distintas, porque no hay sid, cada petición rota. Ese es el valor por defecto, y para la mayoría del scraping es exactamente lo que quieres.
Rotativo vs sticky, en código
Esta es la distinción que hace tropezar a la gente, así que aquí está en concreto. (Para la versión conceptual, ve proxies residenciales sticky vs rotativos.)
Rotativo (IP nueva en cada petición) es el valor por defecto, solo omite sid:
for _ in range(3): r = requests.get("https://api.ipify.org", proxies=proxy("us"), timeout=30) print(r.text) # tres IPs distintasSticky (la misma IP a lo largo de peticiones) necesita un id de sesión y un TTL en el nombre de usuario. Úsalo cuando un flujo abarca varias peticiones que deben parecer un solo usuario, un login, luego las páginas detrás de él:
def sticky_proxy(country="us", session="s1", ttl=600): url = f"http://{USER}-country-{country}-sid-{session}-ttl-{ttl}:{PASS}@{GATEWAY}" return {"http": url, "https": url}
s = requests.Session()p = sticky_proxy(session="checkout-42", ttl=600)for path in ("/login", "/cart", "/checkout"): r = s.get(f"https://shop.example{path}", proxies=p, timeout=30) # las tres peticiones comparten una IP hasta 600sElige el id de sesión tú mismo, cualquier cadena única por sesión lógica. La misma cadena devuelve la misma IP hasta que expira el TTL, luego rota manteniendo tus otros flags.
Geo-targeting
Como el targeting vive en el nombre de usuario, la geografía es solo otro flag. El país es el común; estado, ciudad y ASN funcionan igual:
def geo_proxy(country, city=None): parts = [USER, "country", country] if city: parts += ["city", city.lower().replace(" ", "_")] url = f"http://{'-'.join(parts)}:{PASS}@{GATEWAY}" return {"http": url, "https": url}
requests.get("https://example.com", proxies=geo_proxy("de")) # Alemaniarequests.get("https://example.com", proxies=geo_proxy("us", "new york")) # Nueva YorkLos nombres de ciudad usan guiones bajos para los espacios (new_york). Combina flags libremente; el orden no le importa al gateway.
httpx: la misma idea, lista para async
httpx es la opción moderna cuando quieres async o HTTP/2. El proxy va en el cliente. Fíjate en que el parámetro es proxy= (singular) en el httpx actual; las versiones antiguas usaban proxies=.
import os, httpx
USER = os.environ["SHIFTER_USER"]PASS = os.environ["SHIFTER_PASS"]GATEWAY = "p.shifter.io:443"
def proxy_url(country="us"): return f"http://{USER}-country-{country}:{PASS}@{GATEWAY}"
# Síncronowith httpx.Client(proxy=proxy_url("us"), timeout=30) as client: print(client.get("https://api.ipify.org").text)La versión async es donde httpx se gana el sueldo, despliega muchas peticiones concurrentes, cada una a través de una IP rotativa nueva:
import asyncio, httpx
async def fetch(client, url): r = await client.get(url, timeout=30) return r.status_code, r.text[:80]
async def main(urls): async with httpx.AsyncClient(proxy=proxy_url("us")) as client: return await asyncio.gather(*(fetch(client, u) for u in urls))
urls = ["https://api.ipify.org"] * 10print(asyncio.run(main(urls))) # 10 peticiones concurrentes, IPs rotativasEso son diez peticiones residenciales concurrentes en una docena de líneas. Cuida tu concurrencia, más no siempre es más rápido, y machacar un objetivo desde una ráfaga de IPs todavía puede activar la detección por comportamiento.
Scrapy: un middleware de proxy
Scrapy es el peso pesado para crawls grandes. La forma limpia de adjuntar proxies es fijar request.meta["proxy"] por petición, el HttpProxyMiddleware integrado de Scrapy lo lee y maneja la cabecera Proxy-Authorization a partir de las credenciales en la URL.
Un pequeño middleware que rota geo y da a cada petición una IP nueva:
import os
class ResidentialProxyMiddleware: def __init__(self): self.user = os.environ["SHIFTER_USER"] self.password = os.environ["SHIFTER_PASS"] self.gateway = "p.shifter.io:443"
def process_request(self, request, spider): country = request.meta.get("country", "us") request.meta["proxy"] = ( f"http://{self.user}-country-{country}:" f"{self.password}@{self.gateway}" )Actívalo en settings.py (debe correr antes del middleware de proxy por defecto):
DOWNLOADER_MIDDLEWARES = { "myproject.middlewares.ResidentialProxyMiddleware": 350, "scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware": 400,}Ahora cualquier spider enruta a través de IPs residenciales, y puedes apuntar por petición con Request(url, meta={"country": "gb"}). Para una sesión sticky, construye el nombre de usuario con un sid/ttl exactamente como en el ejemplo de requests y fíjalo en meta["proxy"].
Manejar los errores que de verdad ocurren
Un scraper que ignora los errores de proxy funciona en la demo y muere de madrugada. Tres que encontrarás:
- 407 Proxy Authentication Required, usuario/contraseña incorrectos, o un flag no reconocido (una errata en
countryoasn). Arregla la cadena de credenciales; reintentar no ayudará. - 429 Too Many Requests, el objetivo te está limitando la tasa. Haz backoff, ve más despacio, y rota IPs (cosa que el gateway ya hace por petición cuando no eres sticky).
- 502 Bad Gateway, ninguna IP coincide ahora mismo con tu filtro (normalmente geo demasiado ajustado como country+city+asn). Afloja un flag y reintenta.
Un wrapper de reintento mínimo que hace backoff y se rinde limpiamente:
import time, requests
def get_with_retry(url, proxies, tries=4): for attempt in range(tries): try: r = requests.get(url, proxies=proxies, timeout=30) if r.status_code in (429, 502, 503): raise requests.exceptions.RequestException(f"status {r.status_code}") r.raise_for_status() return r except requests.exceptions.RequestException as e: if attempt == tries - 1: raise sleep = 2 ** attempt # 1s, 2s, 4s print(f"retry {attempt+1}: {e}; sleeping {sleep}s") time.sleep(sleep)Backoff exponencial más rotación por petición maneja la gran mayoría de los fallos transitorios. Si te están bloqueando consistentemente en lugar de intermitentemente, el problema no son los reintentos, es la calidad de la IP o el comportamiento de las peticiones, cubierto en cómo evitar que te bloqueen al hacer scraping.
Verificar que de verdad funciona
Antes de confiar en una configuración, confirma dos cosas: que la IP cambia (rotación) y que está en el país correcto (geo). Una comprobación rápida:
import requests
r = requests.get("http://ip-api.com/json", proxies=proxy("de"), timeout=30)data = r.json()print(data["query"], data["countryCode"]) # se espera una IP DELlámala unas cuantas veces sin un sid y deberías ver IPs distintas, todas DE. Si el país es incorrecto, revisa la ortografía de tu flag. Si la IP nunca cambia, dejaste un sid por accidente en el nombre de usuario.
Una nota sobre SOCKS5
Todo lo de arriba usa proxies HTTP, el valor por defecto correcto para scraping web. Si tu carga necesita SOCKS5 (tráfico no HTTP, o una herramienta que lo espera), el mismo gateway lo habla, cambia el esquema a socks5h:// e instala requests[socks] (o usa el extra de SOCKS de httpx). Los tradeoffs están en HTTP vs SOCKS5 proxies; para scraping simple, quédate con HTTP.
Preguntas frecuentes
¿Necesito un endpoint distinto para cada país?
No. Un endpoint, p.shifter.io:443, para todo. País, ciudad y sesión cambian todos vía la cadena del nombre de usuario, no el host. Ese es el núcleo del modelo de gateway.
¿Por qué ambas claves http y https apuntan a una URL http:// en requests?
Porque requests envía HTTPS a través de un proxy HTTP vía un túnel CONNECT. El esquema de la URL del proxy describe cómo hablas con el proxy (HTTP), no con el objetivo. Esto es correcto y estándar, no pongas la clave https en https://.
¿Cómo roto IPs en cada petición?
Omite sid del nombre de usuario. Sin id de sesión, el gateway te da una IP nueva por petición automáticamente. No gestionas una lista de proxies, la rotación del pool es del lado del servidor.
¿Cómo mantengo la misma IP a lo largo de un flujo multi-paso?
Añade sid-<tu-id>-ttl-<segundos> al nombre de usuario y reúsalo en cada petición del flujo. El mismo id, la misma IP, hasta que expire el TTL.
Mis peticiones son lentas. ¿Es el proxy el problema?
Normalmente no. Las IPs residenciales añaden algo de latencia frente a una conexión directa, pero la lentitud a escala más a menudo es demasiada concurrencia, no reusar conexiones (usa un Session/Client), o un objetivo lento. Perfila antes de culpar al proxy.
¿Funciona también con aiohttp, urllib3 o pycurl?
Sí. Cualquier cliente que acepte una URL de proxy autenticada http://user:pass@host:port funciona, el targeting codificado en las credenciales es idéntico. requests, httpx y Scrapy son solo los tres más comunes.
Cerrando
El patrón es el mismo en cada librería: construye la URL http://USER-flags:PASS@p.shifter.io:443, ponla en el hueco de proxy, omite sid para rotar o añádelo para fijar. La geo es un flag, los errores son un pequeño bucle de reintento, y la concurrencia es lo que tu cliente ya hace.
Empieza desde los snippets de arriba, apúntalos al gateway residencial, y tienes código de proxy con forma de producción en una tarde. Los planes y las tarifas por GB están en la página de precios, y la referencia completa de flags vive en la documentación del gateway.