Google Rank Tracker in Python mit Residential Proxies bauen – Praxis-Leitfaden 2026

Schritt-für-Schritt-Anleitung zum Bau eines produktionsreifen Google-Rank-Trackers in Python: SERP-Paginierung, TLS-Fingerprinting, Residential Proxies, SQLite-Historie und Production-Hardening inklusive.

Build a Google Rank Tracker in Python with Residential Proxies

Einen Google Rank Tracker in Python mit Residential Proxies zu bauen, ist eine der nütztesten Automatisierungen für SEO-Teams: Man erhält tägliche, reproduzierbare Ranking-Snapshots statt einmaliger Stichproben. Seit Google den Parameter num=100 im September 2025 abgeschafft hat, sind zudem Paginierung und Proxy-Rotation zwingend erforderlich. Dieser Leitfaden zeigt, wie man das mit curl_cffi, dem ProxyHat-SDK, SQLite und robustem Production-Hardening umsetzt.

Warum tägliche SERP-Snapshots Einzelchecks überlegen sind

Ein einmaliger Ranking-Check liefert eine Momentaufnahme, die von Tageszeit, Datenzentrum, Personalisierung und Testgerät abhängt. Rankings schwanken jedoch täglich — ein Keyword kann morgens auf Position 4 und abends auf Position 11 stehen. Nur eine kontinuierliche Historie macht Volatilität sichtbar und erlaubt Trend-Analysen.

Die minimale Datenmodellierung für einen Rank-Tracker umfasst:

  • keyword — der Suchbegriff, z. B. „python web scraping“
  • target_domain — die Domain, deren Position getrackt wird
  • country — Geo-Target, z. B. US, DE, GB
  • devicedesktop oder mobile
  • position — organische Position (1–100)
  • captured_at — UTC-Timestamp der Erfassung

Mit diesen Feldern lassen sich Tagesdeltas, 7-Tage-Durchschnitte und Best-/Worst-Positionen pro Keyword berechnen. Eine SQLite-Tabelle reicht für bis zu ~50.000 Keyword-Tag-Kombinationen problemlos; für größere Volumina empfiehlt sich PostgreSQL.

Technischer Kontext: Warum Proxies heute unverzichtbar sind

Google betreibt mehrere Schichten von Anti-Bot-Schutz. Neben der klassischen IP-Rate-Limitierung setzt Google seit Jahren auf TLS/JA3-JA4-Fingerprinting und IP-Reputation-Scoring. Ein plain requests.get() mit einem Datacenter-IP-Bereich wird typischerweise nach 20–50 Requests pro IP pro Stunde eine CAPTCHA-Antwort oder HTTP 429 zurückgeben.

Residential Proxies lösen dieses Problem, weil sie IPs aus realen ISP-Bereichen verwenden, die eine hohe Reputation genießen. Mit City-Level-Geo-Targeting kann man zudem sicherstellen, dass der SERP-Snapshot genau dem entspricht, den ein Nutzer in einer bestimmten Stadt sieht — wichtig für Local-SEO und ländergenaue Rankings.

Die Kombination aus curl_cffi (das den TLS-Fingerprint eines echten Chrome-Browsers nachahmt) und Residential Proxies ist der aktuelle State-of-the-Art für SERP-Scraping. Laut der TLS 1.3-Spezifikation (RFC 8446) enthält der ClientHello eindeutige Fingerprints, die Anti-Bot-Systeme wie Cloudflare und Google auswerten. curl_cffi repliziert diese Fingerprints mit der Option impersonate='chrome'.

SERP-Paginierung: Top 100 ohne num=100

Google hat den Parameter num=100 im September 2025 entfernt. Um die Top 100 zu erfassen, muss man nun mit start=0, start=10, start=20 … paginieren. Jede Seite liefert ~10 organische Ergebnisse. Für die Top 100 braucht man also 10 Requests pro Keyword.

Die Basis-URL für eine Google-Suche sieht so aus:

https://www.google.com/search?q=KEYWORD&gl=us&hl=en&num=10&start=0

Wichtige Parameter:

  • q — URL-kodiertes Keyword
  • gl — Ländercode (z. B. us, de, gb)
  • hl — Sprache (z. B. en, de)
  • start — Offset für Paginierung (0, 10, 20, …)

Vergleich: Proxy-Typen für SERP-Scraping

Eigenschaft Datacenter Residential Mobile
IP-Reputation Niedrig — schnell blockiert Hoch — ISP-IPs Sehr hoch — Carrier-IPs
Block-Risiko bei Google Hoch nach ~30 Requests Niedrig bei Rotation Sehr niedrig
Latenz ~50ms ~200–500ms ~400–800ms
Preis pro GB $0,50–$1 $2–$5 $5–$12
Geo-Targeting Land Land + Stadt Land + Stadt + Carrier

Für Rank-Tracking sind Residential Proxies der Sweet Spot: ausreichende Reputation, City-Level-Geo-Targeting und moderater Preis. Mobile Proxies sind überdimensioniert, es sei denn, man trackt mobile-spezifische SERPs intensiv.

ProxyHat einrichten: Gateway, Geo-Targeting und Sessions

ProxyHat verwendet ein zentrales Gateway unter gate.proxyhat.com. Die Authentifizierung erfolgt über Username und Password, wobei Geo-Targeting und Session-Stickiness im Username kodiert werden.

Formate:

# HTTP — US, Chicago, sticky session
http://user-country-US-city-chicago-session-kw123:pass@gate.proxyhat.com:8080

# SOCKS5 — Deutschland, Berlin
socks5://user-country-DE-city-berlin-session-kw456:pass@gate.proxyhat.com:1080

Die -session--Flag sorgt dafür, dass alle Requests mit derselben Session-ID dieselbe Exit-IP verwenden. Für Rank-Tracking ist das wichtig: Pro Keyword sollte eine konsistente IP verwendet werden, um personalisierungsbedingte Schwankungen zu minimieren.

Weitere Details zur Proxy-Konfiguration finden Sie in der ProxyHat-Dokumentation.

Code-Beispiel 1: SERP mit curl abrufen

#!/bin/bash
# Google SERP abrufen über ProxyHat Residential Proxy
# Keyword: "python web scraping", Land: US, Stadt: Chicago

KEYWORD="python web scraping"
ENCODED=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$KEYWORD'))")

for START in 0 10 20 30 40; do
  curl -s --proxy "http://user-country-US-city-chicago-session-kw1:pass@gate.proxyhat.com:8080" \
    --impersonate chrome \
    "https://www.google.com/search?q=${ENCODED}&gl=us&hl=en&start=${START}" \
    -o "serp_${START}.html"
  echo "Saved serp_${START}.html"
  sleep 2
done

Code-Beispiel 2: Python Rank-Tracker mit curl_cffi und Raw-Proxy

import re
import csv
import time
import sqlite3
from datetime import datetime, timezone
from urllib.parse import quote
from curl_cffi import requests

# --- Konfiguration ---
PROXY_USER = "user-country-US-city-chicago-session-{sid}:pass"
PROXY_GATE = "gate.proxyhat.com:8080"
TARGET_DOMAIN = "example.com"
KEYWORDS = ["python web scraping", "best web scraping tools", "scrapy tutorial"]
COUNTRY = "us"
LANG = "en"
MAX_RESULTS = 100
PAGE_SIZE = 10

# --- SQLite-Historie ---
conn = sqlite3.connect("rank_history.db")
conn.execute("""
    CREATE TABLE IF NOT EXISTS rankings (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        keyword TEXT NOT NULL,
        target_domain TEXT NOT NULL,
        country TEXT NOT NULL,
        device TEXT NOT NULL,
        position INTEGER,
        captured_at TEXT NOT NULL
    )
""")

def fetch_serp_page(keyword: str, start: int, session_id: str) -> str:
    """Eine SERP-Seite über ProxyHat abrufen."""
    proxy_url = f"http://{PROXY_USER.format(sid=session_id)}@{PROXY_GATE}"
    encoded_kw = quote(keyword)
    url = f"https://www.google.com/search?q={encoded_kw}&gl={COUNTRY}&hl={LANG}&start={start}"

    resp = requests.get(
        url,
        proxies={"http": proxy_url, "https": proxy_url},
        impersonate="chrome",
        timeout=30,
    )
    if resp.status_code != 200:
        raise RuntimeError(f"HTTP {resp.status_code} für keyword={keyword} start={start}")
    return resp.text

def parse_organic_results(html: str) -> list[dict]:
    """Organische Ergebnisse aus HTML extrahieren — ohne Ads und SERP-Features."""
    results = []
    # Google nutzt div.g für organische Ergebnisse
    pattern = r'<div class="g"[^>]*>.*?<a href="/url\?q=([^"&]+)|<a href="(https?://[^"\s]+)"[^>]*data-ved='
    # Robuster: regex für href in organischen Links
    href_pattern = r'<a href="(https?://(?!www\.google\.|maps\.google\.|support\.google\.)[^"]+)"'
    matches = re.findall(href_pattern, html)
    for i, url in enumerate(matches[:MAX_RESULTS]):
        results.append({"position": i + 1, "url": url})
    return results

def find_position(results: list[dict], target_domain: str) -> int | None:
    """Position der Ziel-Domain finden."""
    for r in results:
        if target_domain in r["url"]:
            return r["position"]
    return None

def track_keyword(keyword: str):
    """Keyword tracken: Top 100 abrufen, Position finden, speichern."""
    session_id = f"kw{abs(hash(keyword)) % 100000}"
    all_results = []

    for start in range(0, MAX_RESULTS, PAGE_SIZE):
        try:
            html = fetch_serp_page(keyword, start, session_id)
            # CAPTCHA-Erkennung
            if "unusual traffic" in html.lower() or "captcha" in html.lower():
                print(f"[WARN] CAPTCHA erkannt für '{keyword}' bei start={start}")
                break
            page_results = parse_organic_results(html)
            # Positionen korrigieren für Paginierung
            for r in page_results:
                r["position"] = start + r["position"]
            all_results.extend(page_results)
            time.sleep(2)  # Rate-Limiting
        except Exception as e:
            print(f"[ERROR] {keyword} start={start}: {e}")
            break

    position = find_position(all_results, TARGET_DOMAIN)
    now = datetime.now(timezone.utc).isoformat()
    conn.execute(
        "INSERT INTO rankings VALUES (NULL, ?, ?, ?, ?, ?, ?)",
        (keyword, TARGET_DOMAIN, COUNTRY, "desktop", position, now),
    )
    conn.commit()
    print(f"[OK] {keyword} → Position {position}")

# --- Main ---
for kw in KEYWORDS:
    track_keyword(kw)
    time.sleep(3)

conn.close()
print("Tracking complete.")

Code-Beispiel 3: ProxyHat-SDK mit Session-Rotation

import hashlib
from curl_cffi import requests

class ProxyHatSession:
    """ProxyHat-Proxy-Manager mit per-Keyword sticky sessions."""

    def __init__(self, username: str, password: str, country: str, city: str | None = None):
        self.username = username
        self.password = password
        self.country = country.upper()
        self.city = city.lower() if city else None

    def get_proxy_url(self, session_id: str) -> str:
        """Proxy-URL mit Geo-Targeting und Session-ID generieren."""
        parts = [f"user-country-{self.country}"]
        if self.city:
            parts.append(f"city-{self.city}")
        parts.append(f"session-{session_id}")
        user_part = "-".join(parts)
        return f"http://{user_part}:{self.password}@gate.proxyhat.com:8080"

    @staticmethod
    def session_id_for(keyword: str) -> str:
        """Deterministische Session-ID pro Keyword für konsistente IPs."""
        return hashlib.md5(keyword.encode()).hexdigest()[:12]

# --- Verwendung ---
proxy = ProxyHatSession(
    username="user",
    password="pass",
    country="US",
    city="chicago",
)

keyword = "python web scraping"
sid = ProxyHatSession.session_id_for(keyword)
proxy_url = proxy.get_proxy_url(sid)

resp = requests.get(
    "https://www.google.com/search?q=python+web+scraping&gl=us&hl=en",
    proxies={"http": proxy_url, "https": proxy_url},
    impersonate="chrome",
    timeout=30,
)
print(f"Status: {resp.status_code}, Length: {len(resp.text)}")

Code-Beispiel 4: Multi-Country-Rank-Tracking mit asyncio

import asyncio
import aiohttp
from datetime import datetime, timezone

async def fetch_serp_async(
    session: aiohttp.ClientSession,
    keyword: str,
    country: str,
    session_id: str,
    start: int = 0,
) -> str:
    """Async SERP-Fetch mit ProxyHat."""
    proxy = f"http://user-country-{country}-session-{session_id}:pass@gate.proxyhat.com:8080"
    encoded = quote(keyword)
    url = f"https://www.google.com/search?q={encoded}&gl={country.lower()}&hl=en&start={start}"

    async with session.get(url, proxy=proxy, timeout=aiohttp.ClientTimeout(total=30)) as resp:
        if resp.status != 200:
            raise RuntimeError(f"HTTP {resp.status}")
        return await resp.text()

async def track_keyword_country(keyword: str, country: str, target_domain: str):
    """Ein Keyword in einem Land tracken (Top 100)."""
    sid = hashlib.md5(f"{keyword}-{country}".encode()).hexdigest()[:12]
    all_urls = []

    async with aiohttp.ClientSession() as session:
        tasks = []
        for start in range(0, 100, 10):
            tasks.append(fetch_serp_async(session, keyword, country, sid, start))
            await asyncio.sleep(1)  # Staggered Start

        results = await asyncio.gather(*tasks, return_exceptions=True)

    for i, html in enumerate(results):
        if isinstance(html, Exception):
            print(f"[ERROR] {keyword}/{country} page {i}: {html}")
            continue
        urls = re.findall(r'<a href="(https?://[^"]+)"', html)
        all_urls.extend(urls)

    position = None
    for idx, url in enumerate(all_urls, 1):
        if target_domain in url:
            position = idx
            break

    print(f"{keyword} | {country} → Position {position}")
    return position

# --- Main: 3 Keywords × 2 Länder = 6 Tasks, max 3 concurrent ---
KEYWORDS = ["python scraping", "scrapy vs beautifulsoup", "selenium tutorial"]
COUNTRIES = ["US", "DE"]
TARGET = "example.com"

async def main():
    sem = asyncio.Semaphore(3)  # Max 3 gleichzeitige Requests

    async def limited_track(kw, ctry):
        async with sem:
            return await track_keyword_country(kw, ctry, TARGET)

    tasks = [limited_track(kw, ctry) for kw in KEYWORDS for ctry in COUNTRIES]
    await asyncio.gather(*tasks)

asyncio.run(main())

Code-Beispiel 5: Production-Hardening mit Retries und Circuit Breaker

import time
import logging
from functools import wraps
from curl_cffi import requests

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger("rank-tracker")

class CircuitBreakerOpen(Exception):
    pass

class CircuitBreaker:
    """Einfacher Circuit Breaker: öffnet nach N Fehlern, halb-öffnet nach Timeout."""

    def __init__(self, failure_threshold=5, recovery_timeout=60):
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.failures = 0
        self.last_failure_time = None
        self.state = "closed"  # closed | open | half-open

    def record_success(self):
        self.failures = 0
        self.state = "closed"

    def record_failure(self):
        self.failures += 1
        self.last_failure_time = time.time()
        if self.failures >= self.failure_threshold:
            self.state = "open"
            logger.warning(f"Circuit breaker OPEN after {self.failures} failures")

    def can_execute(self) -> bool:
        if self.state == "closed":
            return True
        if self.state == "open":
            if time.time() - self.last_failure_time > self.recovery_timeout:
                self.state = "half-open"
                return True
            return False
        return True  # half-open

def retry_with_backoff(max_retries=3, base_delay=2, max_delay=60):
    """Decorator: Exponentielles Backoff mit Jitter."""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_retries + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_retries:
                        logger.error(f"All retries exhausted: {e}")
                        raise
                    delay = min(base_delay * (2 ** attempt), max_delay)
                    logger.warning(f"Retry {attempt + 1}/{max_retries} after {delay}s: {e}")
                    time.sleep(delay)
        return wrapper
    return decorator

# --- Usage ---
cb = CircuitBreaker(failure_threshold=5, recovery_timeout=60)

@retry_with_backoff(max_retries=3, base_delay=2, max_delay=60)
def fetch_with_protection(url: str, proxy_url: str) -> str:
    if not cb.can_execute():
        raise CircuitBreakerOpen("Circuit breaker is open")

    resp = requests.get(
        url,
        proxies={"http": proxy_url, "https": proxy_url},
        impersonate="chrome",
        timeout=30,
    )

    if resp.status_code == 429:
        cb.record_failure()
        raise RuntimeError("Rate limited (429)")
    if "unusual traffic" in resp.text.lower():
        cb.record_failure()
        raise RuntimeError("CAPTCHA detected")

    cb.record_success()
    return resp.text

# --- CSV-Export der Historie ---
def export_to_csv(db_path: str, csv_path: str):
    import sqlite3
    conn = sqlite3.connect(db_path)
    rows = conn.execute("SELECT keyword, target_domain, country, device, position, captured_at FROM rankings ORDER BY captured_at").fetchall()
    with open(csv_path, "w", newline="") as f:
        writer = csv.writer(f)
        writer.writerow(["keyword", "target_domain", "country", "device", "position", "captured_at"])
        writer.writerows(rows)
    conn.close()
    logger.info(f"Exported {len(rows)} rows to {csv_path}")

Häufige Fehler und Edge Cases

1. CAPTCHA-Erkennung zu spät

Google liefert bei CAPTCHA-Antworten oft HTTP 200, aber der HTML-Body enthält „unusual traffic“ oder ein CAPTCHA-Formular. Prüfen Sie immer den Body-Inhalt, nicht nur den Statuscode.

2. Keine Session-Stickiness

Ohne -session--Flag rotiert ProxyHat die IP bei jedem Request. Für Rank-Tracking bedeutet das, dass jeder SERP-Seitenabruf von einer anderen IP kommt — die Ergebnisse können inkonsistent sein. Verwenden Sie deterministische Session-IDs pro Keyword.

3. Paginierung ignoriert SERP-Features

Featured Snippets, Knowledge Panels und Local Packs verändern die organische Positionierung. Ein einfacher Regex-Ansatz kann falsche Positionen liefern, wenn SERP-Features die Zählung verschieben. Für höhere Präzision empfiehlt sich BeautifulSoup4 mit CSS-Selektoren, die explizit auf div.g oder div[data-sokoban-container] abzielen.

4. Zu hohe Concurrency

Mehr als 5–10 gleichzeitige Requests über dieselbe Proxy-Session erhöhen das Block-Risiko massiv. Verwenden Sie asyncio.Semaphore oder ThreadPoolExecutor(max_workers=5), um die Parallelität zu begrenzen.

Ethik und Grenzen

SERP-Scraping bewegt sich in einer rechtlichen Grauzone. Die Google-Nutzungsbedingungen verbieten automatisierte Abfragen, aber die Rechtslage variiert nach Gerichtsbarkeit. Der FTC-Bericht zu Google zeigt, dass Suchmaschinen-Monopole seit Jahren regulatorisch beobachtet werden.

Best Practices für ethisches Rank-Tracking:

  • Tracken Sie primär eigene Rankings und öffentliche Daten.
  • Respektieren Sie robots.txt — Google erlaubt /search nicht für Bots.
  • Halten Sie Request-Raten niedrig: max. 1 Request alle 2–3 Sekunden pro Session.
  • Bei niedrigem Volumen (unter ~100 Keywords/Tag) ziehen Sie die Google Custom Search API in Betracht — sie ist offiziell, kostengünstig und ToS-konform.
  • Dokumentieren Sie Ihre Datenerfassung für DSGVO-/CCPA-Compliance.

ProxyHat-Angebote und verfügbare Standorte finden Sie auf der Preisseite und der Standort-Übersicht. Weitere Use Cases für Web-Scraping und SERP-Tracking werden separat behandelt.

Key Takeaways

  • Daily Snapshots > One-off Checks: Nur kontinuierliche Historie macht Ranking-Volatilität sichtbar.
  • Paginierung ist Pflicht: Seit September 2025 gibt es kein num=100 mehr — 10 Requests pro Keyword für Top 100.
  • TLS-Fingerprinting umgehen: curl_cffi mit impersonate='chrome' ist der Mindeststandard.
  • Residential + City-Geo: ISP-IPs mit Stadt-Targeting liefern die realistischsten SERP-Ergebnisse.
  • Session-Stickiness pro Keyword: Deterministische Session-IDs sorgen für konsistente IPs.
  • Production-Hardening: Retries, Circuit Breaker, CAPTCHA-Erkennung und Concurrency-Limits sind nicht optional.
  • Ethik zuerst: Bei niedrigem Volumen ist die offizielle Google Custom Search API die sicherere Wahl.

FAQ

Was ist ein Google Rank Tracker mit Residential Proxies?

Ein Google Rank Tracker mit Residential Proxies ist ein Python-Skript oder eine Anwendung, die automatisiert Google-Suchergebnisseiten abruft, die Position einer Ziel-Domain ermittelt und diese historisch speichert. Residential Proxies sorgen dafür, dass die Abrufs aus realen ISP-IP-Adressen erfolgen, was Blockierungen durch Googles Anti-Bot-Systeme minimiert und geo-genau Ergebnisse liefert.

Warum braucht man Residential Proxies für SERP-Scraping?

Google betreibt IP-Reputation-Scoring und TLS-Fingerprinting. Datacenter-IPs werden typischerweise nach 20–50 Requests pro Stunde blockiert. Residential Proxies verwenden ISP-IPs mit hoher Reputation und ermöglichen City-Level-Geo-Targeting, was für länder- und stadtgenaue Rank-Tracking-Ergebnisse essenziell ist.

Welche Proxy-Art funktioniert am besten für Google-Rank-Tracking?

Residential Proxies sind der Sweet Spot für Rank-Tracking: hohe IP-Reputation, City-Level-Geo-Targeting und moderater Preis (~$2–$5 pro GB). Mobile Proxies sind teurer und für reines Rank-Tracking überdimensioniert. Datacenter-Proxies werden zu schnell blockiert und sind für SERP-Scraping ungeeignet.

Wie vermeide ich CAPTCHAs beim SERP-Scraping?

CAPTCHAs lassen sich nicht vollständig vermeiden, aber minimieren: Verwenden Sie curl_cffi mit impersonate='chrome' für korrektes TLS-Fingerprinting, rotieren Sie IPs pro Keyword mit sticky Sessions, halten Sie Request-Raten unter 1 Request alle 2 Sekunden pro Session und implementieren Sie CAPTCHA-Erkennung im HTML-Body („unusual traffic“), um frühzeitig abzubrechen.

Ist SERP-Scraping legal?

SERP-Scraping bewegt sich in einer rechtlichen Grauzone. Googles Nutzungsbedingungen verbieten automatisierte Abfragen, aber die Durchsetzung variiert. Für niedrige Volumina (unter ~100 Keywords/Tag) ist die offizielle Google Custom Search API die ToS-konforme Alternative. Dokumentieren Sie Datenerfassung immer für DSGVO- und CCPA-Compliance.

Bereit loszulegen?

Zugang zu über 50 Mio. Residential-IPs in über 148 Ländern mit KI-gesteuerter Filterung.

Preise ansehenResidential Proxies
← Zurück zum Blog