Conectar un proxy residencial a Playwright son unas pocas líneas una vez que lo has visto hecho. La fricción nunca es la opción de lanzamiento, son los detalles que nadie escribe: cómo las credenciales codifican el targeting, el detalle de Chromium que rompe los proxies por contexto, cómo la rotación se mapea sobre los contextos de navegador, y el hecho de que cada imagen que carga el navegador es ancho de banda que pagas.
Esta es la compañera de cómo usar proxies residenciales con Python, para el lado del navegador. Ejemplos de copiar y pegar que de verdad corren, más las partes que convierten un snippet que funciona en automatización 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 credencial. Los ejemplos son Node/JavaScript; un snippet de Playwright en Python está cerca del final.
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 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 nueva conexión rota a una IP nueva. La contraseña es constante. Ese es todo el modelo mental, el resto es enchufar esto en el hueco de proxy de Playwright.
Guarda las credenciales en variables de entorno, nunca hardcodeadas:
const USER = process.env.SHIFTER_USER; // el nombre de usuario de tu cuentaconst PASS = process.env.SHIFTER_PASS;const GATEWAY = "http://p.shifter.io:443";Una nota específica de Playwright por adelantado: no pongas el usuario y la contraseña en línea en la URL del proxy. Chromium ignora las credenciales de proxy en línea, así que http://user:pass@host falla silenciosamente al autenticar. Playwright tiene campos username y password dedicados en el objeto de proxy, usa esos.
La versión de 10 líneas
Para un único proxy en todo el navegador, pon proxy en launch():
const { chromium } = require("playwright");
const USER = process.env.SHIFTER_USER;const PASS = process.env.SHIFTER_PASS;
(async () => { const browser = await chromium.launch({ proxy: { server: "http://p.shifter.io:443", username: `${USER}-country-us`, // el targeting vive aquí password: PASS, }, }); const page = await browser.newPage(); await page.goto("https://api.ipify.org?format=json"); console.log(await page.textContent("body")); // {"ip":"<una IP residencial de EE. UU.>"} await browser.close();})();El campo username es donde vive todo el modelo del gateway. ${USER}-country-us apunta a EE. UU.; añade sid/ttl para una IP sticky, añade city/asn para acotar más. La contraseña nunca cambia.
El detalle de Chromium: proxies por contexto
La versión de proxy único está bien para una identidad. Pero la verdadera razón para usar Playwright es correr muchas identidades, y para eso quieres un proxy distinto por contexto de navegador (cada contexto es sus propias cookies/almacenamiento aislados, es decir, su propio “usuario”).
Aquí está la trampa: en Chromium, los proxies por contexto solo funcionan si lanzas el navegador con un proxy ya puesto. Lanza sin proxy y cada newContext({ proxy }) se ignora silenciosamente. El arreglo es un servidor placeholder en el lanzamiento:
const browser = await chromium.launch({ proxy: { server: "per-context" }, // placeholder, habilita el proxy por contexto});
// Ahora cada contexto puede llevar su propio proxy + targeting realesconst ctxUS = await browser.newContext({ proxy: { server: "http://p.shifter.io:443", username: `${USER}-country-us-sid-a1-ttl-600`, password: PASS, },});const ctxDE = await browser.newContext({ proxy: { server: "http://p.shifter.io:443", username: `${USER}-country-de-sid-b2-ttl-600`, password: PASS, },});ctxUS navega en una IP de EE. UU., ctxDE en una alemana, al mismo tiempo, en un navegador. Este es el patrón sobre el que construir. (Firefox y WebKit no necesitan estrictamente el placeholder, pero ponerlo es inofensivo y mantiene tu código cross-browser.)
Rotativo vs sticky, a la manera de Playwright
En un scraper HTTP crudo a menudo rotas en cada petición. En un navegador, eso suele estar mal: un usuario real no cambia de IP a mitad de sesión, así que una IP que salta entre cargas de página mientras las cookies se mantienen constantes es una señal de detección. El mapeo idiomático es:
- Un contexto = una identidad sticky. Dale a cada contexto un
sidúnico y unttlque cubra la sesión, así todas sus cargas de página comparten una IP, como un usuario real. - Rota creando nuevos contextos. Un contexto fresco con un
sidnuevo (o sinsid) obtiene una IP nueva. Cierra el viejo cuando termines.
async function contextForSession(browser, sessionId, country = "us") { return browser.newContext({ proxy: { server: "http://p.shifter.io:443", username: `${USER}-country-${country}-sid-${sessionId}-ttl-600`, password: PASS, }, });}
// Procesar 50 objetivos, cada uno con su propia identidad de IP establefor (let i = 0; i < 50; i++) { const ctx = await contextForSession(browser, `job-${i}`, "us"); const page = await ctx.newPage(); await page.goto("https://example.com/item/" + i); // ... scrapear ... await ctx.close(); // terminado con esta identidad}Elige el sid tú mismo, cualquier cadena única por sesión lógica. La misma cadena devuelve la misma IP hasta que expire el TTL. Corre los contextos concurrentemente (un pool pequeño) en lugar de 50 a la vez; más navegadores no es más velocidad, y una ráfaga de IPs golpeando un objetivo aún dispara la detección de comportamiento. Si te bloquean de forma consistente en vez de intermitente, el problema es la calidad de IP o el comportamiento, no la rotación, ve cómo evitar que te bloqueen al scrapear.
Geo-targeting, y hacer que el navegador coincida con la IP
Como el targeting vive en el nombre de usuario, geo es solo un flag: country-de, añade city-berlin o asn-3320 para acotar. Pero con un navegador real hay una segunda mitad que la gente se pierde: la huella de tu navegador tiene que estar de acuerdo con tu IP. Una IP residencial alemana emparejada con un locale en-US y una zona horaria de Nueva York es una contradicción que los sistemas anti-bot marcan a primera vista. (Ve por qué se bloquean los scrapers.)
Playwright te deja alinear todo en el contexto:
const ctx = await browser.newContext({ proxy: { server: "http://p.shifter.io:443", username: `${USER}-country-de-city-berlin-sid-de1-ttl-600`, password: PASS, }, locale: "de-DE", timezoneId: "Europe/Berlin", geolocation: { latitude: 52.52, longitude: 13.405 }, permissions: ["geolocation"],});Los nombres de ciudad usan el formato del gateway (minúsculas); combina flags libremente, el orden no le importa al gateway. Hacer coincidir locale/timezoneId/geolocation con el país del proxy es una de las cosas de mayor palanca que puedes hacer para parecer un usuario local genuino.
Recorta tu ancho de banda (pagas por GB)
Este es específico de los navegadores e importa, porque los proxies residenciales se facturan por gigabyte. Un navegador headless, dejado solo, descarga cada imagen, fuente, vídeo, y script de tracking de la página, la mayoría de los cuales no necesitas para scrapear. Abortarlos puede recortar el ancho de banda (y el coste) en bastante más de la mitad, y acelera tus corridas.
Usa context.route() para bloquear los tipos de recurso pesados antes de que lleguen al proxy:
await ctx.route("**/*", (route) => { const type = route.request().resourceType(); if (["image", "media", "font"].includes(type)) { return route.abort(); // nunca descargado, nunca facturado } return route.continue();});Mantén document, script, xhr, y fetch (los datos reales y el JS que los renderiza); descarta el resto. Si el sitio hace lazy-load de contenido detrás de imágenes, prueba que tu scrape aún funciona, pero para la mayoría de los trabajos esto es dinero gratis. Encaja bien con el modelo por GB cubierto en proxies residenciales facturados por ancho de banda.
Verificar que de verdad funciona
Antes de confiar en una configuración, confirma dos cosas: la IP está en el país correcto (geo) y cambia entre contextos (rotación). Una comprobación rápida:
const page = await ctx.newPage();await page.goto("http://ip-api.com/json");const data = JSON.parse(await page.textContent("body"));console.log(data.query, data.countryCode); // espera una IP DELevanta dos contextos con sids distintos y deberías ver dos IPs distintas, ambas del país correcto. Si el país está mal, revisa la ortografía de tus flags. Si la IP nunca cambia entre contextos, probablemente olvidaste el placeholder en el lanzamiento y Chromium está ignorando el proxy por contexto.
Los errores que de verdad pasan
Playwright expone los problemas de proxy como errores de navegación, no como códigos de estado HTTP. Los tres que te encontrarás:
net::ERR_TUNNEL_CONNECTION_FAILEDfalló la autenticación del proxy, o un flag no reconocido (un typo encountryoasn, o credenciales en línea que Chromium ignoró). Arregla los campos username/password; reintentar no ayudará.net::ERR_PROXY_CONNECTION_FAILEDno se pudo alcanzar el gateway, o ninguna IP coincidió con un filtro demasiado ajustado (como country + city + asn). Revisa la conectividad, luego afloja un flag.- Una página que carga pero muestra un bloqueo/CAPTCHA el objetivo, no el proxy, te está rechazando. Rota a un nuevo contexto/IP, baja el ritmo, y alinea la huella como arriba.
Envuelve la navegación en un pequeño retry que hace un contexto fresco al fallar:
async function gotoWithRetry(browser, url, country = "us", tries = 3) { for (let attempt = 0; attempt < tries; attempt++) { const ctx = await contextForSession(browser, `r-${Date.now()}-${attempt}`, country); const page = await ctx.newPage(); try { await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30000 }); return { ctx, page }; // el llamador cierra ctx al terminar } catch (e) { await ctx.close(); if (attempt === tries - 1) throw e; await new Promise((r) => setTimeout(r, 2 ** attempt * 1000)); // 1s, 2s } }}Un contexto nuevo significa una IP nueva, así que un reintento de verdad consigue una ruta distinta, no la misma que falla.
Una nota sobre SOCKS5
El gateway habla SOCKS5, pero hay una limitación de Playwright que vale la pena conocer: Playwright/Chromium no soporta proxies SOCKS autenticados. Como el gateway autentica por nombre de usuario (ahí es donde vive tu targeting), SOCKS5 no es práctico aquí, usa el proxy HTTP mostrado arriba, que de todos modos es el default correcto para scraping de navegador. Los tradeoffs de SOCKS están en proxies SOCKS5 para automatización.
Lo mismo en Python
La API de Python de Playwright refleja la de JavaScript; el objeto de proxy es idéntico:
from playwright.sync_api import sync_playwrightimport os
USER, PASS = os.environ["SHIFTER_USER"], os.environ["SHIFTER_PASS"]
with sync_playwright() as p: browser = p.chromium.launch(proxy={"server": "per-context"}) ctx = browser.new_context(proxy={ "server": "http://p.shifter.io:443", "username": f"{USER}-country-us-sid-a1-ttl-600", "password": PASS, }) page = ctx.new_page() page.goto("https://api.ipify.org") print(page.text_content("body")) browser.close()La misma regla de placeholder-en-el-lanzamiento, el mismo targeting codificado en el nombre de usuario. Para trabajo HTTP sin navegador, la guía de Python cubre requests, httpx, y Scrapy.
Preguntas frecuentes
¿Por qué no funciona http://user:pass@host en Playwright?
Chromium ignora las credenciales de proxy en línea. Pon el usuario y la contraseña en los campos dedicados username/password del objeto proxy en su lugar. Esta es la razón más común de “el proxy no funciona” en Playwright.
¿Por qué se ignoran mis proxies por contexto?
Porque lanzaste Chromium sin un proxy. El proxy por contexto solo se activa si el navegador se lanzó con uno, pasa un placeholder proxy: { server: "per-context" } a launch(), luego pon proxies reales en cada contexto.
¿Debería rotar la IP en cada petición en un navegador?
No. Una sesión de navegador debería mantener una IP, como un usuario real. Usa un sid sticky por contexto y rota creando nuevos contextos, no cambiando IPs a mitad de sesión.
¿Cómo corro países distintos al mismo tiempo?
Un navegador, múltiples contextos, cada uno con un country-<cc> distinto en su nombre de usuario de proxy. Corren concurrente e independientemente.
¿Esto funciona con Puppeteer o Selenium también?
El gateway sí. Puppeteer toma --proxy-server en el lanzamiento y autentica vía page.authenticate(); Selenium usa una capability de proxy o una extensión para la autenticación. El targeting codificado en el nombre de usuario es idéntico; solo difiere el cableado. El modelo por contexto de Playwright es el más limpio para trabajo multi-identidad.
Mis corridas están quemando ancho de banda rápido. ¿Qué hago?
Bloquea imágenes, media, y fuentes con context.route() como se muestra arriba. Los navegadores descargan todo por defecto, y en un plan por GB eso es dinero real. Abortar los recursos pesados a menudo recorta el uso en más de la mitad.
Cerrando
El patrón: lanza Chromium con un placeholder per-context, dale a cada contexto un proxy cuyo username codifique country/sid/ttl, alinea locale/timezone/geolocation con el país, y aborta imágenes y fuentes para ahorrar ancho de banda. Rota haciendo nuevos contextos, no cambiando IPs. Los errores aparecen como fallos de navegación net::, y un retry que construye un contexto fresco te consigue una IP fresca.
Empieza desde los snippets de arriba, apúntalos al gateway residencial, y tienes automatización de navegador 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 los docs del gateway.