Se hai mai provato a costruire un Google Rank Tracker in Python con proxy residenziali, sai quanto sia frustrante: dopo 20 richieste Google restituisce CAPTCHA, la paginazione si rompe, e le posizioni oscillano del 30% da un giorno all'altro. Questa guida mostra come costruire un tracker production-ready che supera i blocchi di Google, usa fingerprinting TLS realistico e memorizza storico posizioni in SQLite.
Google ha rimosso il parametro num=100 a settembre 2025, rendendo la paginazione SERP obbligatoria. In combinazione con fingerprinting TLS/JA3-JA4 e scoring reputazione IP, questo rende i proxy datacenter quasi inutilizzabili per il rank tracking. La soluzione: proxy residenziali con geo-targeting città, sticky session per keyword, e un client HTTP che impersona Chrome.
Costruire un Google Rank Tracker in Python: il contesto tecnico
Un rank tracker Google deve risolvere tre problemi simultanei:
- Raccolta SERP: scaricare i risultati organici per una keyword in un dato paese e device.
- Parsing posizioni: estrarre la posizione del dominio target dai risultati, saltando annunci e SERP features.
- Storico temporale: memorizzare snapshot giornalieri per rilevare trend e volatilità.
Il problema esiste perché Google non offre un'API pubblica gratuita per SERP. L'API Custom Search di Google costa $5 per 1.000 query e non replica esattamente i risultati organici. Per volumi elevati o tracking multi-keyword, lo scraping resta l'unica via economica.
Google ha rimosso num=100 a settembre 2025: ora la SERP mostra 10 risultati per pagina, e devi paginare con start=0,10,20,…,90 per coprire i primi 100. Ogni pagina aggiuntiva aumenta il rischio di CAPTCHA e richiede un IP pulito.
Perché gli snapshot giornalieri battono i controlli one-off
Un controllo one-off ti dice la posizione attuale, ma non la volatilità. Una keyword che passa dalla posizione 3 alla 12 e poi torna a 4 in 3 giorni segnala instabilità che un singolo snapshot nasconde. Con snapshot giornalieri puoi calcolare:
- Posizione media su 7/30 giorni.
- Deviazione standard per rilevare volatilità anomala.
- Trend con regressione lineare semplice.
Per un'agenzia SEO con 500 keyword, 1 snapshot/giorno = 15.000 richieste/mese. A questo volume, la qualità del proxy determina il successo o il fallimento.
Modello dati: keyword, dominio, paese, device, posizione, timestamp
Il modello dati deve essere semplice ma sufficiente per analisi storica. Una singola tabella SQLite basta per la maggior parte dei casi:
import sqlite3
DB_PATH = "rank_tracker.db"
def init_db(db_path: str = DB_PATH) -> None:
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 DEFAULT 'US',
device TEXT NOT NULL DEFAULT 'desktop',
position INTEGER,
page INTEGER,
result_url TEXT,
captured_at TEXT NOT NULL,
UNIQUE(keyword, target_domain, country, device, captured_at)
)
""")
conn.execute("""
CREATE INDEX IF NOT EXISTS idx_keyword_date
ON rankings(keyword, captured_at)
""")
conn.commit()
conn.close()
def insert_ranking(db_path: str, keyword: str, domain: str,
country: str, device: str, position: int,
page: int, url: str, captured_at: str) -> None:
conn = sqlite3.connect(db_path)
conn.execute("""
INSERT OR REPLACE INTO rankings
(keyword, target_domain, country, device, position, page, result_url, captured_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""", (keyword, domain, country, device, position, page, url, captured_at))
conn.commit()
conn.close()
La chiave UNIQUE su (keyword, target_domain, country, device, captured_at) evita duplicati se lo stesso job gira due volte. L'indice su (keyword, captured_at) rende le query di storico veloci anche con milioni di righe.
Perché i proxy residenziali sono obbligatori nel 2026
Google usa tre livelli di difesa anti-bot:
- Fingerprinting TLS/JA3-JA4: il server Google analizza il handshake TLS del client. JA3 genera un hash univoco basato su cipher suites, estensioni e curve. Le librerie Python standard (urllib3, requests) hanno fingerprint diversi da Chrome e vengono marcate come sospette.
- Scoring reputazione IP: ogni IP ha un punteggio basato su ASN, storico abuso, tipo (datacenter vs residenziale). Range datacenter noti (AWS, DigitalOcean) hanno reputazione bassa.
- Rate limiting comportamentale: troppe richieste dallo stesso IP in poco tempo innescano CAPTCHA o soft-ban temporanei.
I proxy residenziali risolvono il secondo problema: usano IP di ISP reali (Comcast, AT&T, Vodafone) con reputazione alta. Combinati con curl_cffi che impersona Chrome a livello TLS, superano i primi due livelli. Il terzo livello si gestisce con rotazione IP e sticky session.
Geo-targeting città e sticky session
Per il rank tracking, il geo-targeting è critico: i risultati Google a New York differiscono da quelli a Chicago per keyword locali. ProxyHat supporta geo-targeting paese e città nel username:
| Parametro | Formato username | Caso d'uso |
|---|---|---|
| Paese | user-country-US | Tracking nazionale |
| Paese + città | user-country-US-city-chicago | Tracking locale |
| Sticky session | user-session-abc123 | Mantiene stesso IP per paginazione |
| Combinato | user-country-US-city-chicago-session-abc123 | Tracking locale + IP stabile |
La sticky session è essenziale per il rank tracking: se l'IP cambia a metà paginazione (da start=0 a start=90), Google può mostrare risultati diversi perché il nuovo IP ha un'altra geo-locazione. Con -session-abc123, ProxyHat mantiene lo stesso IP per tutta la durata della sessione.
Esempio pratico: curl_cffi + ProxyHat per rank tracking
Installazione delle dipendenze:
pip install curl_cffi beautifulsoup4 lxml
pip install proxyhat # SDK opzionale
1. Test rapido con curl
# Test proxy residenziale ProxyHat con curl
curl -x "http://user-country-US-city-chicago-session-rank001:pass@gate.proxyhat.com:8080" \
"https://www.google.com/search?q=python+tutorial&num=10&start=0" \
-H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
--compressed
2. Fetch SERP con curl_cffi e impersonazione Chrome
from curl_cffi import requests as cffi_requests
from urllib.parse import quote_plus
import time
def build_proxy_url(country: str, city: str, session_id: str) -> str:
"""Costruisce l'URL proxy ProxyHat con geo-targeting e sticky session."""
username = f"user-country-{country}-city-{city}-session-{session_id}"
return f"http://{username}:pass@gate.proxyhat.com:8080"
def fetch_serp_page(keyword: str, start: int, country: str = "US",
city: str = "chicago", session_id: str = "kw1") -> str:
"""Scarica una pagina SERP (10 risultati) con curl_cffi impersonando Chrome."""
proxy_url = build_proxy_url(country, city, session_id)
q = quote_plus(keyword)
url = f"https://www.google.com/search?q={q}&num=10&start={start}&hl=en&gl={country.lower()}"
response = cffi_requests.get(
url,
impersonate="chrome",
proxies={"http": proxy_url, "https": proxy_url},
timeout=30,
)
response.raise_for_status()
return response.text
# Esempio: scarica prima pagina per "python tutorial"
html = fetch_serp_page("python tutorial", start=0, country="US", city="chicago", session_id="py001")
print(f"HTML length: {len(html)} bytes")
Il parametro impersonate="chrome" fa sì che curl_cffi replichi il fingerprint TLS di Chrome 120+, incluse cipher suites e estensioni. Questo inganna il controllo JA3/JA4 di Google.
3. Parsing posizioni organiche con BeautifulSoup
from bs4 import BeautifulSoup
import re
def parse_organic_results(html: str, target_domain: str) -> dict:
"""Estrae risultati organici e trova la posizione del dominio target.
Salta annunci (sponsor), SERP features (people also ask, maps, video).
Restituisce {'position': int|None, 'page': int, 'url': str|None}.
"""
soup = BeautifulSoup(html, "lxml")
# Selettore CSS per risultati organici: div.g con href che inizia con /url o http
organic_divs = soup.select("div.g")
position = None
result_url = None
rank_counter = 0
for div in organic_divs:
link = div.select_one("a[href]")
if not link:
continue
href = link.get("href", "")
# Salta ads (href contiene googleads o google.com/aclk)
if "googleads" in href or "/aclk" in href:
continue
# Salta SERP features (video, maps, images)
if "google.com/maps" in href or "youtube.com/watch" in href:
continue
rank_counter += 1
if target_domain in href:
position = rank_counter
result_url = href
break
return {
"position": position,
"page": (position - 1) // 10 + 1 if position else None,
"url": result_url,
}
# Esempio: trova posizione di python.org per "python tutorial"
result = parse_organic_results(html, "python.org")
print(f"Position: {result['position']}, Page: {result['page']}")
4. Paginazione top-100 con sticky session
from datetime import datetime, timezone
def track_keyword(keyword: str, target_domain: str, country: str = "US",
city: str = "chicago", max_results: int = 100) -> dict:
"""Traccia una keyword paginando fino a max_results (10 pagine da 10 risultati).
Usa una sticky session per keyword per mantenere lo stesso IP.
"""
session_id = f"kw-{hash(keyword) % 100000}"
found_position = None
found_url = None
found_page = None
for start in range(0, max_results, 10):
try:
html = fetch_serp_page(keyword, start, country, city, session_id)
result = parse_organic_results(html, target_domain)
if result["position"]:
# Aggiungi offset della pagina corrente
found_position = result["position"] + start
found_url = result["url"]
found_page = start // 10 + 1
break
time.sleep(2) # Delay tra pagine per evitare rate limit
except Exception as e:
print(f"Errore a start={start}: {e}")
continue
captured_at = datetime.now(timezone.utc).isoformat()
return {
"keyword": keyword,
"target_domain": target_domain,
"country": country,
"device": "desktop",
"position": found_position,
"page": found_page,
"result_url": found_url,
"captured_at": captured_at,
}
# Tracking completo: salva in SQLite
ranking = track_keyword("python tutorial", "python.org", country="US", city="chicago")
insert_ranking(DB_PATH, **ranking)
print(f"Saved: {ranking['position']} at {ranking['captured_at']}")
5. Uso del ProxyHat SDK per rotazione automatica
from proxyhat import ProxyClient
import time
# Inizializza client ProxyHat
client = ProxyClient(username="user", password="pass")
def fetch_with_sdk(keyword: str, country: str, city: str, session_id: str) -> str:
"""Fetch SERP usando il ProxyHat SDK per rotazione IP automatica."""
proxy = client.get_proxy(
proxy_type="residential",
country=country,
city=city,
session_id=session_id,
)
proxy_url = f"http://{proxy.username}:{proxy.password}@{proxy.host}:{proxy.port}"
q = quote_plus(keyword)
url = f"https://www.google.com/search?q={q}&num=10&hl=en&gl={country.lower()}"
response = cffi_requests.get(
url,
impersonate="chrome",
proxies={"http": proxy_url, "https": proxy_url},
timeout=30,
)
response.raise_for_status()
return response.text
# Rotazione IP automatica per keyword diverse
keywords = ["python tutorial", "flask docs", "django tutorial"]
for kw in keywords:
sid = f"sess-{kw.replace(' ', '-')}"
html = fetch_with_sdk(kw, "US", "chicago", sid)
result = parse_organic_results(html, "python.org")
print(f"{kw}: position={result['position']}")
time.sleep(3)
Production hardening: retry, CAPTCHA, concorrenza
Un rank tracker production-ready deve gestire fallimenti gracefully. Ecco i pattern essenziali:
Retry con exponential backoff
import time
import random
from curl_cffi import requests as cffi_requests
def fetch_with_retry(url: str, proxy_url: str, max_retries: int = 3) -> str:
"""Fetch con retry esponenziale e jitter.
Ritenta su errori di rete e 429/503.
"""
for attempt in range(max_retries):
try:
response = cffi_requests.get(
url,
impersonate="chrome",
proxies={"http": proxy_url, "https": proxy_url},
timeout=30,
)
if response.status_code == 200:
return response.text
elif response.status_code in (429, 503):
wait = (2 ** attempt) + random.uniform(0, 1)
print(f"Rate limited, waiting {wait:.1f}s...")
time.sleep(wait)
else:
response.raise_for_status()
except Exception as e:
wait = (2 ** attempt) + random.uniform(0, 1)
print(f"Attempt {attempt+1} failed: {e}, retry in {wait:.1f}s")
time.sleep(wait)
raise RuntimeError(f"Failed after {max_retries} retries")
Rilevamento CAPTCHA
def detect_captcha(html: str) -> bool:
"""Rileva CAPTCHA Google nella risposta HTML."""
captcha_signals = [
"Our systems have detected unusual traffic",
"unusual traffic from your computer network",
"recaptcha",
"g-recaptcha",
"sorry/index",
]
html_lower = html.lower()
return any(signal.lower() in html_lower for signal in captcha_signals)
def fetch_serp_safe(keyword: str, start: int, country: str, city: str,
session_id: str) -> str:
"""Fetch con rilevamento CAPTCHA e pausa prolungata."""
proxy_url = build_proxy_url(country, city, session_id)
q = quote_plus(keyword)
url = f"https://www.google.com/search?q={q}&num=10&start={start}&hl=en&gl={country.lower()}"
html = fetch_with_retry(url, proxy_url)
if detect_captcha(html):
print("CAPTCHA detected, pausing 120s...")
time.sleep(120)
# Cambia session per ottenere nuovo IP
new_sid = f"{session_id}-retry-{int(time.time())}"
proxy_url = build_proxy_url(country, city, new_sid)
html = fetch_with_retry(url, proxy_url)
if detect_captcha(html):
raise RuntimeError("CAPTCHA persistente, blocca keyword")
return html
Concorrenza limitata con ThreadPoolExecutor
from concurrent.futures import ThreadPoolExecutor, as_completed
import threading
# Limite globale per non saturare i proxy
global_semaphore = threading.Semaphore(5)
def track_keyword_safe(keyword: str, domain: str, country: str, city: str) -> dict:
"""Tracking con semaforo per limitare concorrenza a 5 richieste simultanee."""
with global_semaphore:
try:
ranking = track_keyword(keyword, domain, country, city)
insert_ranking(DB_PATH, **ranking)
return ranking
except Exception as e:
print(f"Failed tracking '{keyword}': {e}")
return None
def batch_track(keywords: list, domain: str, country: str = "US",
city: str = "chicago", max_workers: int = 5) -> list:
"""Batch tracking con concorrenza limitata."""
results = []
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = {
executor.submit(track_keyword_safe, kw, domain, country, city): kw
for kw in keywords
}
for future in as_completed(futures):
result = future.result()
if result:
results.append(result)
return results
# Esempio: traccia 20 keyword con 5 worker simultanei
keywords = ["python tutorial", "flask tutorial", "django tutorial", "fastapi guide"]
results = batch_track(keywords, "python.org", max_workers=5)
print(f"Tracked {len(results)}/{len(keywords)} keywords")
Smoothing volatilità rank
import statistics
def get_rank_history(db_path: str, keyword: str, domain: str, days: int = 7) -> list:
"""Recupera storico posizioni degli ultimi N giorni."""
conn = sqlite3.connect(db_path)
rows = conn.execute("""
SELECT position, captured_at FROM rankings
WHERE keyword = ? AND target_domain = ?
ORDER BY captured_at DESC
LIMIT ?
""", (keyword, domain, days)).fetchall()
conn.close()
return [r[0] for r in rows if r[0] is not None]
def compute_rank_metrics(positions: list) -> dict:
"""Calcola media, deviazione standard, trend."""
if not positions:
return {"avg": None, "stdev": None, "trend": None}
avg = statistics.mean(positions)
stdev = statistics.stdev(positions) if len(positions) > 1 else 0.0
# Trend semplice: differenza tra primo e ultimo
trend = positions[-1] - positions[0] if len(positions) > 1 else 0
return {
"avg": round(avg, 1),
"stdev": round(stdev, 2),
"trend": trend, # negativo = miglioramento (posizione più bassa = meglio)
}
# Esempio: analizza volatilità di una keyword
history = get_rank_history(DB_PATH, "python tutorial", "python.org", days=7)
metrics = compute_rank_metrics(history)
print(f"Avg: {metrics['avg']}, StDev: {metrics['stdev']}, Trend: {metrics['trend']}")
Una deviazione standard superiore a 3.0 indica volatilità anomala: potrebbe significare che Google sta testando SERP diverse o che il dominio è in una zona di transizione. In questi casi, conviene aumentare la frequenza di tracking a 2 volte al giorno.
Etica e limiti nel rank tracking
Lo scraping SERP esiste in un'area grigia. Linee guida pratiche:
- Traccia il tuo dominio: monitorare i propri ranking è legittimo e non viola ToS di Google nella maggior parte dei casi.
- Rispetta
robots.txt: Google permette lo scraping di/searchma con rate limit. Controlla sempre robots.txt prima di avviare un job. - Volume ragionevole: per poche keyword (sotto 50), considera l'API Custom Search di Google a $5 per 1.000 query. È più affidabile e non rischia ban.
- GDPR e CCPA: se memorizzi dati SERP che contengono informazioni personali (snippet con nomi), assicurati di avere una base legale e cancellare i dati dopo il periodo necessario.
- Rate limits: mantieni almeno 2-3 secondi tra richieste per keyword e non superare 10 richieste simultanee per pool proxy.
Per volumi enterprise (oltre 10.000 keyword/giorno), valuta provider SERP API specializzati come SerpAPI o DataForSEO: costano di più ma gestiscono proxy, parsing e compliance internamente.
ProxyHat: setup e configurazione
ProxyHat offre proxy residenziali con geo-targeting paese e città, sticky session, e rotazione automatica. Per il rank tracking Google, la configurazione consigliata è:
- Tipo proxy: residenziale (reputazione IP alta, supera fingerprinting).
- Geo-targeting: paese + città per tracking locale accurato.
- Sticky session: una session per keyword, TTL 30 minuti.
- Porta HTTP: 8080 (default), SOCKS5 su 1080 per uso specifico.
Consulta la documentazione ProxyHat per dettagli su pool disponibili, limiti di concorrenza e best practice. Verifica i piani su /it/pricing e le location disponibili su /it/locations.
Per use case correlati, vedi /it/use-cases/web-scraping e /it/use-cases/serp-tracking.
Punti chiave
1. Proxy residenziali sono obbligatori: i datacenter vengono bloccati in poche richieste per il rank tracking Google. Usa ProxyHat con geo-targeting città.
2. curl_cffi con impersonate="chrome": replica il fingerprint TLS di Chrome, superando JA3/JA4. requests/urllib3 non bastano nel 2026.
3. Sticky session per keyword: mantieni lo stesso IP durante la paginazione top-100 per evitare risultati inconsistenti.
4. Snapshot giornalieri in SQLite: memorizza posizione, page, URL e timestamp per calcolare volatilità e trend.
5. Hardening production: retry con backoff, rilevamento CAPTCHA, concorrenza limitata a 5-10 worker, delay 2-3s tra richieste.
6. Etica prima di tutto: traccia il tuo dominio, rispetta robots.txt, e usa API ufficiali per volumi bassi.
FAQ
Come costruire un Google Rank Tracker in Python con proxy residenziali?
Per costruire un rank tracker Google in Python con proxy residenziali serve un client HTTP con fingerprint TLS realistico (curl_cffi), un pool di proxy residenziali con geo-targeting città, una logica di paginazione SERP con start=0,10,20, e uno storage SQLite per storico posizioni. I proxy residenziali riducono blocchi IP e CAPTCHA rispetto ai datacenter, garantendo tassi di successo superiori al 90% anche con 500+ keyword giornaliere.
Perché i proxy residenziali sono importanti per un rank tracker Google?
Google applica fingerprinting TLS/JA3-JA4 e scoring reputazione IP. I proxy datacenter vengono bloccati rapidamente perché i loro range IP sono noti e a bassa reputazione. I proxy residenziali usano IP di ISP reali, quindi hanno reputazione più alta e superano meglio i controlli anti-bot, garantendo tassi di successo più elevati nel rank tracking.
Quale tipo di proxy funziona meglio per il rank tracking Google?
I proxy residenziali rotanti con sticky session per keyword sono la scelta ottimale per il rank tracking Google. Permettono geo-targeting a livello paese e città, mantengono lo stesso IP durante una sessione di paginazione, e offrono reputazione IP superiore ai datacenter. I mobile proxy sono un'alternativa valida per device mobile.
Come evitare blocchi quando si implementa un rank tracker Google in Python?
Per evitare blocchi: usa curl_cffi con impersonate="chrome" per fingerprint TLS realistico, ruota IP residenziali con sticky session per keyword, limita la concorrenza a 5-10 richieste simultanee, implementa retry con backoff esponenziale, rileva CAPTCHA e pausa 60-120 secondi, e rispetta rate limits con delay di 2-3 secondi tra richieste.




