Budowa trackera pozycji Google w Pythonie z proxy residentialnymi

Kompletny przewodnik kodowy: model danych, paginacja SERP po usunięciu num=100, fingerprinting TLS/JA3, proxy residentialne ProxyHat, parsowanie pozycji i hardening produkcji w Pythonie.

Build a Google Rank Tracker in Python with Residential Proxies
W tym artykule

Dlaczego warto zbudować tracker pozycji Google w Pythonie z proxy residentialnymi

Śledzenie pozycji w Google to nie jednorazowy raport — to ciągły strumień danych, który pokazuje, jak zmiany algorytmu, aktualizacje treści i konkurencja wpływają na widoczność domeny. Jeśli chcesz zbudować tracker pozycji Google w Pythonie z proxy residentialnymi, musisz rozwiązać trzy powiązane problemy: (1) stabilne pobieranie stron wyników bez banów, (2) poprawne parsowanie pozycji organicznych z pominięciem reklam i funkcji SERP, oraz (3) przechowywanie historycznych snapshotów, które umożliwiają analizę trendów. Ten przewodnik prowadzi od modelu danych aż po hardening produkcji, z gotowymi do uruchomienia przykładami w Pythonie.

Według danych Moz Search Ranking Factors, ponad 200 sygnałów wpływa na kolejność wyników organicznych, a pozycje mogą zmieniać się nawet codziennie. Dlatego dzienne snapshoty SERP dują wartość, której nie zapewnią okazjonalne sprawdzenia w przeglądarce.

Model danych: keyword, domena, kraj, urządzenie, pozycja, captured_at

Minimalny model danych dla trackera pozycji powinien przechwytywać pełen kontekst zapytania oraz pozycję docelowej domeny. Bez kontekstu (kraj, urządzenie, data) pojedyncza liczba „pozycja 7” jest bezużyteczna — nie wiesz, czy to wynik dla polskiego desktopa czy amerykańskiego mobile.

from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Optional

@dataclass(frozen=True)
class RankRecord:
    keyword: str
    target_domain: str          # np. "example.com" (bez https://)
    country: str                 # ISO-3166-1 alpha-2, np. "US"
    device: str                  # "desktop" lub "mobile"
    position: Optional[int]      # 1–100 lub None jeśli poza top 100
    url: Optional[str]           # URL wyniku organicznego
    captured_at: datetime        # UTC timestamp

    def __post_init__(self):
        if self.position is not None and not (1 <= self.position <= 100):
            raise ValueError(f"Pozycja poza zakresem: {self.position}")
        if self.target_domain.startswith(("http://", "https://")):
            raise ValueError("Domena bez protokołu")

Dlaczego dzienne snapshoty biją jednorazowe sprawdzenia: pojedynczy pomiar pokazuje punkt, nie trend. Przy dziennej częstotliwości przez 30 dni masz 30 punktów, z których można policzyć średnią kroczącą, wariancję i wykryć anomalie. Volatility rankingowa (np. skok z pozycji 3 na 14) często zanika w ciągu 24–48 godzin — bez historii nie odróżnisz trwałej utraty od chwilowej fluktuacji.

Pobieranie SERP po usunięciu parametru num=100

Google usunęło parametr num=100 w okolicach września 2025 roku, co oznacza, że nie możesz już jednym żądaniem pobrać 100 wyników. Musisz paginować parametrem start z krokiem 10: start=0, start=10, start=20 … aż do start=90 dla top 100.

To zmienia architekturę trackera: jedno śledzenie słowa kluczowego wymaga teraz do 10 żądań zamiast jednego. Przy 500 słówach i 4 krajach oznacza to 20 000 żądań dziennie — tu proxy residentialne stają się koniecznością, nie opcją.

from urllib.parse import urlencode, quote_plus

def build_serp_urls(keyword: str, country: str, device: str, max_results: int = 100) -> list[str]:
    """Buduje listę URL-i SERP dla paginacji start=0,10,20..."""
    base = "https://www.google.com/search"
    urls = []
    for start in range(0, max_results, 10):
        params = {
            "q": keyword,
            "start": start,
            "num": 10,
            "hl": "en" if country == "US" else "pl",
            "gl": country,
            "ie": "utf-8",
            "oe": "utf-8",
        }
        if device == "mobile":
            params["udm"] = "2"   # mobile SERP
        urls.append(f"{base}?{urlencode(params, quote_via=quote_plus)}")
    return urls

Ważne: parametr udm=2 wymusza mobilny układ wyników. Bez niego Google może zwracać hybrydowy SERP zależny od nagłówka User-Agent i fingerprintu TLS.

Dlaczego fingerprinting TLS/JA3-JA4 i reputacja IP wymuszają proxy residentialne

Google nie blokuje tylko po adresie IP. Nowoczesne systemy anti-bot łączą co najmniej trzy warstwy:

  • Reputacja IP — datacenter IP (AWS, DigitalOcean, OVH) są oznaczane jako „hosting” i otrzymują wyższy wskaźnik podejrzliwości. Według IETF TLS specyfikacji, fingerprint TLS jest sygnałem identyfikującym klienta.
  • Fingerprint TLS (JA3/JA4) — biblioteki takie jak requests czy urllib3 mają charakterystyczny handshake TLS różny od prawdziwego Chrome, co pozwala Google rozpoznać automat.
  • Behavioral signals — tempo żądań, powtarzalne nagłówki, brak interakcji z JavaScriptem.

Dlatego datacenter proxy nie wystarczą do SERP scraping w 2026. Proxy residentialne z geo-targetingiem na poziomie miasta (np. Chicago, Berlin) dają adresy IP przypisane do realnych dostawców ISP, co obniża prawdopodobieństwo wyzwania CAPTCHA i blokady. ProxyHat obsługuje to przez flagi w nazwie użytkownika.

ProxyHat: konfiguracja proxy residentialnych z geo-targetingiem i sticky sessions

ProxyHat używa bramy gate.proxyhat.com na porcie HTTP 8080 (lub SOCKS5 1080). Geo-targeting i sesje kleiste (sticky) konfiguruje się w nazwie użytkownika, co oznacza, że nie musisz zmieniać kodu HTTP — wystarczy zmienić credentials.

ParametrFormat w usernamePrzykład
Krajcountry-XXuser-country-US
Miastocity-xxxuser-country-US-city-chicago
Sesja kleistasession-xxxuser-session-abc123
# Połączenie HTTP z geo-targetingiem USA/Chicago + sticky session
PROXY_HTTP = "http://user-country-US-city-chicago-session-kw123:PASSWORD@gate.proxyhat.com:8080"

# SOCKS5 (gdy HTTP jest blokowany przez lokalny firewall)
PROXY_SOCKS5 = "socks5://user-country-US-city-chicago-session-kw123:PASSWORD@gate.proxyhat.com:1080"

# Każde słowo kluczowe powinno mieć własną sesję, aby IP było spójne
# w obrębie paginacji (start=0,10,20...):
def session_id(keyword: str, country: str) -> str:
    import hashlib
    h = hashlib.md5(f"{keyword}:{country}".encode()).hexdigest()[:8]
    return f"kw{h}"

Sticky session dla danego słowa kluczowego zapewnia, że wszystkie 10 żądań paginacji wychodzi z tego samego IP — Google widzi spójnego użytkownika, a nie 10 różnych adresów w ciągu sekundy. Szczegóły konfiguracji znajdziesz w dokumentacji ProxyHat.

Praktyczny przykład: curl_cffi + ProxyHat SDK

Biblioteka curl_cffi potrafi impersonować przeglądarkę Chrome, co oznacza, że handshake TLS wygląda identycznie jak prawdziwy Chrome 120+. To kluczowe dla SERP scraping — fingerprint JA3/JA4 pasuje do deklarowanego User-Agent.

# pip install curl_cffi beautifulsoup4 lxml
import re
from curl_cffi import requests as cffi_requests
from bs4 import BeautifulSoup
from typing import Optional

def fetch_serp_page(url: str, proxy: str, timeout: int = 20) -> str:
    """Pobiera stronę SERP z impersonacją Chrome i proxy residentialnym."""
    resp = cffi_requests.get(
        url,
        proxies={"http": proxy, "https": proxy},
        impersonate="chrome",
        timeout=timeout,
        allow_redirects=True,
    )
    resp.raise_for_status()
    return resp.text

def parse_organic_results(html: str, target_domain: str) -> Optional[int]:
    """Zwraca pozycję (1-based) pierwszego wyniku organicznego dla target_domain."""
    soup = BeautifulSoup(html, "lxml")
    position = 0
    for div in soup.select("div.g, div div.g"):
        link = div.find("a", href=True)
        if not link:
            continue
        href = link["href"]
        # Pomiń reklamy: href zawiera googleads lub aclk
        if "googleads" in href or "aclk" in href:
            continue
        position += 1
        # Normalizuj domenę
        m = re.match(r"https?://([^/]+)", href)
        if m and target_domain in m.group(1):
            return position
    return None

Powyższy kod używa selektora CSS div.g, który jest historycznie najstabilniejszym kontenerem wyników organicznych w Google. Reklamy są filtrowane po wzorcach URL (googleads, aclk). Funkcje SERP (People Also Ask, mapy, karuzele) nie zawierają linku organicznego w div.g, więc są pomijane naturalnie.

Pełny pipeline: paginacja + parsowanie + zapis do SQLite

import sqlite3
from datetime import datetime, timezone
from pathlib import Path

def init_db(path: str = "ranks.db") -> sqlite3.Connection:
    conn = sqlite3.connect(path)
    conn.execute("""
        CREATE TABLE IF NOT EXISTS rank_history (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            keyword TEXT NOT NULL,
            target_domain TEXT NOT NULL,
            country TEXT NOT NULL,
            device TEXT NOT NULL,
            position INTEGER,
            url TEXT,
            captured_at TEXT NOT NULL
        )
    """)
    conn.execute("""
        CREATE INDEX IF NOT EXISTS idx_kw_dom_country
        ON rank_history(keyword, target_domain, country, captured_at)
    """)
    return conn

def track_keyword(
    keyword: str,
    target_domain: str,
    country: str,
    device: str,
    proxy_template: str,
    conn: sqlite3.Connection,
    max_results: int = 100,
) -> Optional[int]:
    """Śledzi pozycję domeny dla słowa kluczowego. Zwraca pozycję lub None."""
    sid = session_id(keyword, country)
    proxy = proxy_template.format(session=sid, country=country)
    urls = build_serp_urls(keyword, country, device, max_results)
    found_position = None
    for page_idx, url in enumerate(urls):
        try:
            html = fetch_serp_page(url, proxy)
        except Exception as e:
            print(f"[BŁĄD] {keyword} strona {page_idx}: {e}")
            continue
        pos = parse_organic_results(html, target_domain)
        if pos is not None:
            found_position = page_idx * 10 + pos
            break  # znaleziono — nie trzeba dalej paginować
    # Zapisz rekord nawet jeśli None (poza top 100)
    conn.execute(
        "INSERT INTO rank_history VALUES (NULL, ?, ?, ?, ?, ?, ?, ?)",
        (keyword, target_domain, country, device, found_position, None,
         datetime.now(timezone.utc).isoformat()),
    )
    conn.commit()
    return found_position

# Użycie
PROXY_TPL = "http://user-country-{country}-city-chicago-session-{session}:PASSWORD@gate.proxyhat.com:8080"
conn = init_db()
pos = track_keyword("best vpn", "nordvpn.com", "US", "desktop", PROXY_TPL, conn)
print(f"Pozycja: {pos}")

Hardening produkcji: retry, CAPTCHA, concurrency, smoothing

Tracker na produkcji musi radzić z błędami sieci, wyzwaniami CAPTCHA i limitami współbieżności. Oto kluczowe wzorce:

1. Retry z exponential backoff

import time
import random
from curl_cffi import requests as cffi_requests

def fetch_with_retry(url: str, proxy: str, max_retries: int = 3) -> str:
    for attempt in range(max_retries):
        try:
            resp = cffi_requests.get(
                url, proxies={"http": proxy, "https": proxy},
                impersonate="chrome", timeout=20,
            )
            if resp.status_code == 429:
                wait = 2 ** attempt + random.uniform(0, 1)
                time.sleep(wait)
                continue
            resp.raise_for_status()
            return resp.text
        except Exception as e:
            if attempt == max_retries - 1:
                raise
            wait = 2 ** attempt + random.uniform(0, 1)
            time.sleep(wait)
    raise RuntimeError("Przekroczono liczbę prób")

2. Detekcja CAPTCHA

Google zwraca stronę CAPTCHA (zwykle HTTP 200, nie 403) z charakterystycznym treści. Sprawdzaj sygnatury:

def detect_captcha(html: str) -> bool:
    signatures = [
        "Our systems have detected unusual traffic",
        "unusual traffic from your computer network",
        "captcha",
        "g-recaptcha",
        "/sorry/index",
    ]
    lower = html.lower()
    return any(sig.lower() in lower for sig in signatures)

# W pipeline: jeśli detect_captcha(html): zmień sesję proxy i ponów po 60s

3. Limity współbieżności per-kraj

Nie wysyłaj 100 równoległych żądań do Google z jednego kraju. Użyj Semaphore lub ThreadPoolExecutor z limitem 5–10 jednoczesnych połączeń na kraj:

from concurrent.futures import ThreadPoolExecutor, as_completed

def track_batch(keywords: list[str], target_domain: str, country: str,
                proxy_template: str, conn: sqlite3.Connection,
                max_workers: int = 5):
    with ThreadPoolExecutor(max_workers=max_workers) as pool:
        futures = {
            pool.submit(track_keyword, kw, target_domain, country, "desktop",
                        proxy_template, conn): kw
            for kw in keywords
        }
        for fut in as_completed(futures):
            kw = futures[fut]
            try:
                pos = fut.result()
                print(f"{kw}: pozycja {pos}")
            except Exception as e:
                print(f"{kw}: BŁĄD {e}")

4. Rank-volatility smoothing

Pojedyncze skoki pozycji mogą być szumem. Oblicz średnią kroczącą 7-dniową, aby wygładzić anomalie:

def rolling_avg(positions: list[int], window: int = 7) -> list[float]:
    """Zwraca średnią kroczącą. None traktuj jako 100 (poza top 100)."""
    vals = [p if p is not None else 100 for p in positions]
    result = []
    for i in range(len(vals)):
        start = max(0, i - window + 1)
        result.append(sum(vals[start:i+1]) / (i - start + 1))
    return result

Przy 500 słowach kluczowych, 4 krajach i paginacji do 100 wyników (10 żądań/słowo) potrzebujesz ok. 20 000 żądań dziennie. Przy limicie 5 współbieżnych na kraj i średniej latencji 800ms na żądanie, pełny cykl trwa ok. 55 minut — akceptowalnie dla dziennego harmonogramu.

Etyka i limity: śledź własne i publiczne rankingi

Scraping Google SERP istnieje w szarej strefie prawnej. Google ToS formalnie zabraniają automatycznego pobierania, ale organy regulacyjne takie jak FTC w USA i orzecznictwo EU wskazują, że publicznie dostępne dane nie są chronione prawem autorskim same w sobie. Mimo to:

  • Przestrzegaj robots.txt — Google pozwala na ścieżkę /search dla wybranych botów, ale nie dla ogólnych scraperów.
  • Utrzymuj niskie tempo — 1–2 żądania/sekundę na sesję, nie 50.
  • Śledź własne domeny i publiczne rankingi konkurencji, nie prywatne dane.
  • Przy małej skali (poniżej 100 słów dziennie) rozważ oficjalne API Google Custom Search lub komercyjne SERP API — taniej i bezpieczniej.

Dla wyższych wolumenów proxy residentialne z geo-targetingiem miasta i sticky sessions per słowo kluczowe to pragmatyczny kompromis między kosztem a niezawodnością. Zobacz ceny ProxyHat i dostępne lokalizacje.

Key Takeaways

  • Model danych musi zawierać kontekst: keyword, domena, kraj, urządzenie, pozycja, captured_at — bez tego pojedyncza liczba jest bezużyteczna.
  • Po usunięciu num=100 paginuj start=0,10,20…90 — 10 żądań na słowo kluczowe zamiast jednego.
  • Fingerprinting TLS/JA3 i reputacja IP wymagają proxy residentialnych z geo-targetingiem miasta — datacenter proxy dostaną blokadę w ciągu godzin.
  • Używaj curl_cffi z impersonate="chrome" + ProxyHat sticky session per słowo, aby handshake TLS pasował do User-Agent.
  • Filtruj reklamy po URL (googleads, aclk) i funkcje SERP (brak linku w div.g).
  • Hardening: retry z backoff, detekcja CAPTCHA, limit 5–10 współbieżnych na kraj, średnia krocząca 7-dniowa.
  • Etyka: śledź własne i publiczne rankingi, przestrzegaj robots.txt, preferuj oficjalne API przy małej skali.

Potrzebujesz proxy do SERP tracking na większą skalę? Zobacz nasze use case: SERP tracking oraz web scraping, a potem przejdź do cennika.

Często zadawane pytania

Czym jest budowa trackera pozycji Google w Pythonie z proxy residentialnymi?

To proces tworzenia aplikacji w Pythonie, która automatycznie pobiera strony wyników Google (SERP) przez proxy residentialne, parsuje pozycje organiczne docelowej domeny i przechowuje historyczne snapshoty w bazie danych. Proxy residentialne są konieczne, ponieważ Google blokuje adresy datacenter na podstawie reputacji IP i fingerprintu TLS.

Dlaczego proxy residentialne są ważne dla trackera pozycji Google?

Google stosuje wielowarstwową detekcję botów: reputację IP, fingerprint TLS (JA3/JA4) i sygnały behawioralne. Proxy datacenter są oznaczane jako hosting i otrzymują wyższy wskaźnik podejrzliwości, co prowadzi do wyzwań CAPTCHA i banów. Proxy residentialne z geo-targetingiem miasta i sticky sessions per słowo kluczowe wyglądają jak realni użytkownicy ISP.

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

Proxy residentialne z geo-targetingiem na poziomie kraju i miasta oraz sticky sessions per słowo kluczowe. Proxy datacenter są szybko blokowane, a mobile proxy mogą być zbyt wolne i drogie dla dziennego trackingu. Residentialne zapewniają najlepszy kompromis między niezawodnością, kosztem i jakością danych.

Jak uniknąć blokad przy implementacji trackera pozycji Google w Pythonie?

Użyj curl_cffi z impersonate='chrome', aby handshake TLS pasował do prawdziwej przeglądarki. Stosuj sticky session per słowo kluczowe, aby wszystkie żądania paginacji wychodziły z jednego IP. Utrzymuj limit 5–10 współbieżnych żądań na kraj, dodaj retry z exponential backoff i detekcję CAPTCHA. Przy małej skali rozważ oficjalne SERP API zamiast scrapingu.

Śledzenie pozycji, którego nikt nie zablokuje

Dokładne dane SERP dzięki proxy residential. Załóż konto i rozpocznij monitorowanie.

Rozpocznij śledzenie
← Powrót do Bloga