Brancher un proxy résidentiel dans Playwright, c’est quelques lignes une fois que vous l’avez vu faire. La friction n’est jamais l’option de lancement, ce sont les détails que personne n’écrit : comment les identifiants encodent le ciblage, le piège de Chromium qui casse les proxys par contexte, comment la rotation se mappe sur les contextes de navigateur, et le fait que chaque image que le navigateur charge est de la bande passante que vous payez.
C’est le compagnon de comment utiliser des proxys résidentiels avec Python, pour le côté navigateur. Des exemples copier-coller qui tournent vraiment, plus les parties qui transforment un snippet fonctionnel en une automatisation qui survit à la production.
Tout ci-dessous utilise le gateway résidentiel de Shifter : un point de terminaison, p.shifter.io:443, avec tout le ciblage encodé dans le nom d’utilisateur. Si vous êtes chez un autre fournisseur la forme est la même ; changez l’hôte et le format d’identifiant. Les exemples sont en Node/JavaScript ; un snippet Playwright en Python figure vers la fin.
La seule chose à comprendre d’abord
Sur un gateway résidentiel, le nom d’utilisateur du proxy porte votre authentification et votre ciblage. Vous ne changez pas de point de terminaison pour changer de pays ou de session, vous changez la chaîne du nom d’utilisateur. Un nom d’utilisateur ressemble à ceci :
customer-USERNAME-country-us-sid-abc123-ttl-600Lisez-le de gauche à droite : id de compte, puis flags. country-us cible les États-Unis. sid-abc123 fixe une session sticky. ttl-600 maintient cette IP pendant 600 secondes. Enlevez sid/ttl et chaque nouvelle connexion tourne vers une nouvelle IP. Le mot de passe est constant. C’est tout le modèle mental, le reste consiste à brancher cela dans l’emplacement proxy de Playwright.
Gardez les identifiants dans des variables d’environnement, jamais en dur :
const USER = process.env.SHIFTER_USER; // le nom d'utilisateur de votre compteconst PASS = process.env.SHIFTER_PASS;const GATEWAY = "http://p.shifter.io:443";Une note propre à Playwright d’emblée : ne mettez pas le nom d’utilisateur et le mot de passe en ligne dans l’URL du proxy. Chromium ignore les identifiants de proxy en ligne, donc http://user:pass@host échoue silencieusement à s’authentifier. Playwright a des champs username et password dédiés sur l’objet proxy, utilisez ceux-là.
La version en 10 lignes
Pour un seul proxy sur tout le navigateur, mettez proxy sur 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`, // le ciblage vit ici password: PASS, }, }); const page = await browser.newPage(); await page.goto("https://api.ipify.org?format=json"); console.log(await page.textContent("body")); // {"ip":"<une IP résidentielle américaine>"} await browser.close();})();Le champ username est là où vit tout le modèle du gateway. ${USER}-country-us cible les États-Unis ; ajoutez sid/ttl pour une IP sticky, ajoutez city/asn pour restreindre davantage. Le mot de passe ne change jamais.
Le piège de Chromium : les proxys par contexte
La version à proxy unique convient pour une identité. Mais la vraie raison d’utiliser Playwright est de faire tourner plusieurs identités, et pour cela vous voulez un proxy différent par contexte de navigateur (chaque contexte est ses propres cookies/stockage isolés, c’est-à-dire son propre « utilisateur »).
Voici le piège : dans Chromium, les proxys par contexte ne fonctionnent que si vous lancez le navigateur avec un proxy déjà défini. Lancez sans proxy et chaque newContext({ proxy }) est ignoré silencieusement. Le correctif est un serveur placeholder au lancement :
const browser = await chromium.launch({ proxy: { server: "per-context" }, // placeholder, active le proxy par contexte});
// Maintenant chaque contexte peut porter son propre proxy + ciblage réelsconst 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 navigue sur une IP américaine, ctxDE sur une allemande, en même temps, dans un seul navigateur. C’est le motif sur lequel bâtir. (Firefox et WebKit n’ont pas strictement besoin du placeholder, mais le mettre est inoffensif et garde votre code cross-browser.)
Rotatif vs sticky, à la manière de Playwright
Dans un scraper HTTP brut, vous tournez souvent à chaque requête. Dans un navigateur, c’est généralement une erreur : un vrai utilisateur ne change pas d’IP en milieu de session, donc une IP qui saute entre les chargements de page pendant que les cookies restent constants est un signal de détection. Le mapping idiomatique est :
- Un contexte = une identité sticky. Donnez à chaque contexte un
sidunique et unttlqui couvre la session, ainsi tous ses chargements de page partagent une IP, comme un vrai utilisateur. - Tournez en créant de nouveaux contextes. Un contexte frais avec un nouveau
sid(ou sanssid) obtient une nouvelle IP. Fermez l’ancien quand vous avez terminé.
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, }, });}
// Traiter 50 cibles, chacune avec sa propre identité à IP stablefor (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); // ... scraper ... await ctx.close(); // terminé avec cette identité}Choisissez le sid vous-même, n’importe quelle chaîne unique par session logique. La même chaîne renvoie la même IP jusqu’à l’expiration du TTL. Faites tourner les contextes en parallèle (un petit pool) plutôt que 50 d’un coup ; plus de navigateurs n’est pas plus de vitesse, et une rafale d’IP martelant une cible déclenche toujours la détection comportementale. Si vous êtes bloqué de façon constante plutôt qu’intermittente, le problème est la qualité d’IP ou le comportement, pas la rotation, voir comment éviter de se faire bloquer en scrapant.
Le geo-targeting, et accorder le navigateur à l’IP
Parce que le ciblage vit dans le nom d’utilisateur, la géo n’est qu’un flag : country-de, ajoutez city-berlin ou asn-3320 pour restreindre. Mais avec un vrai navigateur il y a une seconde moitié que l’on manque : l’empreinte de votre navigateur doit s’accorder avec votre IP. Une IP résidentielle allemande couplée à un locale en-US et un fuseau horaire de New York est une contradiction que les systèmes anti-bot repèrent au premier coup d’œil. (Voir pourquoi les scrapers se font bloquer.)
Playwright vous laisse tout aligner sur le contexte :
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"],});Les noms de ville utilisent le format du gateway (minuscules) ; combinez les flags librement, l’ordre est indifférent au gateway. Accorder locale/timezoneId/geolocation au pays du proxy est l’une des actions à plus fort levier pour ressembler à un vrai utilisateur local.
Réduisez votre bande passante (vous payez au Go)
Celui-ci est propre aux navigateurs et il compte, car les proxys résidentiels sont facturés au gigaoctet. Un navigateur headless, laissé à lui-même, télécharge chaque image, police, vidéo, et script de tracking de la page, dont la plupart ne vous servent pas au scraping. Les abandonner peut réduire la bande passante (et le coût) de bien plus de la moitié, et accélère vos exécutions.
Utilisez context.route() pour bloquer les types de ressources lourds avant qu’ils n’atteignent le proxy :
await ctx.route("**/*", (route) => { const type = route.request().resourceType(); if (["image", "media", "font"].includes(type)) { return route.abort(); // jamais téléchargé, jamais facturé } return route.continue();});Gardez document, script, xhr, et fetch (les données réelles et le JS qui les rend) ; jetez le reste. Si le site charge du contenu en lazy-load derrière des images, testez que votre scrape fonctionne encore, mais pour la plupart des travaux c’est de l’argent gratuit. Cela s’accorde bien avec le modèle au Go couvert dans proxys résidentiels facturés à la bande passante.
Vérifier que ça marche vraiment
Avant de faire confiance à une configuration, confirmez deux choses : l’IP est dans le bon pays (géo) et elle change entre les contextes (rotation). Une vérification rapide :
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); // attendez une IP DELancez deux contextes avec des sid différents et vous devriez voir deux IP différentes, toutes deux du bon pays. Si le pays est faux, vérifiez l’orthographe de vos flags. Si l’IP ne change jamais entre les contextes, vous avez probablement oublié le placeholder au lancement et Chromium ignore le proxy par contexte.
Les erreurs qui arrivent vraiment
Playwright fait remonter les problèmes de proxy comme des erreurs de navigation, pas comme des codes de statut HTTP. Les trois que vous rencontrerez :
net::ERR_TUNNEL_CONNECTION_FAILEDéchec de l’authentification du proxy, ou un flag non reconnu (une faute de frappe danscountryouasn, ou des identifiants en ligne que Chromium a ignorés). Corrigez les champs username/password ; réessayer n’aidera pas.net::ERR_PROXY_CONNECTION_FAILEDle gateway était injoignable, ou aucune IP ne correspondait à un filtre trop serré (comme country + city + asn). Vérifiez la connectivité, puis desserrez un flag.- Une page qui charge mais affiche un blocage/CAPTCHA la cible, pas le proxy, vous refuse. Tournez vers un nouveau contexte/IP, ralentissez, et alignez l’empreinte comme ci-dessus.
Enveloppez la navigation dans un petit retry qui crée un contexte frais en cas d’échec :
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 }; // l'appelant ferme ctx à la fin } catch (e) { await ctx.close(); if (attempt === tries - 1) throw e; await new Promise((r) => setTimeout(r, 2 ** attempt * 1000)); // 1s, 2s } }}Un nouveau contexte signifie une nouvelle IP, donc un réessai obtient vraiment une route différente, pas la même qui échoue.
Une note sur SOCKS5
Le gateway parle SOCKS5, mais il y a une limitation de Playwright bonne à connaître : Playwright/Chromium ne prend pas en charge les proxys SOCKS authentifiés. Comme le gateway authentifie par nom d’utilisateur (c’est là que vit votre ciblage), SOCKS5 n’est pas pratique ici, utilisez le proxy HTTP montré ci-dessus, qui est de toute façon le bon défaut pour le scraping navigateur. Les compromis de SOCKS sont dans proxys SOCKS5 pour l’automatisation.
La même chose en Python
L’API Python de Playwright reflète celle en JavaScript ; l’objet proxy est identique :
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 même règle de placeholder-au-lancement, le même ciblage encodé dans le nom d’utilisateur. Pour le travail HTTP hors navigateur, le guide Python couvre requests, httpx, et Scrapy.
FAQ
Pourquoi http://user:pass@host ne marche-t-il pas dans Playwright ?
Chromium ignore les identifiants de proxy en ligne. Mettez plutôt le nom d’utilisateur et le mot de passe dans les champs dédiés username/password de l’objet proxy. C’est la raison la plus courante du « le proxy ne marche pas » dans Playwright.
Pourquoi mes proxys par contexte sont-ils ignorés ?
Parce que vous avez lancé Chromium sans proxy. Le proxy par contexte ne s’active que si le navigateur a été lancé avec un, passez un placeholder proxy: { server: "per-context" } à launch(), puis mettez des proxys réels sur chaque contexte.
Dois-je faire tourner l’IP à chaque requête dans un navigateur ?
Non. Une session de navigateur devrait garder une IP, comme un vrai utilisateur. Utilisez un sid sticky par contexte et tournez en créant de nouveaux contextes, pas en changeant d’IP en milieu de session.
Comment faire tourner des pays différents en même temps ?
Un navigateur, plusieurs contextes, chacun avec un country-<cc> différent dans son nom d’utilisateur de proxy. Ils tournent en parallèle et indépendamment.
Est-ce que ça marche avec Puppeteer ou Selenium aussi ?
Le gateway, oui. Puppeteer prend --proxy-server au lancement et s’authentifie via page.authenticate() ; Selenium utilise une capability de proxy ou une extension pour l’auth. Le ciblage encodé dans le nom d’utilisateur est identique ; seul le câblage diffère. Le modèle par contexte de Playwright est le plus propre pour le travail multi-identités.
Mes exécutions brûlent la bande passante vite. Que faire ?
Bloquez les images, médias, et polices avec context.route() comme montré ci-dessus. Les navigateurs téléchargent tout par défaut, et sur un forfait au Go c’est de l’argent réel. Abandonner les ressources lourdes réduit souvent l’usage de plus de la moitié.
Pour conclure
Le motif : lancez Chromium avec un placeholder per-context, donnez à chaque contexte un proxy dont le username encode country/sid/ttl, alignez locale/timezone/geolocation sur le pays, et abandonnez images et polices pour économiser la bande passante. Tournez en créant de nouveaux contextes, pas en changeant d’IP. Les erreurs apparaissent comme des échecs de navigation net::, et un retry qui construit un contexte frais vous obtient une IP fraîche.
Partez des snippets ci-dessus, pointez-les vers le gateway résidentiel, et vous avez une automatisation de navigateur en forme de production en un après-midi. Les forfaits et tarifs au Go sont sur la page tarifs, et la référence complète des flags vit dans les docs du gateway.