Base de connaissances

Comment utiliser des proxies résidentiels avec Python (requests, httpx, Scrapy)

Un guide pratique sur les proxies résidentiels en Python : authentification, IPs rotatives ou fixes, ciblage géographique, tentatives de reconnexion, avec des exemples pour requests, httpx et Scrapy.

Chris Collins

Chris Collins

21 juin 2026 · 10 min de lecture

Connecter un proxy résidentiel à un script Python est une affaire de cinq minutes une fois qu’on l’a vu faire. La difficulté ne vient jamais du code, mais des détails que personne ne documente : comment les identifiants encodent le ciblage, quand faire tourner les IPs plutôt que d’en fixer une, comment gérer les erreurs spécifiques aux proxies, et les petites différences entre requests, httpx et Scrapy.

Voici le guide que j’aurais aimé avoir. Des exemples prêts à copier-coller qui fonctionnent vraiment, pour les trois bibliothèques que vous utilisez le plus probablement, ainsi que les éléments qui transforment un snippet fonctionnel en un scraper capable de tenir en production.

Tout ce qui suit utilise la passerelle résidentielle Shifter residential gateway : un seul point d’entrée, p.shifter.io:443, avec tout le ciblage encodé dans le nom d’utilisateur. Si vous utilisez un autre fournisseur, la structure est identique ; changez simplement l’hôte et le format des identifiants.

La seule chose à comprendre avant tout

Sur une passerelle résidentielle, le nom d’utilisateur du proxy contient à la fois votre authentification et votre ciblage. Pour changer de pays ou de session, vous ne changez pas de point d’entrée, vous modifiez la chaîne du nom d’utilisateur. Un nom d’utilisateur ressemble à ceci :

customer-USERNAME-country-us-sid-abc123-ttl-600

Lisez-le de gauche à droite : identifiant de compte, puis les options. country-us cible les États-Unis. sid-abc123 fixe une session persistante. ttl-600 maintient cette IP pendant 600 secondes. Supprimez sid/ttl et chaque requête bascule vers une nouvelle IP. Le mot de passe est constant. C’est tout le modèle mental ; le reste consiste simplement à insérer cette chaîne dans le champ proxy de chaque bibliothèque.

Conservez les identifiants dans des variables d’environnement, jamais en dur dans le code :

import os
USER = os.environ["SHIFTER_USER"] # your account username
PASS = os.environ["SHIFTER_PASS"]
GATEWAY = "p.shifter.io:443"

requests : la version en 6 lignes

requests accepte un dictionnaire proxies. Les clés http et https pointent toutes deux vers la même URL de proxy http:// — c’est correct, requests tunnelise HTTPS à travers elle via 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': '<a US residential IP>'}

Exécutez-le deux fois et vous obtiendrez deux IPs différentes, car il n’y a pas de sid : chaque requête tourne. C’est le comportement par défaut, et pour la plupart des scrapings, c’est exactement ce que vous voulez.

Rotation vs session fixe, en code

C’est la distinction qui perturbe souvent, voici donc comment elle se traduit concrètement. (Pour la version conceptuelle, consultez sticky vs rotating residential proxies.)

Rotation (nouvelle IP à chaque requête) est le comportement par défaut, il suffit d’omettre sid :

for _ in range(3):
r = requests.get("https://api.ipify.org", proxies=proxy("us"), timeout=30)
print(r.text) # three different IPs

Session fixe (même IP sur plusieurs requêtes) nécessite un identifiant de session et un TTL dans le nom d’utilisateur. Utilisez-la lorsqu’un flux s’étend sur plusieurs requêtes qui doivent ressembler à un seul utilisateur : une connexion, puis les pages qui la suivent :

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)
# all three requests share one IP for up to 600s

Choisissez vous-même l’identifiant de session : n’importe quelle chaîne unique par session logique. Le même identifiant renvoie la même IP jusqu’à l’expiration du TTL, puis elle tourne tout en conservant vos autres options.

Ciblage géographique

Puisque le ciblage est dans le nom d’utilisateur, la géolocalisation n’est qu’une option supplémentaire. Le pays est la plus courante ; l’état, la ville et l’ASN fonctionnent de la même façon :

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")) # Germany
requests.get("https://example.com", proxies=geo_proxy("us", "new york")) # New York City

Les noms de villes utilisent des underscores à la place des espaces (new_york). Combinez les options librement ; l’ordre n’a pas d’importance pour la passerelle.

httpx : même principe, compatible async

httpx est le choix moderne lorsque vous souhaitez de l’async ou HTTP/2. Le proxy est défini sur le client. Notez que le paramètre est proxy= (singulier) dans les versions récentes de httpx ; les versions plus anciennes utilisaient 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}"
# Sync
with httpx.Client(proxy=proxy_url("us"), timeout=30) as client:
print(client.get("https://api.ipify.org").text)

La version async est là où httpx révèle tout son intérêt : envoyer de nombreuses requêtes en parallèle, chacune via une IP rotative fraîche :

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"] * 10
print(asyncio.run(main(urls))) # 10 concurrent requests, rotating IPs

Dix requêtes résidentielles concurrentes en une douzaine de lignes. Attention à la concurrence : plus n’est pas toujours mieux, et bombarder une cible depuis une rafale d’IPs peut tout de même déclencher une détection comportementale.

Scrapy : un middleware proxy

Scrapy est la solution de référence pour les crawls à grande échelle. La façon propre d’y attacher des proxies est de définir request.meta["proxy"] par requête ; le HttpProxyMiddleware intégré de Scrapy le lit et gère l’en-tête Proxy-Authorization à partir des identifiants dans l’URL.

Un petit middleware qui fait tourner la géolocalisation et attribue une IP fraîche à chaque requête :

middlewares.py
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}"
)

Activez-le dans settings.py (il doit s’exécuter avant le middleware proxy par défaut) :

settings.py
DOWNLOADER_MIDDLEWARES = {
"myproject.middlewares.ResidentialProxyMiddleware": 350,
"scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware": 400,
}

Désormais, chaque spider passe par des IPs résidentielles, et vous pouvez cibler par requête avec Request(url, meta={"country": "gb"}). Pour une session fixe, construisez le nom d’utilisateur avec un sid/ttl exactement comme dans l’exemple requests et définissez-le sur meta["proxy"].

Gérer les erreurs qui surviennent vraiment

Un scraper qui ignore les erreurs de proxy fonctionne en démo et plante du jour au lendemain. Voici les trois que vous rencontrerez :

  • 407 Proxy Authentication Required : nom d’utilisateur ou mot de passe incorrect, ou option non reconnue (une faute de frappe dans country ou asn). Corrigez la chaîne d’identifiants ; réessayer ne servira à rien.
  • 429 Too Many Requests : la cible vous limite en débit. Ralentissez et faites tourner les IPs (ce que la passerelle fait déjà par requête lorsque vous n’êtes pas en session fixe).
  • 502 Bad Gateway : aucune IP ne correspond actuellement à votre filtre (généralement un ciblage géographique trop restrictif comme pays+ville+ASN). Assouplissez une option et réessayez.

Un wrapper de tentatives minimal avec backoff qui abandonne proprement :

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)

Le backoff exponentiel combiné à la rotation par requête gère la grande majorité des échecs transitoires. Si vous êtes bloqué de façon systématique plutôt qu’intermittente, le problème ne vient pas des tentatives, mais de la qualité des IPs ou du comportement des requêtes — voir how to avoid getting blocked when scraping.

Vérifier que ça fonctionne vraiment

Avant de faire confiance à une configuration, confirmez deux choses : que l’IP change (rotation) et qu’elle est dans le bon pays (géolocalisation). Une vérification rapide :

import requests
r = requests.get("http://ip-api.com/json", proxies=proxy("de"), timeout=30)
data = r.json()
print(data["query"], data["countryCode"]) # expect a DE IP

Appelez-le plusieurs fois sans sid et vous devriez voir des IPs différentes, toutes DE. Si le pays est incorrect, vérifiez l’orthographe de votre option. Si l’IP ne change jamais, vous avez accidentellement laissé un sid dans le nom d’utilisateur.

Note sur SOCKS5

Tout ce qui précède utilise des proxies HTTP, ce qui est le bon choix par défaut pour le scraping web. Si votre cas d’usage nécessite SOCKS5 (trafic non-HTTP, ou un outil qui l’exige), la même passerelle le supporte : changez le schéma en socks5h:// et installez requests[socks] (ou utilisez l’extra SOCKS de httpx). Les compromis sont détaillés dans HTTP vs SOCKS5 proxies ; pour du scraping classique, restez sur HTTP.

FAQ

Ai-je besoin d’un point d’entrée différent pour chaque pays ? Non. Un seul point d’entrée, p.shifter.io:443, pour tout. Le pays, la ville et la session se modifient via la chaîne du nom d’utilisateur, pas via l’hôte. C’est le principe fondamental du modèle de passerelle.

Pourquoi les clés http et https sont-elles toutes deux définies sur une URL http:// dans requests ? Parce que requests envoie HTTPS à travers un proxy HTTP via un tunnel CONNECT. Le schéma de l’URL du proxy décrit comment vous parlez au proxy (HTTP), pas à la cible. C’est correct et standard ; ne définissez pas la clé https sur https://.

Comment faire tourner les IPs à chaque requête ? Omettez sid du nom d’utilisateur. Sans identifiant de session, la passerelle vous attribue automatiquement une nouvelle IP par requête. Vous n’avez pas à gérer une liste de proxies ; la rotation du pool est côté serveur.

Comment conserver la même IP sur un flux en plusieurs étapes ? Ajoutez sid-<votre-id>-ttl-<secondes> au nom d’utilisateur et réutilisez-le pour chaque requête du flux. Même identifiant, même IP, jusqu’à l’expiration du TTL.

Mes requêtes sont lentes. Est-ce la faute du proxy ? Généralement non. Les IPs résidentielles ajoutent un peu de latence par rapport à une connexion directe, mais la lenteur à grande échelle est plus souvent due à une concurrence trop élevée, à l’absence de réutilisation des connexions (utilisez une Session/Client), ou à une cible lente. Profilez avant d’accuser le proxy.

Est-ce que ça fonctionne aussi avec aiohttp, urllib3 ou pycurl ? Oui. Tout client qui accepte une URL proxy authentifiée http://user:pass@host:port fonctionne ; le ciblage encodé dans les identifiants est identique. requests, httpx et Scrapy sont simplement les trois plus courants.

En résumé

Le schéma est le même dans chaque bibliothèque : construisez l’URL http://USER-flags:PASS@p.shifter.io:443, insérez-la dans le champ proxy, omettez sid pour faire tourner les IPs ou ajoutez-le pour les fixer. La géolocalisation est une option, les erreurs se gèrent avec une petite boucle de tentatives, et la concurrence est celle que votre client gère déjà.

Partez des snippets ci-dessus, pointez-les vers la passerelle résidentielle, et vous aurez un code proxy prêt pour la production dans l’après-midi. Les offres et les tarifs au Go sont sur la page de tarification, et la référence complète des options se trouve dans la documentation de la passerelle.

Tags : python residential proxies requests httpx scrapy web scraping

Prêt à commencer ?

Essayez les proxies résidentiels de Shifter, 205M+ IPs, 195+ pays, à partir de $1.00/GB.

Commencer