Warum Selenium proxy auth so frustrierend ist
Wer schon einmal versucht hat, einen authentifizierten Proxy in Standard-Selenium zu konfigurieren, kennt das Problem: ChromeOptions.add_argument('--proxy-server=http://user:pass@host:port') funktioniert nicht. Chrome ignoriert die Credentials im URL-Format und zeigt stattdessen einen Pop-up-Dialog, den Selenium nicht abfangen kann. Das Gleiche gilt für Firefox – die Proxy-Auth muss über einen anderen Weg injiziert werden.
Dieses Problem trifft Selenium residential proxies besonders hart, da Residential-Provider – inklusive ProxyHat – fast immer Benutzername und Passwort verlangen. Ohne funktionierende Selenium proxy auth ist der gesamte IP-Rotation-Ansatz nutzlos.
In diesem Guide zeige ich dir framework-idiomatische Lösungen: selenium-wire für Chrome, Profile-Generierung für Firefox, Stealth-Erweiterungen, ein rotierender Proxy-Pool und Container-basiertes Parallel-Scraping. Am Ende gibt es einen ehrlichen Vergleich mit Playwright.
Chrome + selenium-wire: Authentifizierte Proxies richtig konfigurieren
selenium-wire erweitert den WebDriver um einen lokalen MitM-Proxy, der die Authentifizierung übernimmt. Der Browser verbindet sich unauthentifiziert zum lokalen Proxy, und selenium-wire leitet den Traffic mit Credentials an den Upstream-Proxy weiter.
Installation und Grundkonfiguration
pip install selenium-wire seleniumfrom seleniumwire import webdriver
from selenium.webdriver.chrome.options import Options
opts = Options()
opts.add_argument('--headless=new')
opts.add_argument('--disable-blink-features=AutomationControlled')
proxy_config = {
'proxy': {
'http': 'http://user-country-US:PASSWORD@gate.proxyhat.com:8080',
'https': 'http://user-country-US:PASSWORD@gate.proxyhat.com:8080',
},
'verify_ssl': False,
}
driver = webdriver.Chrome(options=opts, seleniumwire_options=proxy_config)
# Prüfe die ausgehende IP
driver.get('https://httpbin.org/ip')
print(driver.find_element('tag name', 'body').text)
driver.quit()Der entscheidende Vorteil: Kein Chrome-Extension-Hack, kein Pop-up-Dialog. selenium-wire fängt den Traffic auf Port-Ebene ab und fügt den Proxy-Authorization-Header automatisch ein. Das funktioniert zuverlässig mit Selenium residential proxies, Mobile- und Datacenter-Proxies gleichermaßen.
Geo-Targeting und Sticky Sessions
Mit ProxyHat steuerst du Geo-Targeting und Session-Verhalten über den Benutzernamen:
# Deutschland, Berlin, Sticky Session für 10 Minuten
proxy_config = {
'proxy': {
'http': 'http://user-country-DE-city-berlin-session-abc123:PASSWORD@gate.proxyhat.com:8080',
'https': 'http://user-country-DE-city-berlin-session-abc123:PASSWORD@gate.proxyhat.com:8080',
},
}
# Neue IP pro Request: einfach einen neuen Session-String generieren
import uuid
session_id = uuid.uuid4().hex[:12]
proxy_url = f'http://user-country-DE-session-{session_id}:PASSWORD@gate.proxyhat.com:8080'Einschränkungen von selenium-wire
- Der lokale MitM-Proxy addiert ~50-100ms Latenz pro Request.
- SSL-Verifikation muss ggf. deaktiviert werden (
verify_ssl: False), was in Produktivumgebungen Sicherheitsbedenken aufwirft. - Bei sehr hohem Concurrency-Level (100+ parallele Sessions) kann der lokale Proxy zum Flaschenhals werden.
Firefox: Proxy-Profile programmatisch generieren
Firefox bietet einen saubereren Weg: Proxy-Einstellungen werden direkt in prefs.js eines Firefox-Profils geschrieben. Die Authentifizierung erledigen wir über eine signierte Extension, die den Proxy-Authorization-Header setzt.
Profil-Generierung und Auth-Extension
import tempfile
import os
import zipfile
from selenium import webdriver
from selenium.webdriver.firefox.options import Options as FirefoxOptions
def create_proxy_auth_extension(proxy_host, proxy_port, proxy_user, proxy_pass):
"""Erstellt eine signierte XPI-Extension für Firefox Proxy-Auth."""
manifest_json = '{"manifest_version": 2, "name": "proxy-auth", "version": "1.0", "background": {"scripts": ["background.js"]}, "permissions": ["webRequest", "webRequestBlocking", "<all_urls>"]}'
background_js = '''
var authHeader = "Basic " + btoa("%s:%s");
browser.webRequest.onBeforeSendHeaders.addListener(
function(details) {
details.requestHeaders.push({name: "Proxy-Authorization", value: authHeader});
return {requestHeaders: details.requestHeaders};
},
{urls: ["<all_urls>"]},
["blocking", "requestHeaders"]
);
''' % (proxy_user, proxy_pass)
pluginfile = tempfile.mktemp(suffix='.xpi')
with zipfile.ZipFile(pluginfile, 'w') as zp:
zp.writestr('manifest.json', manifest_json)
zp.writestr('background.js', background_js)
return pluginfile
def create_firefox_proxy_profile(proxy_host, proxy_port, proxy_user, proxy_pass):
"""Erstellt ein Firefox-Profil mit Proxy-Einstellungen."""
profile_dir = tempfile.mkdtemp()
prefs_path = os.path.join(profile_dir, 'user.js')
prefs = f'''
user_pref("network.proxy.type", 1);
user_pref("network.proxy.http", "{proxy_host}");
user_pref("network.proxy.http_port", {proxy_port});
user_pref("network.proxy.ssl", "{proxy_host}");
user_pref("network.proxy.ssl_port", {proxy_port});
user_pref("network.proxy.no_proxies_on", "localhost, 127.0.0.1");
'''
with open(prefs_path, 'w') as f:
f.write(prefs)
return profile_dir
# Nutzung
PROXY_HOST = 'gate.proxyhat.com'
PROXY_PORT = 8080
PROXY_USER = 'user-country-DE-city-berlin'
PROXY_PASS = 'PASSWORD'
profile_dir = create_firefox_proxy_profile(PROXY_HOST, PROXY_PORT, PROXY_USER, PROXY_PASS)
extension_path = create_proxy_auth_extension(PROXY_HOST, PROXY_PORT, PROXY_USER, PROXY_PASS)
opts = FirefoxOptions()
opts.add_argument('--headless')
opts.add_argument('-profile')
opts.add_argument(profile_dir)
driver = webdriver.Firefox(options=opts)
driver.install_addon(extension_path, temporary=True)
driver.get('https://httpbin.org/ip')
print(driver.find_element('tag name', 'body').text)
driver.quit()Dieser Ansatz ist framework-idiomatisch: Die Proxy-Konfiguration lebt im Profil, die Auth in einer Extension. Beides wird programmatisch erzeugt und kann pro Session variiert werden – ideal für rotierende Proxy-Pools.
Selenium stealth: Automation-Fingerprints reduzieren
Selbst mit dem richtigen Proxy erkennen viele Seiten, dass du Selenium nutzt. Die Indizien sind vielfältig: navigator.webdriver ist true, Chrome spezifische Automation-Flags sind gesetzt, window.chrome fehlt, und die Standard-Auflösung von Headless-Browsern ist 800x600.
selenium-stealth: Der Quick-Fix
undetected-chromedriver und selenium-stealth patchen die offensichtlichsten Fingerprints:
from selenium import webdriver
from selenium_stealth import stealth
opts = webdriver.ChromeOptions()
opts.add_argument('--headless=new')
opts.add_experimental_option('excludeSwitches', ['enable-automation'])
opts.add_experimental_option('useAutomationExtension', False)
driver = webdriver.Chrome(options=opts)
stealth(driver,
languages=['de-DE', 'de', 'en-US', 'en'],
vendor='Google Inc.',
platform='Win32',
webgl_vendor='Intel Inc.',
renderer='Intel Iris OpenGL Engine',
fix_hairline=True,
)
# navigator.webdriver ist jetzt false
print(driver.execute_script('return navigator.webdriver')) # None
driver.get('https://bot.sannysoft.com/')
driver.save_screenshot('stealth_check.png')
driver.quit()selenium-driverless: Der radikalere Ansatz
selenium-driverless kommuniziert direkt über die Chrome DevTools Protocol (CDP), ohne einen WebDriver-Prozess. Das eliminiert die größte Erkennungsquelle komplett:
from selenium_driverless import webdriver as driverless_webdriver
from selenium_driverless.types import Options
opts = Options()
opts.add_argument('--headless=new')
# Proxy über CDP-Flags setzen (kein selenium-wire nötig!)
opts.add_argument(f'--proxy-server=http://gate.proxyhat.com:8080')
async def main():
driver = await driverless_webdriver.Chrome(options=opts)
# Proxy-Auth über CDP Fetch domain
await driver.execute_cdp('Fetch.enable',
handleAuthRequests=True,
patterns=[{'urlPattern': '*'}]
)
await driver.execute_cdp('Fetch.continueWithAuth',
requestId='<request_id>',
authChallengeResponse={
'response': 'ProvideCredentials',
'username': 'user-country-US',
'password': 'PASSWORD'
}
)
await driver.get('https://httpbin.org/ip')
print(await driver.page_source)
await driver.quit()
import asyncio
asyncio.run(main())Praxistipp: Kombiniere Selenium residential proxies mit Stealth-Maßnahmen. Ein Residential-IP allein reicht nicht – moderne Anti-Bot-Systeme wie Cloudflare und Datadome prüfen sowohl die IP-Reputation als auch Browser-Fingerprints.
Rotierender Proxy-Pool: Jede Session bekommt eine frische IP
Der produktive Einsatz erfordert ein Muster, bei dem jede neue WebDriver-Instanz automatisch einen frischen Proxy zugewiesen bekommt. Das ist der Kern von Selenium proxy auth bei Scale.
Pool-Manager als Middleware
Anstatt Proxy-URLs hart zu coden, implementieren wir einen Proxy-Pool-Manager, der als zentrale Middleware fungiert:
import itertools
import uuid
from dataclasses import dataclass
from typing import Generator
@dataclass
class ProxyConfig:
host: str
port: int
username_template: str
password: str
def get_url(self, country: str = 'US', city: str = None, session: str = None) -> str:
"""Generiert eine Proxy-URL mit Geo-Targeting und Session-Kontrolle."""
user = self.username_template.format(
country=country,
city=city or '',
session=session or uuid.uuid4().hex[:12]
)
return f'http://{user}:{self.password}@{self.host}:{self.port}'
class ProxyPool:
"""Rotierender Proxy-Pool für Selenium WebDriver-Sessions."""
def __init__(self, config: ProxyConfig, countries: list[str] = None):
self.config = config
self.countries = countries or ['US', 'DE', 'GB', 'FR']
self._cycle = itertools.cycle(self.countries)
def next_proxy(self, country: str = None, city: str = None, sticky: bool = False) -> dict:
"""Liefert die nächste Proxy-Konfiguration für selenium-wire."""
target_country = country or next(self._cycle)
session_id = uuid.uuid4().hex[:12] if not sticky else 'sticky'
proxy_url = self.config.get_url(
country=target_country,
city=city,
session=session_id
)
return {
'proxy': {
'http': proxy_url,
'https': proxy_url,
},
'country': target_country,
'session': session_id,
}
# Nutzung mit ProxyHat
pool = ProxyPool(
ProxyConfig(
host='gate.proxyhat.com',
port=8080,
username_template='user-country-{country}-city-{city}-session-{session}',
password='PASSWORD'
),
countries=['US', 'DE', 'GB', 'JP']
)
# Jeder Aufruf liefert eine andere IP
for _ in range(5):
cfg = pool.next_proxy()
print(f"Country: {cfg['country']}, Session: {cfg['session']}")
# -> Übergib cfg['proxy'] an seleniumwire_optionsDieses Pattern trennt die Proxy-Logik von der WebDriver-Logik. Der Pool kann erweitert werden um Retry-Logik, IP-Blacklisting und Rate-Limiting – alles als saubere Middleware, nicht als Hack im Test-Code.
WebDriver-Factory mit integriertem Proxy-Pool
Die Factory kapselt die Browser-Erstellung und injiziert automatisch den nächsten Proxy:
from seleniumwire import webdriver as sw_webdriver
from selenium.webdriver.chrome.options import Options
class WebDriverFactory:
"""Erstellt WebDriver-Instanzen mit rotierenden Proxies und Stealth."""
def __init__(self, proxy_pool: ProxyPool, headless: bool = True):
self.pool = proxy_pool
self.headless = headless
def create_chrome(self, country: str = None, city: str = None) -> sw_webdriver.Chrome:
proxy_cfg = self.pool.next_proxy(country=country, city=city)
opts = Options()
if self.headless:
opts.add_argument('--headless=new')
opts.add_argument('--disable-blink-features=AutomationControlled')
opts.add_argument('--window-size=1920,1080')
opts.add_experimental_option('excludeSwitches', ['enable-automation'])
opts.add_experimental_option('useAutomationExtension', False)
driver = sw_webdriver.Chrome(
options=opts,
seleniumwire_options=proxy_cfg['proxy']
)
# Stealth-Patches anwenden
driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {
'source': '''
Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
Object.defineProperty(navigator, 'plugins', {get: () => [1, 2, 3, 4, 5]});
window.chrome = {runtime: {}};
'''
})
return driver, proxy_cfg
# Nutzung
factory = WebDriverFactory(pool)
driver, proxy_info = factory.create_chrome(country='DE', city='berlin')
driver.get('https://httpbin.org/ip')
print(f"IP via {proxy_info['country']}: {driver.find_element('tag name', 'body').text}")
driver.quit()Selenium Grid und Containerisierung: Paralleles Scraping at Scale
Einzelne WebDriver-Sessions skalieren nicht. Für ernsthaftes Scraping brauchst du parallele Ausführung. Selenium Grid bietet das Framework, Docker die Isolation.
Docker Compose für Selenium Grid v4
# docker-compose.yml
version: '3.8'
services:
selenium-hub:
image: selenium/hub:4.18.0
ports:
- '4442-4444:4442-4444'
environment:
- SE_SESSION_REQUEST_TIMEOUT=300
- SE_NODE_SESSION_TIMEOUT=300
chrome-node:
image: selenium/node-chrome:4.18.0
depends_on:
- selenium-hub
environment:
- SE_EVENT_BUS_HOST=selenium-hub
- SE_EVENT_BUS_PUBLISH_PORT=4442
- SE_EVENT_BUS_SUBSCRIBE_PORT=4443
- SE_NODE_MAX_SESSIONS=4
- SE_NODE_OVERRIDE_MAX_SESSIONS=true
deploy:
replicas: 4
shm_size: '2gb'Parallel-Scraper mit Grid und Proxy-Pool
from concurrent.futures import ThreadPoolExecutor, as_completed
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
GRID_URL = 'http://localhost:4444/wd/hub'
def scrape_with_proxy(proxy_url: str, target_url: str) -> dict:
"""Startet eine Remote-Session über Selenium Grid mit Proxy."""
opts = Options()
opts.add_argument('--headless=new')
opts.add_argument(f'--proxy-server={proxy_url.split("@")[1]}') # Nur host:port
# Auth muss über selenium-wire oder CDP gelöst werden
driver = webdriver.Remote(
command_executor=GRID_URL,
options=opts
)
try:
driver.get(target_url)
return {'status': 'ok', 'title': driver.title, 'url': target_url}
except Exception as e:
return {'status': 'error', 'url': target_url, 'error': str(e)}
finally:
driver.quit()
# 20 URLs parallel über 4 Chrome-Nodes (je 4 Sessions = 16 parallel)
urls = [f'https://example.com/page/{i}' for i in range(20)]
pool = ProxyPool(
ProxyConfig(
host='gate.proxyhat.com',
port=8080,
username_template='user-country-US-session-{session}',
password='PASSWORD'
)
)
with ThreadPoolExecutor(max_workers=16) as executor:
futures = {}
for url in urls:
cfg = pool.next_proxy()
# Beachte: Für Grid brauchst du selenium-wire auf Node-Ebene
# oder CDP-basierte Auth – siehe Abschnitt selenium-driverless
futures[executor.submit(scrape_with_proxy, cfg['proxy']['http'], url)] = url
for future in as_completed(futures):
result = future.result()
print(result)Hinweis zu Grid + Auth: Selenium Grid unterstützt standardmäßig keine Proxy-Authentifizierung. Du musst entweder
selenium-wireauf den Node-Images installieren (Custom-Dockerfile), die CDP-basierte Auth viaselenium-driverlessnutzen, oder eine lokale Proxy-Forwarder-Lösung wie tinyproxy +basic-authauf dem Node einsetzen.
Skalierungs-Strategien
- Horizontal skalieren: Mehrere Chrome-Nodes via
replicasin Docker Compose oder Kubernetes HPA. - Session-Pooling: WebDriver-Sessions wiederverwenden statt für jeden Request neu zu erstellen – reduziert Overhead von ~3-5s pro Session-Start.
- Headless Chrome Flags:
--disable-gpu,--disable-dev-shm-usage,--no-sandboxfür Container-Optimierung. - shm_size: Immer auf mindestens
2gbsetzen – Chrome crasht sonst bei mehreren Tabs.
Playwright vs. Selenium: Wann wechseln?
Playwright hat sich als ernsthafte Alternative etabliert. Die Entscheidung hängt von deinem spezifischen Kontext ab:
| Kriterium | Selenium | Playwright |
|---|---|---|
| Proxy-Auth (User:Pass) | Nativ nicht unterstützt – selenium-wire oder Extension nötig | Native Unterstützung via proxy: {username, password} |
| Stealth / Anti-Detection | Manuell: selenium-stealth, undetected-chromedriver, CDP-Hacks | Besserer Standard-Stealth, aber auch nicht unsichtbar |
| API-Design | Veraltet: explizite Waits, String-basierte Locators | Modern: Auto-Waiting, typed Locators, context-basiert |
| Browser-Support | Chrome, Firefox, Edge, Safari (via WebDriver) | Chromium, Firefox, WebKit (eigene Builds) |
| Ecosystem / Community | Massiv: 20+ Jahre, unzählige Tutorials, CI-Integrationen | Wachsend, aber kleiner – weniger Legacy-Ressourcen |
| Parallel Execution | Selenium Grid nötig (komplexe Infrastruktur) | Eingebaut: browser.contexts() – keine externe Infrastruktur |
| Legacy-Kompatibilität | Existierende Test-Suites, Selenese, BDD-Frameworks | Neue Projekte bevorzugt – Migration aufwändig |
| Mobile Testing | Appium-Integration verfügbar | Device-Emulation eingebaut, keine echte App-Unterstützung |
Wann Playwright die bessere Wahl ist
- Neue Projekte ohne Legacy-Code: Die modernere API spart erheblich Entwicklungszeit.
- Proxy-Auth ist zentral: Playwrights native Proxy-Auth-Unterstützung eliminiert den selenium-wire-Umweg.
- Parallele Ausführung ohne Grid: Playwright startet Browser-Contexte in Millisekunden – kein Grid-Setup nötig.
Wann du bei Selenium bleiben solltest
- Bestehende Test-Suites: Eine Migration von 500+ Tests lohnt sich selten nur für bessere Proxy-Auth.
- CI/CD-Integration: Viele CI-Systeme haben native Selenium-Plugins.
- Breite Browser-Abdeckung: Wenn du echten Safari via WebDriver testen musst, bleibt Selenium die einzige Option.
Praxistipp: Du musst dich nicht entscheiden. Viele Teams nutzen Playwright für neue Scraping-Projekte und behalten Selenium für bestehende QA-Suites. Die Proxy-Infrastruktur (ProxyHat) funktioniert mit beiden Frameworks identisch.
Key Takeaways
- Selenium proxy auth erfordert immer einen Workaround:
selenium-wirefür Chrome, Firefox-Profile + Auth-Extension für Firefox, oder CDP-basierte Auth viaselenium-driverless. - Selenium stealth ist ein Mehrschichten-Problem: IP-Reputation (Residential-Proxies), Browser-Fingerprints (selenium-stealth / undetected-chromedriver), und Verhaltens-Signale (Mouse-Movements, Timing) müssen alle adressiert werden.
- Ein rotierender Proxy-Pool als Middleware-Pattern trennt Proxy-Logik sauber von WebDriver-Code und ermöglicht Geo-Targeting, Sticky Sessions und Retry-Logik.
- Selenium Grid + Docker ermöglicht paralleles Scraping, erfordert aber Custom-Images für Proxy-Auth.
- Playwright ist für neue Projekte oft die bessere Wahl – aber Selenium bleibt relevant für Legacy-Suites und breite Browser-Kompatibilität.






