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ądzenie —
desktoplubmobile - pozycja — 1–100 (lub
nulljeś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-region-illinois-city-chicago-sid-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 (-sid-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}-region-{region}-city-{city}-"
"sid-{session_id}:pass@gate.proxyhat.com:8080"
)
def fetch_serp_page(keyword: str, country: str, region: 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, region=region, 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", region: str = "illinois",
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, region, 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", "illinois", "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, region, city, start, session_id):
"""Pobiera stronę SERP z retries i backoff."""
async with semaphore:
proxy_url = (
f"http://user-country-{country}-region-{region}-city-{city}-"
f"sid-{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", region="illinois", 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, region, 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-region-illinois-city-chicago |
Kraj i miasto w username |
| Sticky session | -sid-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.txtprzed 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 TLS —
requestsbez impersonacji zostanie zablokowany. Używajcurl_cffizimpersonate='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.





