Zbuduj tracker pozycji Google w Python z proxy rezydencyjnymi

Kompletny przewodnik dla programistów: jak zbudować produkcjny tracker pozycji Google w Python przy użyciu curl_cffi i proxy rezydencyjnych ProxyHat. Kod, pułapki i najlepsze praktyki.

Build a Google Rank Tracker in Python with Residential Proxies

Jeśli chcesz zbudować tracker pozycji Google w Python z proxy rezydencyjnymi, ten przewodnik przeprowadzi Cię przez pełny proces — od modelu danych, przez paginację SERP, aż po produkcjne utwardzenie z retries, wykrywaniem CAPTCHA i rotacją IP. Pokażemy działający kod w Python z biblioteką curl_cffi i ProxyHat SDK.

Śledzenie pozycji to nie jednorazowy zrzut — to ciągły proces. Algorytmy Google zmieniają się setki razy w roku, a pozycje potrafią skakać o kilkanaście miejsc w ciągu 24 godzin. Dlatego codzienne snapshoty SERP są znacznie cenniejsze niż pojedyncze sprawdzenia.

Dlaczego potrzebujesz tracker pozycji Python z proxy rezydencyjnymi

Google aktywnie blokuje zautomatyzowane zapytania. Płaskie zapytania z datacenter IP są często odrzucane już na poziomie TLS, zanim w ogóle dotrą do warstwy HTTP. Google stosuje odcisk palca TLS (JA3/JA4) oraz scoring reputacji IP, aby odróżnić prawdziwych użytkowników od botów. Proxy rezydencyjne rozwiązują oba problemy: pochodzą z puli ISP, mają realną reputację i można je przypisać do konkretnego miasta.

Bez odpowiedniego proxy Twoje zapytania będą trafiać na CAPTCHA, błędy 429 lub ciche przekierowania. Oznacza to, że Twoje dane o pozycjach będą po prostu błędne — a w świecie SEO błędne dane są gorsze niż brak danych.

Kontekst techniczny: co się zmieniło w Google SERP

Google usunęło parametr num=100 we wrześniu 2025 roku, co oznacza, że nie możesz już pobrać 100 wyników jednym zapytaniem. Musisz teraz paginować z parametrem start=0,10,20,...,90, aby pokryć top 100 wyników organicznych. To dziesięć zapytań na słowo kluczowe zamiast jednego — co zwiększa ryzyko wykrycia i sprawia, że rotacja IP staje się krytyczna.

Więcej o polityce Google wobec automatycznych zapytań znajdziesz w oficjalnej dokumentacji Google Search. Dodatkowo, specyfikacja nagłówka User-Agent opisana jest w MDN Web Docs.

Model danych: słowo kluczowe, domena, kraj, urządzenie, pozycja, data

Dobry tracker pozycji zaczyna się od prostego modelu danych. Każdy rekord snapshotu SERP powinien zawierać:

  • słowo kluczowe — śledzone zapytanie
  • domena docelowa — np. example.com
  • kraj — kod geo, np. US, DE, PL
  • urządzeniedesktop lub mobile
  • pozycja — 1–100 (lub null jeśli poza top 100)
  • URL wyniku — dokładny URL znaleziony w SERP
  • data pobrania — timestamp UTC

Codzienne snapshoty pozwalają na analizę trendów, wykrywanie anomaliów i obliczanie zmienności pozycji. Pojedyncze sprawdzenie mówi Ci tylko, gdzie jesteś teraz — historia mówi, dokąd zmierzasz.

Schemat SQLite

import sqlite3

conn = sqlite3.connect("rank_tracker.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 DEFAULT 'desktop',
    position INTEGER,
    result_url TEXT,
    captured_at TEXT NOT NULL,
    UNIQUE(keyword, target_domain, country, device, captured_at)
)
""")
conn.commit()

Pobieranie SERP: paginacja z start=0,10,20

Po usunięciu num=100 musisz paginować. Google zwraca około 10 wyników organicznych na stronę. Aby pokryć top 100, potrzebujesz 10 zapytań z parametrem start rosnącym o 10. Oto podstawowy przykład z curl:

# Pojedyncza strona SERP przez ProxyHat (HTTP, port 8080)
curl -x http://user-country-US-city-chicago-session-keyword1:pass@gate.proxyhat.com:8080 \
  -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" \
  "https://www.google.com/search?q=python+web+scraping&gl=us&hl=en&start=0&num=10"

Parametr gl ustawia kraj, hl język, a start offset paginacji. Sticky session (-session-keyword1) utrzymuje ten sam IP dla całego słowa kluczowego, co zapobiega niespójnościom w wynikach.

Dlaczego TLS fingerprinting wymusza proxy rezydencyjne

Google analizuje odcisk TLS klienta (JA3/JA4) już podczas handshake'u. Standardowe biblioteki Python (requests, httpx) generują odcisk, który różni się od prawdziwych przeglądarek — Google to wykrywa. Biblioteka curl_cffi rozwiązuje ten problem poprzez impersonację przeglądarki Chrome na poziomie TLS.

Proxy rezydencyjne dodatkowo zapewniają:

  • Reputację IP — adresy z prawdziwych ISP, nie datacenter
  • Geo-targeting na poziomie miasta — krytyczne dla lokalnych wyników
  • Sticky sessions — ten sam IP dla całej sesji paginacji
  • Naturalną dystrybucję zapytań — różne IP dla różnych słów kluczowych

Dowiedz się więcej o dostępnych lokalizacjach na stronie ProxyHat Locations.

Pełny przykład: curl_cffi + ProxyHat + parsowanie pozycji

Poniższy kod łączy wszystko: pobiera SERP z impersonacją Chrome, używa ProxyHat z geo-targetingiem i sticky session, parsuje pozycje organiczne i zapisuje do SQLite.

import sqlite3
import re
import time
from datetime import datetime, timezone
from curl_cffi import requests as cffi_requests
from bs4 import BeautifulSoup

PROXY_TEMPLATE = (
    "http://user-country-{country}-city-{city}-"
    "session-{session_id}:pass@gate.proxyhat.com:8080"
)

def fetch_serp_page(keyword: str, country: str, city: str,
                    start: int, session_id: str) -> str:
    """Pobiera jedną stronę SERP (10 wyników) przez ProxyHat."""
    proxy_url = PROXY_TEMPLATE.format(
        country=country, city=city, session_id=session_id
    )
    proxies = {"http": proxy_url, "https": proxy_url}

    url = "https://www.google.com/search"
    params = {
        "q": keyword,
        "gl": country.lower(),
        "hl": "en",
        "start": start,
        "num": 10,
    }

    resp = cffi_requests.get(
        url,
        params=params,
        proxies=proxies,
        impersonate="chrome",
        timeout=30,
    )
    resp.raise_for_status()
    return resp.text

def parse_organic_results(html: str) -> list[dict]:
    """Parsuje organiczne wyniki z HTML SERP. Pomija reklamy."""
    soup = BeautifulSoup(html, "html.parser")
    results = []

    # Google organic results w div.g z ahref link
    for div in soup.select("div.g"):
        link = div.select_one("a[href]")
        if not link:
            continue
        href = link.get("href", "")
        if not href.startswith("http"):
            continue
        # Pomiń reklamy — mają atrybut data-text-ad lub label "Ad"
        if div.get("data-text-ad") or div.select_one("[aria-label='Ad']"):
            continue
        title_el = div.select_one("h3")
        title = title_el.get_text(strip=True) if title_el else ""
        results.append({"url": href, "title": title})

    return results

def track_keyword(keyword: str, target_domain: str,
                  country: str = "US", city: str = "chicago") -> dict:
    """Śledzi pozycję domeny dla słowa kluczowego w top 100."""
    session_id = re.sub(r"[^a-z0-9]", "", keyword.lower())[:20]
    all_results = []

    for start in range(0, 100, 10):
        try:
            html = fetch_serp_page(keyword, country, city, start, session_id)
            page_results = parse_organic_results(html)
            all_results.extend(page_results)
            time.sleep(2)  # opóźnienie między stronami
        except Exception as e:
            print(f"Błąd dla start={start}: {e}")
            continue

    # Znajdź pozycję domeny docelowej
    position = None
    result_url = None
    for i, r in enumerate(all_results, 1):
        if target_domain in r["url"]:
            position = i
            result_url = r["url"]
            break

    return {
        "keyword": keyword,
        "target_domain": target_domain,
        "country": country,
        "position": position,
        "result_url": result_url,
        "captured_at": datetime.now(timezone.utc).isoformat(),
    }

# Zapis do SQLite
conn = sqlite3.connect("rank_tracker.db")
record = track_keyword("python web scraping", "example.com", "US", "chicago")
conn.execute(
    "INSERT OR REPLACE INTO rankings "
    "(keyword, target_domain, country, device, position, result_url, captured_at) "
    "VALUES (?, ?, ?, ?, ?, ?, ?)",
    (record["keyword"], record["target_domain"], record["country"],
     "desktop", record["position"], record["result_url"], record["captured_at"])
)
conn.commit()
print(f"Pozycja: {record['position']}")

Eksport do CSV

import csv

rows = conn.execute(
    "SELECT keyword, target_domain, country, position, captured_at "
    "FROM rankings ORDER BY captured_at DESC"
).fetchall()

with open("rankings_export.csv", "w", newline="") as f:
    writer = csv.writer(f)
    writer.writerow(["keyword", "domain", "country", "position", "captured_at"])
    writer.writerows(rows)

Utrwardzenie produkcjne: retries, CAPTCHA, współbieżność

W produkcji musisz obsłużyć błędy sieciowe, CAPTCHA, limity zapytań i współbieżność. Poniższy kod dodaje exponential backoff, wykrywanie CAPTCHA i limit współbieżności.

import asyncio
import logging
from tenacity import retry, stop_after_attempt, wait_exponential

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("rank_tracker")

MAX_CONCURRENT = 5  # limit współbieżnych zapytań
semaphore = asyncio.Semaphore(MAX_CONCURRENT)

CAPTCHA_INDICATORS = [
    "captcha", "unusual traffic", "detected unusual traffic",
    "sorry/image", "recaptcha",
]

def is_captcha(html: str) -> bool:
    lower = html.lower()
    return any(ind in lower for ind in CAPTCHA_INDICATORS)

@retry(
    stop=stop_after_attempt(3),
    wait=wait_exponential(multiplier=2, min=4, max=60),
)
async def fetch_with_retry(keyword, country, city, start, session_id):
    """Pobiera stronę SERP z retries i backoff."""
    async with semaphore:
        proxy_url = (
            f"http://user-country-{country}-city-{city}-"
            f"session-{session_id}:pass@gate.proxyhat.com:8080"
        )
        proxies = {"http": proxy_url, "https": proxy_url}

        url = "https://www.google.com/search"
        params = {
            "q": keyword, "gl": country.lower(), "hl": "en",
            "start": start, "num": 10,
        }

        # curl_cffi obsługuje async przez asyncio
        resp = await cffi_requests.async_get(
            url, params=params, proxies=proxies,
            impersonate="chrome", timeout=30,
        )
        resp.raise_for_status()

        if is_captcha(resp.text):
            raise RuntimeError(f"CAPTCHA detected for '{keyword}' at start={start}")

        return resp.text

async def track_keyword_async(keyword, target_domain, country="US", city="chicago"):
    """Asynchroniczne śledzenie pozycji z paginacją."""
    session_id = re.sub(r"[^a-z0-9]", "", keyword.lower())[:20]
    all_results = []

    tasks = []
    for start in range(0, 100, 10):
        tasks.append(fetch_with_retry(keyword, country, city, start, session_id))

    pages = await asyncio.gather(*tasks, return_exceptions=True)
    for page in pages:
        if isinstance(page, Exception):
            logger.error(f"Strona nieudana: {page}")
            continue
        all_results.extend(parse_organic_results(page))

    position = None
    for i, r in enumerate(all_results, 1):
        if target_domain in r["url"]:
            position = i
            break

    return {"keyword": keyword, "position": position,
            "country": country, "captured_at": datetime.now(timezone.utc).isoformat()}

async def track_batch(keywords: list[str], target_domain: str):
    """Śledzi wiele słów kluczowych współbieżnie."""
    tasks = [track_keyword_async(kw, target_domain) for kw in keywords]
    results = await asyncio.gather(*tasks, return_exceptions=True)
    for r in results:
        if isinstance(r, Exception):
            logger.error(f"Słowo nieudane: {r}")
        else:
            logger.info(f"{r['keyword']}: pozycja={r['position']}")
    return results

# Uruchomienie
# asyncio.run(track_batch(["python scraping", "best proxies", "serp api"], "example.com"))

Wygładzanie zmienności pozycji

Pozycje w Google potrafią skakać z dnia na dzień, szczególnie na pozycjach 5–20. Aby uniknąć fałszywych alarmów, zastosuj średnią kroczącą z 7 dni:

def rolling_average(positions: list[int | None], window: int = 7) -> float | None:
    """Oblicza średnią kroczącą pozycji, ignorując brakujące wartości."""
    valid = [p for p in positions[-window:] if p is not None]
    if not valid:
        return None
    return sum(valid) / len(valid)

# Przykład użycia z historii SQLite
history = conn.execute(
    "SELECT position FROM rankings WHERE keyword=? ORDER BY captured_at",
    ("python web scraping",)
).fetchall()
positions = [row[0] for row in history]
smoothed = rolling_average(positions, window=7)
print(f"Średnia 7-dniowa: {smoothed:.1f}")

ProxyHat: konfiguracja i najlepsze praktyki

ProxyHat oferuje proxy rezydencyjne, mobilne i datacenter z geo-targetingiem na poziomie kraju i miasta. Dla śledzenia pozycji Google rekomendujemy:

Parametr Wartość Opis
Gateway gate.proxyhat.com Host bramy ProxyHat
Port HTTP 8080 Domyślny port HTTP
Port SOCKS5 1080 Port SOCKS5 (opcjonalny)
Geo-targeting user-country-US-city-chicago Kraj i miasto w username
Sticky session -session-abc123 Utrzymuje ten sam IP dla sesji

Pełną dokumentację znajdziesz na docs.proxyhat.com. Cennik i plany znajdziesz na stronie ProxyHat Pricing.

Strategia rotacji IP

Kluczowa zasada: jeden sticky session na słowo kluczowe. Oznacza to, że wszystkie 10 stron paginacji dla danego słowa kluczowego przechodzą przez ten sam IP. Zapewnia to spójność wyników — Google personalizuje wyniki częściowo na podstawie IP, więc mieszanie IP w ramach jednego słowa kluczowego daje niespójne pozycje.

Dla różnych słów kluczowych używaj różnych sesji, aby rozproszyć zapytania. Sprawdź też nasz artykuł o SERP tracking oraz web scraping.

Etyka i limity: śledź odpowiedzialnie

Śledzenie pozycji Google wiąże się z kwestiami etycznymi i prawnymi:

  • Respektuj robots.txt — sprawdź https://www.google.com/robots.txt przed zautomatyzowanymi zapytaniami.
  • Stosuj opóźnienia — 2–5 sekund między zapytaniami to minimum. 100 współbieżnych sesji to realistyczny limit dla produkcji.
  • Śledź własne i publiczne pozycje — nie śledź konkurencji w sposób, który narusza jej prawa.
  • Preferuj oficjalne API przy niskim wolumenie — jeśli śledzisz mniej niż 50 słów kluczowych, rozważ Google Custom Search API zamiast scrapingu.
  • GDPR i CCPA — dane SERP mogą zawierać dane osobowe; przechowuj je zgodnie z RODO.

Więcej o automatycznym indeksowaniu Google znajdziesz w dokumentacji Google Crawlers.

Najczęstsze błędy i pułapki

  • Ignorowanie odcisku TLSrequests bez impersonacji zostanie zablokowany. Używaj curl_cffi z impersonate='chrome'.
  • Brak sticky session — mieszanie IP w ramach jednego słowa kluczowego daje niespójne pozycje.
  • Zbyt wysoka współbieżność — 100+ zapytań naraz z jednego IP wywoła CAPTCHA. Limituj do 5–10 współbieżnych.
  • Brak wykrywania CAPTCHA — ciche przekierowania na CAPTCHA dają fałszywe dane. Zawsze sprawdzaj HTML.
  • Pomijanie reklam — reklamy Google Ads mają inną strukturę DOM. Filtruj je, aby nie liczyć ich jako wyników organicznych.
  • Brak opóźnień — zapytania bez opóźnienia to pewny sposób na ban. 2 sekundy między stronami to minimum.

Kluczowe wnioski

Podsumowanie:

  • Używaj curl_cffi z impersonate='chrome' dla poprawnego odcisku TLS.
  • Proxy rezydencyjne ProxyHat z geo-targetingiem miasta i sticky session na słowo kluczowe.
  • Paginuj start=0,10,20,...,90 dla top 100 wyników (10 zapytań na słowo).
  • Implementuj retries z exponential backoff i wykrywanie CAPTCHA.
  • Limituj współbieżność do 5–10 i stosuj 2s opóźnienia między stronami.
  • Przechowuj historię w SQLite i wygładzaj pozycje średnią kroczącą 7 dni.
  • Preferuj oficjalne API przy niskim wolumenie; respektuj robots.txt i RODO.

FAQ

Czym jest tracker pozycji Google w Python z proxy rezydencyjnymi?

To system, który automatycznie pobiera wyniki SERP Google dla wybranych słów kluczowych i śledzi pozycję docelowej domeny w czasie. Proxy rezydencyjne są niezbędne, ponieważ Google blokuje zapytania z datacenter IP na poziomie TLS i reputacji IP. Python z biblioteką curl_cffi umożliwia impersonację przeglądarki Chrome, co omija fingerprinting TLS.

Dlaczego tracker pozycji Python ma znaczenie dla użytkowników proxy?

Śledzenie pozycji to jedno z najbardziej wymagających zastosowań proxy: wymaga geo-targetingu na poziomie miasta, sticky session dla spójności wyników i wysokiej reputacji IP. Proxy rezydencyjne ProxyHat spełniają wszystkie te wymagania. Bez odpowiedniego proxy dane o pozycjach są błędne lub niedostępne z powodu CAPTCHA i blokad.

Który typ proxy działa najlepiej dla trackera pozycji Google?

Proxy rezydencyjne z geo-targetingiem na poziomie miasta i sticky session. Adresy z prawdziwych ISP mają reputację, której datacenter IP nie posiadają. Sticky session zapewnia, że wszystkie 10 stron paginacji dla jednego słowa kluczowego przechodzi przez ten sam IP, co gwarantuje spójność wyników. ProxyHat oferuje konfigurację user-country-US-city-chicago-session-abc123 na porcie 8080.

Jak uniknąć blokad przy implementacji trackera pozycji Google?

Stosuj cztery zasady: po pierwsze, używaj curl_cffi z impersonate='chrome' dla poprawnego odcisku TLS. Po drugie, limituj współbieżność do 5–10 zapytań i stosuj 2 sekundy opóźnienia między stronami. Po trzecie, implementuj wykrywanie CAPTCHA i retries z exponential backoff. Po czwarte, używaj różnych sticky session dla różnych słów kluczowych, aby rozproszyć zapytania.

Czy mogę używać Google Custom Search API zamiast scrapingu?

Tak, przy niskim wolumenie (poniżej 50 słów kluczowych) Google Custom Search API jest lepszym wyborem — jest legalne, stabilne i nie wymaga proxy. Jednak API ma limit 100 zapytań dziennie w darmowym planie i nie zwraca organicznych wyników w takiej samej formie jak prawdziwy SERP. Dla większego wolumenu lub precyzyjnego śledzenia pozycji scraping z proxy rezydencyjnymi jest bardziej praktyczny.

Gotowy, aby zacząć?

Dostęp do ponad 50 mln rezydencjalnych IP w ponad 148 krajach z filtrowaniem AI.

Zobacz cenyProxy rezydencjalne
← Powrót do Bloga