Google Rank Tracker in Python mit Residential Proxies bauen

Vollständiger Developer-Guide: Google Rank Tracker in Python mit Residential Proxies bauen — inklusive curl_cffi, ProxyHat-Gateway, SQLite-Historie, Paginierung und Production-Hardening.

Build a Google Rank Tracker in Python with Residential Proxies
In diesem Artikel

Google Rank Tracker in Python mit Residential Proxies bauen: Grundlagen

Wer einen Google Rank Tracker in Python mit Residential Proxies bauen möchte, steht vor drei zentralen Herausforderungen: Google blockiert automatisierte SERP-Anfragen aggressiv, der klassische num=100-Parameter wurde im September 2025 entfernt, und TLS-Fingerprinting macht einfache HTTP-Clients wie requests zunehmend ineffektiv. Dieser Guide zeigt Schritt für Schritt, wie Sie mit curl_cffi, ProxyHat Residential Proxies und einer SQLite-Historie einen produktionsreifen Rank-Tracker implementieren.

Ein Rank-Tracker ist kein Einmal-Check — er ist ein Monitoringsystem. Tagesaktuelle SERP-Snapshots zeigen Volatilitätsmuster, Algorithmus-Updates und Wettbewerbsverschiebungen, die ein einzelner Check nie sichtbar macht. Wer SERP-Tracking ernsthaft betreiben will, braucht reproduzierbare, blockierungsresistente Datenerfassung.

Datenmodell: Keyword, Domain, Land, Gerät, Position, Zeitstempel

Ein robuster Rank-Tracker beginnt mit einem sauberen Datenmodell. Mindestens diese Felder benötigen Sie pro Ranking-Messung:

  • keyword — der Suchbegriff, z. B. best running shoes
  • target_domain — die Domain, deren Ranking Sie tracken, z. B. example.com
  • country — das Geo-Target, z. B. US oder DE
  • devicedesktop oder mobile
  • position — die organische Position (1–100), 0 wenn nicht gefunden
  • captured_at — ISO-8601-Zeitstempel der Messung

Warum tägliche Snapshots statt Einmal-Checks? Rankings schwanken täglich. Ein Keyword kann montags auf Position 3 stehen und mittwochs auf Position 12 — oft wegen Algorithmus-Updates, Personalisierung oder Indexierungsänderungen. Mit einer SQLite- oder CSV-Historie erkennen Sie Trends über Wochen und Monate, nicht nur Momentaufnahmen.

SQLite-Schema für Ranking-Historie

import sqlite3

def init_db(db_path="rankings.db"):
    conn = sqlite3.connect(db_path)
    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 DEFAULT 'US',
            device TEXT NOT NULL DEFAULT 'desktop',
            position INTEGER NOT NULL,
            captured_at TEXT NOT NULL,
            UNIQUE(keyword, target_domain, country, device, captured_at)
        )
    """)
    conn.execute("""
        CREATE INDEX IF NOT EXISTS idx_keyword_domain
        ON rankings(keyword, target_domain, country, device)
    """)
    conn.commit()
    return conn

Das UNIQUE-Constraint verhindert doppelte Einträge für dieselbe Keyword-Domain-Land-Gerät-Kombination am selben Tag. So können Sie den Tracker mehrfach laufen lassen, ohne Duplikate zu erzeugen.

SERPs abrufen: Nach der Entfernung von num=100

Google hat im September 2025 den num=100-URL-Parameter aus den Suchergebnissen entfernt. Statt 100 Ergebnisse auf einer Seite zu laden, müssen Sie jetzt paginieren: start=0, start=10, start=20, … bis start=90. Das bedeutet bis zu 10 Anfragen pro Keyword für die Top 100, statt einer einzigen.

Diese Änderung hat zwei Konsequenzen für Rank-Tracking:

  1. Mehr Requests — 10× so viele Anfragen pro Keyword erhöhen das Blockierungsrisiko und den Proxy-Verbrauch deutlich.
  2. Mehr Parsing-Komplexität — Jede Seite kann unterschiedliche SERP-Features enthalten (Featured Snippets, People Also Ask, Local Pack), die von den organischen Ergebnissen getrennt werden müssen.

Paginierungs-Logik

def build_serp_urls(query: str, country: str = "US", max_results: int = 100) -> list[str]:
    """Erzeugt Google Search URLs für Paginierung (start=0,10,20,...)."""
    urls = []
    gl_map = {"US": "com", "DE": "de", "UK": "co.uk", "FR": "fr"}
    domain = gl_map.get(country, "com")
    for start in range(0, max_results, 10):
        url = f"https://www.google.{domain}/search?q={query}&start={start}&num=10&hl=en"
        urls.append(url)
    return urls

# Beispiel: Top 100 für "best running shoes" in den USA
urls = build_serp_urls("best+running+shoes", "US", 100)
# Ergibt 10 URLs: start=0, start=10, ..., start=90

Organische Ergebnisse parsen

Nach dem Abruf müssen Sie organische Ergebnisse aus dem HTML extrahieren und Anzeigen sowie SERP-Features überspringen. Google strukturiert organische Ergebnisse in <div class="g">-Containern, aber die genauen CSS-Selektoren ändern sich häufig. Eine robuste Strategie kombiniert CSS-Selektoren mit Regex-Fallbacks.

from bs4 import BeautifulSoup
import re

def parse_organic_results(html: str, target_domain: str) -> int:
    """Extrahiert die Position von target_domain aus den organischen Ergebnissen."""
    soup = BeautifulSoup(html, "html.parser")
    position = 0

    # Primärer Selektor: div.g mit Link
    results = soup.select("div.g div.yuRUbf > a")
    if not results:
        # Fallback: alle h3 mit übergeordnetem Link
        results = soup.select("a:has(h3)")

    for idx, link in enumerate(results, start=1):
        href = link.get("href", "")
        if target_domain in href:
            position = idx
            break

    # Regex-Fallback: Suche nach URL-Mustern im HTML
    if position == 0:
        pattern = rf'href="(https?://[^"]*{re.escape(target_domain)}[^"]*)"'
        matches = re.findall(pattern, html)
        if matches:
            all_links = re.findall(r'href="(https?://[^"]*)"', html)
            organic_count = sum(1 for l in all_links if "google." not in l)
            position = min(organic_count, 100)

    return position

Warum Residential Proxies mit Stadt-Level Geo-Targeting nötig sind

Google nutzt zwei Hauptschichten zur Erkennung von Scraping-Bots:

1. TLS/JA3-JA4-Fingerprinting

Beim TLS-Handshake erzeugt jeder HTTP-Client einen einzigartigen Fingerabdruck (JA3/JA4). Pythons requests-Bibliothek verwendet OpenSSL mit einem charakteristischen Fingerabdruck, der sich deutlich von echten Browsern unterscheidet. Google kann requests-Traffic bereits auf TLS-Ebene erkennen, bevor die eigentliche HTTP-Anfrage gesendet wird. Die curl_cffi-Bibliothek löst dieses Problem, indem sie den TLS-Stack von Chrome nachahmt (impersonate='chrome').

2. IP-Reputation-Scoring

Google bewertet die Reputation jeder IP-Adresse. Datacenter-IPs aus bekannten Cloud-Providern (AWS, DigitalOcean, Hetzner) haben niedrige Reputation und werden schnell blockiert oder mit CAPTCHAs versehen. Residential-IPs stammen aus echten ISP-Blöcken und haben hohe Reputation, weil sie wie normale Nutzer aussehen.

Stadt-Level Geo-Targeting ist wichtig, weil Google Suchergebnisse nach geografischer Nähe personalisiert. Ein Nutzer in Chicago sieht andere lokale Ergebnisse als einer in New York. Für akkurates Rank-Tracking müssen Sie die Proxy-Location an die Zielregion anpassen. ProxyHat bietet Residential Proxies mit Stadt-Level Geo-Targeting über 90+ Standorte.

Proxy-Typen im Vergleich

Proxy-TypErkennungsrisikoGeo-TargetingPreis (ca.)Eignung für Rank-Tracking
DatacenterHoch — IP aus AS, nicht ISPLand$0,50–2/GBNur für nicht-kritische Checks
ResidentialNiedrig — IP aus echtem ISP-BlockLand + Stadt$3–15/GBPrimäre Wahl
MobileSehr niedrig — IP aus MobilfunknetzLand$10–30/GBÜberfürsorglich, teuer

Worked Example: curl_cffi + ProxyHat

Jetzt kombinieren wir alle Bausteine: curl_cffi für TLS-Kompatibilität, ProxyHat Residential Proxies für IP-Rotation und SQLite für die Historie.

ProxyHat Proxy-Konfiguration

ProxyHat verwendet einen zentralen Gateway unter gate.proxyhat.com:8080. Geo-Targeting und Session-Flags werden im Username kodiert:

# ProxyHat HTTP Proxy URL Formate:
# Land:      http://user-country-US:pass@gate.proxyhat.com:8080
# Stadt:     http://user-country-US-city-chicago:pass@gate.proxyhat.com:8080
# Session:   http://user-country-US-city-chicago-session-abc123:pass@gate.proxyhat.com:8080

# curl Beispiel:
# curl -x http://user-country-US-city-chicago-session-kw123:pass@gate.proxyhat.com:8080 \\
#   "https://www.google.com/search?q=best+running+shoes&start=0&num=10"

Python-Implementierung mit curl_cffi

from curl_cffi import requests as cffi_requests
import sqlite3
import hashlib
import time
import logging

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger(__name__)

PROXYHAT_GATEWAY = "gate.proxyhat.com:8080"
PROXYHAT_USER = "user"
PROXYHAT_PASS = "pass"

def build_proxy(country: str, city: str = None, session_id: str = None) -> str:
    """Erzeugt eine ProxyHat-Proxy-URL mit Geo-Targeting und Sticky Session."""
    parts = [PROXYHAT_USER, f"country-{country}"]
    if city:
        parts.append(f"city-{city}")
    if session_id:
        parts.append(f"session-{session_id}")
    username = "-".join(parts)
    return f"http://{username}:{PROXYHAT_PASS}@{PROXYHAT_GATEWAY}"

def is_captcha_page(html: str) -> bool:
    """Erkennt CAPTCHA-Seiten anhand typischer Indikatoren."""
    captcha_signals = [
        "Our systems have detected unusual traffic",
        "detected unusual traffic from your computer network",
        "g-recaptcha",
        "api/services/redirect/CaptchaForward",
    ]
    return any(signal in html for signal in captcha_signals)

def fetch_serp(query: str, country: str, start: int = 0, session_id: str = None) -> str:
    """Ruft eine Google SERP-Seite über ProxyHat ab."""
    proxy = build_proxy(country=country, session_id=session_id)
    gl_map = {"US": "com", "DE": "de", "UK": "co.uk", "FR": "fr"}
    domain = gl_map.get(country, "com")
    url = f"https://www.google.{domain}/search?q={query}&start={start}&num=10&hl=en"

    try:
        response = cffi_requests.get(
            url,
            impersonate="chrome",
            proxies={"http": proxy, "https": proxy},
            timeout=30,
        )
        if response.status_code == 200:
            return response.text
        elif response.status_code == 429:
            logger.warning("Rate limit (429) — pausiere 60s")
            time.sleep(60)
            return fetch_serp(query, country, start, session_id)
        else:
            logger.error(f"HTTP {response.status_code} fuer {url}")
            return ""
    except Exception as e:
        logger.error(f"Fehler beim Abruf von {url}: {e}")
        return ""

def track_keyword(keyword: str, target_domain: str, country: str = "US",
                  device: str = "desktop", db_path: str = "rankings.db") -> int:
    """Trackt das Ranking einer Domain fuer ein Keyword ueber die Top 100."""
    conn = init_db(db_path)
    session_id = hashlib.md5(keyword.encode()).hexdigest()[:8]
    final_position = 0

    for start in range(0, 100, 10):
        html = fetch_serp(keyword.replace(" ", "+"), country, start, session_id)
        if not html:
            continue
        if is_captcha_page(html):
            logger.warning(f"CAPTCHA erkannt bei start={start} — ueberspringe")
            time.sleep(30)
            continue

        pos = parse_organic_results(html, target_domain)
        if pos > 0:
            final_position = start + pos
            break

    captured_at = time.strftime("%Y-%m-%dT%H:%M:%S")
    conn.execute(
        "INSERT OR REPLACE INTO rankings (keyword, target_domain, country, device, position, captured_at) "
        "VALUES (?, ?, ?, ?, ?, ?)",
        (keyword, target_domain, country, device, final_position, captured_at)
    )
    conn.commit()
    conn.close()
    logger.info(f"{keyword} → Position {final_position} fuer {target_domain}")
    return final_position

if __name__ == "__main__":
    track_keyword("best running shoes", "nike.com", "US")

CSV-Export für Reporting

import csv

def export_csv(db_path: str = "rankings.db", output: str = "rankings.csv"):
    """Exportiert die Ranking-Historie als CSV."""
    conn = sqlite3.connect(db_path)
    rows = conn.execute(
        "SELECT keyword, target_domain, country, device, position, captured_at "
        "FROM rankings ORDER BY keyword, captured_at"
    ).fetchall()
    conn.close()

    with open(output, "w", newline="", encoding="utf-8") as f:
        writer = csv.writer(f)
        writer.writerow(["keyword", "target_domain", "country", "device", "position", "captured_at"])
        writer.writerows(rows)
    logger.info(f"{len(rows)} Eintraege nach {output} exportiert")

Production Hardening: Retries, CAPTCHA-Erkennung, Concurrency

Ein produktionsreifer Rank-Tracker braucht mehr als einen einfachen Fetch-Loop. Hier sind die wichtigsten Hardening-Maßnahmen:

Exponential Backoff mit Retry-Logik

import random
import asyncio
from typing import Optional

class SERPFetcher:
    def __init__(self, max_retries: int = 3, base_delay: float = 2.0):
        self.max_retries = max_retries
        self.base_delay = base_delay

    async def fetch_with_retry(self, url: str, proxy: str) -> Optional[str]:
        """Fetch mit exponentiellem Backoff und Jitter."""
        for attempt in range(self.max_retries):
            try:
                response = await cffi_requests.get_async(
                    url,
                    impersonate="chrome",
                    proxies={"http": proxy, "https": proxy},
                    timeout=30,
                )
                if response.status_code == 200:
                    return response.text
                elif response.status_code == 429:
                    delay = self.base_delay * (2 ** attempt) + random.uniform(0, 1)
                    logger.warning(f"429 — Retry in {delay:.1f}s (Versuch {attempt+1}/{self.max_retries})")
                    await asyncio.sleep(delay)
                elif response.status_code == 503:
                    logger.warning("503 — Google temporaer nicht verfuegbar")
                    await asyncio.sleep(5)
                else:
                    logger.error(f"HTTP {response.status_code}")
                    return None
            except Exception as e:
                delay = self.base_delay * (2 ** attempt)
                logger.error(f"Fehler (Versuch {attempt+1}): {e} — Retry in {delay}s")
                await asyncio.sleep(delay)
        return None

Concurrency-Limits und Sticky Sessions pro Keyword

Um Google nicht zu überlasten und Blockierungen zu vermeiden, sollten Sie maximal 5–10 gleichzeitige Requests senden und pro Keyword eine sticky Session verwenden. Sticky Sessions stellen sicher, dass alle Paginierungs-Anfragen für ein Keyword von derselben IP kommen — das reduziert Inkonsistenzen in den Ergebnissen.

import asyncio

async def track_keywords_batch(keywords: list[str], target_domain: str,
                                 country: str = "US", max_concurrent: int = 5):
    """Trackt mehrere Keywords mit begrenzter Concurrency."""
    semaphore = asyncio.Semaphore(max_concurrent)
    fetcher = SERPFetcher(max_retries=3, base_delay=2.0)

    async def track_one(kw: str):
        async with semaphore:
            session_id = hashlib.md5(kw.encode()).hexdigest()[:8]
            proxy = build_proxy(country=country, session_id=session_id)
            # ... fetch und parse logik hier ...
            await asyncio.sleep(random.uniform(1, 3))  # Jitter zwischen Requests
            return kw

    tasks = [track_one(kw) for kw in keywords]
    results = await asyncio.gather(*tasks, return_exceptions=True)
    return results

# Ausfuehrung
# asyncio.run(track_keywords_batch(["best running shoes", "trail running shoes"], "nike.com"))

Rank-Volatility Smoothing

Einzelne Ranking-Schwankungen können durch Personalisierung oder temporäre Störungen verursacht werden. Ein 7-Tage-Durchschnitt (Moving Average) glättet kurzfristige Ausreißer und zeigt echte Trends:

def smoothed_rank(conn: sqlite3.Connection, keyword: str,
                    target_domain: str, window: int = 7) -> float | None:
    """Berechnet den gleitenden Durchschnitt der letzten N Tage."""
    rows = conn.execute(
        "SELECT position FROM rankings WHERE keyword=? AND target_domain=? "
        "ORDER BY captured_at DESC LIMIT ?",
        (keyword, target_domain, window)
    ).fetchall()
    if not rows:
        return None
    positions = [r[0] for r in rows if r[0] > 0]
    return sum(positions) / len(positions) if positions else None

Ethik und Limits: Verantwortungsvolles Rank-Tracking

SERP-Scraping bewegt sich in einer rechtlichen Grauzone. Beachten Sie folgende Prinzipien:

  • Tracken Sie eigene Rankings — Wenn Sie Ihre eigene Website tracken, ist das Risiko minimal. Die Google Search Console API bietet hierfür offizielle Daten.
  • Respektieren Sie Rate Limits — Senden Sie nicht mehr als 1–2 Requests pro Sekunde pro Proxy-IP. ProxyHat ermöglicht flexible Rate-Limits durch Rotation.
  • Bevorzugen Sie offizielle Quellen bei geringem Volumen — Für unter 100 Keywords ist die Google Search Console API oft ausreichend und kostenlos.
  • robots.txt beachten — Google erlaubt in seiner robots.txt kein Scraping der Suchergebnisseiten. Das ist ein bewusstes Signal, auch wenn es technisch nicht durchsetzbar ist.
  • DSGVO/GDPR — SERP-Daten enthalten keine personenbezogenen Daten, aber speichern Sie keine Suchanfragen, die personenidentifizierbar sein könnten.

Weitere Details zur Proxy-Konfiguration finden Sie in der ProxyHat-Dokumentation. Für einen breiteren Überblick über Web-Scraping-Anwendungen siehe unsere Web-Scraping-Use-Case-Seite.

Key Takeaways

  • Datenmodell zuerst — Keyword, Domain, Land, Gerät, Position, Zeitstempel in SQLite oder CSV speichern.
  • Paginierung statt num=100 — Seit September 2025 müssen Sie start=0,10,20,…,90 iterieren für die Top 100.
  • curl_cffi mit impersonate='chrome' — Umgeht TLS/JA3-Fingerprinting, das requests entlarvt.
  • Residential Proxies mit Stadt-Level Geo-Targeting — Über gate.proxyhat.com:8080 mit -country-US-city-chicago im Username.
  • Sticky Sessions pro Keyword-session-{hash} im Username sorgt für konsistente Ergebnisse über alle Paginierungs-Seiten.
  • Production Hardening — Exponential Backoff, CAPTCHA-Erkennung, max. 5–10 concurrent Requests, 7-Tage-Smoothing.

Häufig gestellte Fragen

Was ist ein Google Rank Tracker in Python mit Residential Proxies?

Ein Google Rank Tracker in Python mit Residential Proxies ist ein automatisiertes System, das Suchergebnisseiten von Google über Python-Skripte abruft und die Position einer Zieldomain für bestimmte Keywords extrahiert. Residential Proxies leiten die Anfragen über echte ISP-IP-Adressen, um Blockierungen zu vermeiden. Der Tracker speichert historische Daten in SQLite oder CSV, um Ranking-Trends über Zeit zu analysieren.

Warum sind Residential Proxies für das SERP-Scraping wichtig?

Google bewertet die Reputation jeder IP-Adresse. Datacenter-IPs aus Cloud-Providern wie AWS oder Hetzner haben niedrige Reputation und werden schnell blockiert oder mit CAPTCHAs versehen. Residential Proxies verwenden IP-Adressen aus echten ISP-Blöcken, die wie normale Nutzerverbindungen aussehen. Mit Stadt-Level Geo-Targeting können Sie außerdem sicherstellen, dass die Ergebnisse der Zielregion entsprechen.

Welcher Proxy-Typ eignet sich am besten für Rank-Tracking?

Residential Proxies sind die beste Wahl für Rank-Tracking. Sie bieten niedriges Erkennungsrisiko, Land- und Stadt-Level Geo-Targeting und kosten etwa 3 bis 15 Dollar pro GB. Datacenter Proxies sind günstiger, aber werden von Google schnell erkannt. Mobile Proxies haben das niedrigste Erkennungsrisiko, sind aber mit 10 bis 30 Dollar pro GB deutlich teurer und für Rank-Tracking meist überdimensioniert.

Wie vermeidet man Blocks beim SERP-Scraping?

Verwenden Sie curl_cffi mit impersonate='chrome', um TLS-Fingerprinting zu umgehen. Nutzen Sie Residential Proxies mit Sticky Sessions pro Keyword, begrenzen Sie die Concurrency auf 5 bis 10 gleichzeitige Requests, implementieren Sie exponentielles Backoff bei 429-Antworten, und erkennen Sie CAPTCHA-Seiten anhand typischer HTML-Indikatoren. Eine zufällige Verzögerung von 1 bis 3 Sekunden zwischen Requests reduziert das Blockierungsrisiko zusätzlich.

Wie oft sollte man Rankings tracken?

Tägliches Tracking ist der Standard für professionelle SEO-Teams. Es erkennt Algorithmus-Updates, Volatilitätsmuster und Wettbewerbsverschiebungen, die wöchentliche oder monatliche Checks verpassen. Für weniger kritische Keywords reicht wöchentliches Tracking. Ein 7-Tage-Durchschnitt glättet kurzfristige Schwankungen und zeigt echte Trends zuverlässiger als Einzelmessungen.

Bereit loszulegen?

Residential-, ISP- und Mobile-Proxys in über 148 Ländern. Erstelle ein kostenloses Konto.

Kostenloses Konto erstellen
← Zurück zum Blog