Costruire un Google Rank Tracker in Python con Proxy Residenziali
Se hai già provato a tracciare posizioni organiche su Google, sai quanto sia facile incappare in blocchi IP, CAPTCHA e risultati inconsistenti. Costruire un Google rank tracker in Python con proxy residenziali è oggi l'approccio più affidabile per ottenere dati SERP coerenti nel lungo periodo. In questa guida implementeremo un tracker completo: modello dati, paginazione SERP, rotazione IP, parsing e persistenza.
Google ha rimosso il parametro num=100 a settembre 2025, obbligando i tracker a paginare con start=0,10,20 per coprire le prime 100 posizioni. In combinazione con fingerprinting TLS (JA3/JA4) e scoring della reputazione IP, questo rende i proxy residenziali con geo-targeting a livello città una scelta quasi obbligatoria per chi fa scraping SERP in Python nel 2026.
Contesto tecnico: perché il rank tracking si rompe
Google applica almeno tre livelli di difesa contro lo scraping automatizzato:
- Fingerprinting TLS (JA3/JA4): il server analizza l'handshake TLS del client. Librerie HTTP standard come
requestsourllibproducono fingerprint distinti dai browser reali, e Google può rifiutare o degradare la risposta. - Reputazione IP: gli IP dei datacenter (AWS, GCP, DigitalOcean) sono catalogati come traffico automatizzato. Le richieste da questi range vengono sfidate con CAPTCHA o reindirizzate.
- Personalizzazione geografica: i risultati variano per paese, città e dispositivo. Un tracker che non controlla questi parametri produce dati non riproducibili.
Per approfondire il funzionamento dei proxy HTTP, consulta la documentazione MDN sui proxy server.
Snapshot giornalieri vs controlli una tantum
Un singolo controllo SERP è un'istantanea rumorosa: le posizioni fluttuano del 10-20% in un giorno per keyword competitive. Solo raccogliendo snapshot giornalieri puoi:
- Rilevare trend reali separandoli dal rumore di breve termine.
- Identificare penalizzazioni o aggiornamenti algoritmo entro 24-48 ore.
- Calcolare metriche di volatilità (deviazione standard, range min-max).
Modello dati del rank tracker
Prima di scrivere codice, definiamo lo schema. Un record di ranking contiene:
| Campo | Tipo | Descrizione |
|---|---|---|
| keyword | TEXT | Termine di ricerca monitorato |
| target_domain | TEXT | Dominio di cui tracciare la posizione |
| country | TEXT | Codice paese ISO (US, DE, IT…) |
| device | TEXT | desktop o mobile |
| position | INTEGER | Posizione organica (1-100) |
| url | TEXT | URL del risultato |
| captured_at | TIMESTAMP | Momento della raccolta |
Questo schema minimalista si estende facilmente: puoi aggiungere campi per SERP features (featured snippet, people-also-ask), volume di ricerca o difficoltà keyword.
Paginazione SERP: gestire la rimozione di num=100
Da settembre 2025, Google non onora più num=100. Per coprire le prime 100 posizioni devi effettuare 10 richieste paginate con start=0,10,20,…,90. Ogni pagina restituisce circa 10 risultati organici.
Ecco un esempio base con curl tramite proxy ProxyHat:
curl -x http://user-country-US-city-chicago-session-kw123:pass@gate.proxyhat.com:8080 \
"https://www.google.com/search?q=python+web+scraping&num=10&start=0&gl=us&hl=en" \
-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"
Nota l'uso del flag -country-US-city-chicago per il geo-targeting e -session-kw123 per una sessione sticky legata alla keyword. Questo garantisce che tutte le paginazioni della stessa keyword provengano dallo stesso IP, evitando risultati inconsistenti.
Perché i proxy residenziali sono obbligatori
Confrontiamo i tre tipi di proxy per il rank tracking:
| Criterio | Datacenter | Residenziale | Mobile |
|---|---|---|---|
| Reputazione IP | Bassa (flaggato) | Alta (ISP reale) | Altissima |
| Latenza tipica | 50-100ms | 200-500ms | 300-800ms |
| Costo per GB | $0.5-2 | $3-15 | $10-30 |
| Rischio CAPTCHA | Alto | Basso | Molto basso |
| Geo-targeting città | No | Sì | Sì |
I proxy residenziali offrono il miglior compromesso tra costo e affidabilità per il rank tracking. La latenza di 200-500ms è accettabile per job batch giornalieri, e il geo-targeting a livello città permette di simulare ricerche da località specifiche — fondamentale per SEO locale. Esplora le locazioni ProxyHat disponibili per configurare il targeting corretto.
Implementazione: curl_cffi + ProxyHat
La libreria curl supporta l'impersonazione del fingerprint TLS. In Python, curl_cffi espone questa funzionalità con un'API simile a requests. Questo è il modo più efficace per bypassare il fingerprinting JA3/JA4 di Google.
Ecco l'implementazione del fetcher SERP:
from curl_cffi import requests as cffi_requests
from datetime import datetime, timezone
PROXY_TEMPLATE = (
"http://user-country-{country}-city-{city}-session-{session}"
":pass@gate.proxyhat.com:8080"
)
def fetch_serp_page(
keyword: str,
country: str,
city: str,
start: int = 0,
device: str = "desktop",
) -> str:
"""Recupera una singola pagina SERP da Google."""
session_id = f"kw{abs(hash(keyword)) % 100000}"
proxy = PROXY_TEMPLATE.format(
country=country, city=city, session=session_id
)
ua_desktop = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/131.0.0.0 Safari/537.36"
)
ua_mobile = (
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) "
"AppleWebKit/605.1.15 (KHTML, like Gecko) "
"Version/17.0 Mobile/15E148 Safari/604.1"
)
headers = {
"User-Agent": ua_mobile if device == "mobile" else ua_desktop,
"Accept-Language": "en-US,en;q=0.9",
}
params = {
"q": keyword,
"num": 10,
"start": start,
"gl": country.lower(),
"hl": "en",
}
try:
resp = cffi_requests.get(
"https://www.google.com/search",
params=params,
headers=headers,
proxies={"http": proxy, "https": proxy},
impersonate="chrome",
timeout=30,
)
resp.raise_for_status()
return resp.text
except Exception as exc:
print(f"Errore fetch SERP per '{keyword}' start={start}: {exc}")
return ""
Funzione di parsing che estrae le posizioni organiche saltando annunci e SERP features:
from bs4 import BeautifulSoup
from dataclasses import dataclass
from typing import List
@dataclass
class OrganicResult:
position: int
url: str
title: str
def parse_organic_results(html: str, base_position: int = 0) -> List[OrganicResult]:
"""Estrae risultati organici dall'HTML di Google, saltando ads e features."""
soup = BeautifulSoup(html, "html.parser")
results = []
position = base_position
for div in soup.select("div.g, div.MjjYud"):
link = div.select_one("a[href]")
if not link:
continue
href = link.get("href", "")
# Salta ads: gli URL di Google Ads contengono /aclk o googleads
if "/aclk" in href or "googleads" in href:
continue
# Salta link interni a Google
if href.startswith("/") or "google.com/search?" in href:
continue
title_elem = div.select_one("h3")
title = title_elem.get_text(strip=True) if title_elem else ""
# Pulisci URL (rimuovi redirect Google)
if "/url?q=" in href:
href = href.split("/url?q=")[1].split("&")[0]
position += 1
results.append(OrganicResult(position=position, url=href, title=title))
return results
Funzione principale che combina fetch + parse su tutte le pagine:
import time
def track_keyword(
keyword: str,
target_domain: str,
country: str = "US",
city: str = "chicago",
device: str = "desktop",
) -> dict:
"""Traccia la posizione di target_domain per keyword su Google."""
all_results = []
for start in range(0, 100, 10):
html = fetch_serp_page(keyword, country, city, start, device)
if not html:
continue
# Rileva CAPTCHA
lower_html = html.lower()
if "detected unusual traffic" in lower_html or "captcha" in lower_html:
print(f"CAPTCHA rilevato per '{keyword}' a start={start}")
break
page_results = parse_organic_results(
html, base_position=len(all_results)
)
all_results.extend(page_results)
# Pausa tra le pagine per evitare rate limiting
time.sleep(2)
# Trova la posizione del dominio target
target_pos = None
target_url = None
for result in all_results:
if target_domain in result.url:
target_pos = result.position
target_url = result.url
break
return {
"keyword": keyword,
"target_domain": target_domain,
"country": country,
"device": device,
"position": target_pos,
"url": target_url,
"captured_at": datetime.now(timezone.utc).isoformat(),
"total_results_parsed": len(all_results),
}
Persistenza: SQLite e CSV
Per accumulare history giornaliera, SQLite è la scelta più semplice — zero configurazione, file singolo, query SQL complete:
import sqlite3
import csv
def init_db(db_path: str = "ranktracker.db") -> sqlite3.Connection:
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,
device TEXT NOT NULL,
position INTEGER,
url TEXT,
captured_at TEXT NOT NULL,
total_results INTEGER
)
""")
conn.execute("""
CREATE INDEX IF NOT EXISTS idx_kw_domain_date
ON rankings(keyword, target_domain, captured_at)
""")
conn.commit()
return conn
def save_ranking(conn: sqlite3.Connection, ranking: dict) -> None:
conn.execute("""
INSERT INTO rankings
(keyword, target_domain, country, device, position, url, captured_at, total_results)
VALUES
(:keyword, :target_domain, :country, :device, :position, :url, :captured_at, :total_results)
""", ranking)
conn.commit()
def export_csv(conn: sqlite3.Connection, output_path: str = "rankings.csv") -> None:
cursor = conn.execute("SELECT * FROM rankings ORDER BY captured_at DESC")
columns = [desc[0] for desc in cursor.description]
with open(output_path, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(columns)
writer.writerows(cursor.fetchall())
print(f"Esportato in {output_path}")
Production hardening: retry, backoff e concorrenza
In produzione, le richieste SERP falliscono per timeout, CAPTCHA o blocchi temporanei. Un sistema robusto richiede:
- Retry con backoff esponenziale: riprova dopo 2s, 4s, 8s, max 3 tentativi.
- Rilevamento CAPTCHA: interrompi la paginazione e cambia sessione/IP.
- Pool per paese: mantieni sessioni sticky per keyword ma ruota IP tra keyword diverse.
- Limite di concorrenza: max 5-10 richieste simultanee per evitare ban.
- Smoothing della volatilità: usa media mobile a 7 giorni per ridurre il rumore.
import time
import random
from functools import wraps
def retry_with_backoff(max_retries: int = 3, base_delay: float = 2.0):
"""Decorator per retry con backoff esponenziale e jitter."""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
last_exc = None
for attempt in range(max_retries):
try:
return func(*args, **kwargs)
except Exception as exc:
last_exc = exc
if attempt < max_retries - 1:
delay = base_delay * (2 ** attempt) + random.uniform(0, 1)
print(f"Tentativo {attempt+1} fallito ({exc}), retry in {delay:.1f}s")
time.sleep(delay)
raise last_exc
return wrapper
return decorator
@retry_with_backoff(max_retries=3, base_delay=2.0)
def fetch_serp_page_retry(keyword, country, city, start=0, device="desktop"):
return fetch_serp_page(keyword, country, city, start, device)
# Smoothing con media mobile a 7 giorni
def rolling_avg(conn: sqlite3.Connection, keyword: str, domain: str, window: int = 7):
"""Calcola la media mobile delle posizioni degli ultimi `window` giorni."""
cursor = conn.execute("""
SELECT position FROM rankings
WHERE keyword = ? AND target_domain = ? AND position IS NOT NULL
ORDER BY captured_at DESC LIMIT ?
""", (keyword, domain, window))
positions = [row[0] for row in cursor.fetchall() if row[0] is not None]
if not positions:
return None
return sum(positions) / len(positions)
Per la concorrenza, usa ThreadPoolExecutor con un limite di worker:
from concurrent.futures import ThreadPoolExecutor, as_completed
def track_keywords_batch(
keywords: list,
target_domain: str,
country: str = "US",
city: str = "chicago",
max_workers: int = 5,
) -> list:
"""Traccia multiple keyword in parallelo con limite di concorrenza."""
results = []
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = {
executor.submit(track_keyword, kw, target_domain, country, city): kw
for kw in keywords
}
for future in as_completed(futures):
kw = futures[future]
try:
result = future.result()
results.append(result)
print(f"[{kw}] posizione: {result['position']}")
except Exception as exc:
print(f"[{kw}] errore: {exc}")
results.append({"keyword": kw, "error": str(exc)})
return results
Configurazione ProxyHat multi-paese
ProxyHat supporta geo-targeting a livello paese e città tramite flag nel username. Per il rank tracking, la configurazione consigliata è:
- Sessione sticky per keyword: usa
-session-{keyword_id}nel username per mantenere lo stesso IP durante tutta la paginazione. - Geo-targeting paese: usa
-country-{ISO}per controllare la località dei risultati. - Geo-targeting città: usa
-city-{nome}per SEO locale. - Rotazione tra keyword: cambia sessione per ogni keyword per distribuire il carico su IP diversi.
# USA - Chicago (HTTP)
proxy_us = "http://user-country-US-city-chicago-session-kw001:pass@gate.proxyhat.com:8080"
# Germania - Berlin (HTTP)
proxy_de = "http://user-country-DE-city-berlin-session-kw002:pass@gate.proxyhat.com:8080"
# Italia - Milano (SOCKS5)
proxy_it = "socks5://user-country-IT-city-milan-session-kw003:pass@gate.proxyhat.com:1080"
Per altri casi d'uso legati al web scraping, visita la pagina sui casi d'uso di web scraping e sul SERP tracking. La documentazione ProxyHat contiene la lista completa dei parametri supportati. Per stimare i costi in base al tuo volume, consulta il prezzi ProxyHat.
Etica e limiti
Il rank tracking automatizzato opera in un'area grigia. Linee guida pratiche:
- Traccia il tuo dominio e keyword pubbliche: evita di monitorare competitor in modo aggressivo.
- Rispetta i rate limit: mantieni 2-5 secondi di pausa tra le richieste. Google può servire 429 Too Many Requests se superi circa 50 richieste/minuto.
- Considera API ufficiali a basso volume: per meno di 100 keyword/giorno, il Custom Search API di Google offre 100 query/giorno gratuite senza proxy.
- Consulta robots.txt: anche se legalmente non vincolante per l'accesso HTTP, è buona pratica rispettarlo.
- GDPR e privacy: non raccogliere dati personali dai risultati SERP. Traccia solo posizioni e URL pubblici.
Punti chiave
- Proxy residenziali obbligatori: il fingerprinting TLS e la reputazione IP rendono i datacenter proxy inaffidabili per il rank tracking.
- Paginazione obbligatoria: dopo la rimozione di
num=100, servono 10 richieste paginate per coprire le prime 100 posizioni.- Sessione sticky per keyword: mantieni lo stesso IP durante la paginazione per evitare risultati inconsistenti.
- Snapshot giornalieri: la volatilità SERP richiede raccolta quotidiana con smoothing a media mobile a 7 giorni.
- Hardening essenziale: retry con backoff esponenziale, CAPTCHA detection, limite di concorrenza a 5-10 thread.





