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 urlsWaż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
requestsczyurllib3mają 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.
| Parametr | Format w username | Przykład |
|---|---|---|
| Kraj | country-XX | user-country-US |
| Miasto | city-xxx | user-country-US-city-chicago |
| Sesja kleista | session-xxx | user-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 NonePowyż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 resultPrzy 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ę/searchdla 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=100paginujstart=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_cffizimpersonate="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 wdiv.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.




