Einen Residential-Proxy in ein Python-Skript zu verdrahten ist ein Fünf-Minuten-Job, sobald du es einmal gesehen hast. Die Reibung ist nie der Code, sondern die Details, die niemand aufschreibt: wie die Credentials das Targeting kodieren, wann man rotiert statt eine IP zu fixieren, wie man die proxy-spezifischen Fehler behandelt und die kleinen Library-Unterschiede zwischen requests, httpx und Scrapy.
Das ist der Leitfaden, den ich gern gehabt hätte. Copy-paste-Beispiele, die tatsächlich laufen, für die drei Libraries, die du am wahrscheinlichsten nutzt, plus die Teile, die aus einem funktionierenden Snippet einen Scraper machen, der die Produktion überlebt.
Alles unten nutzt das Shifter-Residential-Gateway: ein Endpoint, p.shifter.io:443, mit allem Targeting im Benutzernamen kodiert. Wenn du bei einem anderen Anbieter bist, ist die Form dieselbe; tausche Host und Credential-Format.
Das Eine, das du zuerst verstehen musst
Auf einem Residential-Gateway trägt der Proxy-Benutzername deine Auth und dein Targeting. Du wechselst nicht den Endpoint, um Land oder Session zu ändern, du änderst den Benutzernamen-String. Ein Benutzername sieht so aus:
customer-USERNAME-country-us-sid-abc123-ttl-600Lies ihn von links nach rechts: Account-ID, dann Flags. country-us zielt auf die USA. sid-abc123 fixiert eine Sticky-Session. ttl-600 hält diese IP 600 Sekunden. Lass sid/ttl weg, und jeder Request rotiert zu einer neuen IP. Das Passwort ist konstant. Das ist das ganze mentale Modell, der Rest ist nur, diesen String in den Proxy-Slot jeder Library zu stecken.
Halte Credentials in Umgebungsvariablen, nie hardcoded:
import osUSER = os.environ["SHIFTER_USER"] # dein Account-BenutzernamePASS = os.environ["SHIFTER_PASS"]GATEWAY = "p.shifter.io:443"requests: die 6-Zeilen-Version
requests nimmt ein proxies-Dict. Beide Keys http und https zeigen auf dieselbe http://-Proxy-URL, das ist korrekt, requests tunnelt HTTPS via CONNECT durch sie hindurch.
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': '<eine US-Residential-IP>'}Lass es zweimal laufen, und du bekommst zwei verschiedene IPs, weil es kein sid gibt, jeder Request rotiert. Das ist der Default, und für die meisten Scrapes genau das, was du willst.
Rotierend vs Sticky, im Code
Das ist die Unterscheidung, über die Leute stolpern, also hier konkret. (Für die konzeptionelle Version siehe Sticky vs rotierende Residential-Proxies.)
Rotierend (neue IP bei jedem Request) ist der Default, lass einfach sid weg:
for _ in range(3): r = requests.get("https://api.ipify.org", proxies=proxy("us"), timeout=30) print(r.text) # drei verschiedene IPsSticky (dieselbe IP über mehrere Requests) braucht eine Session-ID und ein TTL im Benutzernamen. Nutze es, wenn ein Flow mehrere Requests umfasst, die wie ein Nutzer aussehen müssen, ein Login, dann die Seiten dahinter:
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) # alle drei Requests teilen sich bis zu 600s eine IPWähle die Session-ID selbst, irgendein eindeutiger String pro logischer Session. Derselbe String liefert dieselbe IP, bis das TTL abläuft, dann rotiert sie unter Beibehaltung deiner anderen Flags.
Geo-Targeting
Weil das Targeting im Benutzernamen lebt, ist Geo nur ein weiterer Flag. Land ist der häufige; Bundesland, Stadt und ASN funktionieren genauso:
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")) # Deutschlandrequests.get("https://example.com", proxies=geo_proxy("us", "new york")) # New York CityStädtenamen nutzen Unterstriche für Leerzeichen (new_york). Kombiniere Flags frei; die Reihenfolge ist dem Gateway egal.
httpx: dieselbe Idee, async-fähig
httpx ist die moderne Wahl, wenn du async oder HTTP/2 willst. Der Proxy geht an den Client. Beachte, dass der Parameter im aktuellen httpx proxy= (Singular) ist; ältere Versionen nutzten 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}"
# Synchronwith httpx.Client(proxy=proxy_url("us"), timeout=30) as client: print(client.get("https://api.ipify.org").text)Die async-Version ist da, wo httpx sich lohnt, fächere viele Requests gleichzeitig auf, jeder durch eine frische rotierende IP:
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 gleichzeitige Requests, rotierende IPsDas sind zehn gleichzeitige Residential-Requests in einem Dutzend Zeilen. Achte auf deine Concurrency, mehr ist nicht immer schneller, und ein Ziel aus einem Schwall von IPs zu hämmern kann trotzdem die Verhaltenserkennung auslösen.
Scrapy: ein Proxy-Middleware
Scrapy ist das Schwergewicht für große Crawls. Der saubere Weg, Proxies anzuhängen, ist request.meta["proxy"] pro Request zu setzen, Scrapys eingebautes HttpProxyMiddleware liest es und behandelt den Proxy-Authorization-Header aus den Credentials in der URL.
Ein winziges Middleware, das Geo rotiert und jedem Request eine frische IP gibt:
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}" )Aktiviere es in settings.py (es muss vor dem Default-Proxy-Middleware laufen):
DOWNLOADER_MIDDLEWARES = { "myproject.middlewares.ResidentialProxyMiddleware": 350, "scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware": 400,}Jetzt leitet jeder Spider über Residential-IPs, und du kannst pro Request mit Request(url, meta={"country": "gb"}) targeten. Für eine Sticky-Session baue den Benutzernamen mit einem sid/ttl genau wie im requests-Beispiel und setze ihn auf meta["proxy"].
Die Fehler behandeln, die wirklich auftreten
Ein Scraper, der Proxy-Fehler ignoriert, funktioniert in der Demo und stirbt über Nacht. Drei, denen du begegnest:
- 407 Proxy Authentication Required, falscher Benutzername/falsches Passwort, oder ein nicht erkannter Flag (ein Tippfehler in
countryoderasn). Korrigiere den Credential-String; ein Retry hilft nicht. - 429 Too Many Requests, das Ziel drosselt dich. Mach Backoff, werde langsamer, und rotiere IPs (was das Gateway pro Request schon tut, wenn du nicht sticky bist).
- 502 Bad Gateway, gerade passt keine IP zu deinem Filter (meist zu enges Geo wie country+city+asn). Lockere einen Flag und versuche es erneut.
Ein minimaler Retry-Wrapper, der Backoff macht und sauber aufgibt:
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)Exponentielles Backoff plus Rotation pro Request behandelt die große Mehrheit transienter Fehler. Wenn du konsistent statt sporadisch geblockt wirst, ist das Problem nicht der Retry, sondern die IP-Qualität oder das Request-Verhalten, behandelt in wie man beim Scrapen nicht geblockt wird.
Verifizieren, dass es tatsächlich funktioniert
Bevor du einer Konfiguration traust, bestätige zwei Dinge: dass die IP wechselt (Rotation) und dass sie im richtigen Land ist (Geo). Ein schneller Check:
import requests
r = requests.get("http://ip-api.com/json", proxies=proxy("de"), timeout=30)data = r.json()print(data["query"], data["countryCode"]) # erwartet eine DE-IPRuf es ein paar Mal ohne sid auf, und du solltest verschiedene IPs sehen, alle DE. Ist das Land falsch, prüfe die Schreibweise deines Flags. Wechselt die IP nie, hast du versehentlich ein sid im Benutzernamen gelassen.
Eine Anmerkung zu SOCKS5
Alles oben nutzt HTTP-Proxies, der richtige Default fürs Web-Scraping. Wenn deine Last SOCKS5 braucht (Nicht-HTTP-Traffic oder ein Tool, das es erwartet), spricht dasselbe Gateway es, ändere das Schema zu socks5h:// und installiere requests[socks] (oder nutze httpx’ SOCKS-Extra). Die Tradeoffs stehen in HTTP vs SOCKS5 Proxies; fürs reine Scrapen bleib bei HTTP.
FAQ
Brauche ich einen anderen Endpoint für jedes Land?
Nein. Ein Endpoint, p.shifter.io:443, für alles. Land, Stadt und Session ändern sich alle über den Benutzernamen-String, nicht über den Host. Das ist der Kern des Gateway-Modells.
Warum sind in requests beide Keys http und https auf eine http://-URL gesetzt?
Weil requests HTTPS über einen HTTP-Proxy via CONNECT-Tunnel sendet. Das Schema der Proxy-URL beschreibt, wie du mit dem Proxy sprichst (HTTP), nicht mit dem Ziel. Das ist korrekt und Standard, setze den https-Key nicht auf https://.
Wie rotiere ich IPs bei jedem Request?
Lass sid aus dem Benutzernamen weg. Ohne Session-ID gibt dir das Gateway automatisch eine frische IP pro Request. Du verwaltest keine Proxy-Liste, die Pool-Rotation ist serverseitig.
Wie halte ich dieselbe IP über einen Multi-Step-Flow?
Füge sid-<deine-id>-ttl-<sekunden> zum Benutzernamen hinzu und nutze es für jeden Request im Flow erneut. Gleiche ID, gleiche IP, bis das TTL abläuft.
Meine Requests sind langsam. Ist der Proxy schuld?
Meist nicht. Residential-IPs fügen gegenüber einer Direktverbindung etwas Latenz hinzu, aber Langsamkeit bei Skalierung ist öfter zu hohe Concurrency, keine Connection-Wiederverwendung (nutze ein Session/Client) oder ein langsames Ziel. Profile, bevor du dem Proxy die Schuld gibst.
Funktioniert das auch mit aiohttp, urllib3 oder pycurl?
Ja. Jeder Client, der eine authentifizierte http://user:pass@host:port-Proxy-URL akzeptiert, funktioniert, das credential-kodierte Targeting ist identisch. requests, httpx und Scrapy sind nur die drei häufigsten.
Zum Abschluss
Das Muster ist in jeder Library dasselbe: baue die http://USER-flags:PASS@p.shifter.io:443-URL, steck sie in den Proxy-Slot, lass sid weg zum Rotieren oder füge es hinzu zum Fixieren. Geo ist ein Flag, Fehler sind eine kleine Retry-Schleife, und Concurrency ist, was dein Client ohnehin schon macht.
Starte mit den Snippets oben, richte sie aufs Residential-Gateway, und du hast produktionsförmigen Proxy-Code an einem Nachmittag. Pläne und Preise pro GB stehen auf der Pricing-Seite, und die vollständige Flag-Referenz lebt in der Gateway-Dokumentation.