Vous suivez des mots-clés pour vos clients ou votre propre site, et Google vous bloque après quelques dizaines de requêtes. Les outils SaaS de rank tracking coûtent cher et ne vous donnent pas le contrôle sur les paramètres. La solution : créer un tracker de positions Google en Python avec des proxies résidentiels que vous opérez vous-même. Ce guide vous montre comment construire un outil de suivi de positions Google en Python avec des proxies résidentiels, du modèle de données jusqu'au durcissement production.
Nous couvrons l'extraction des SERPs avec curl_cffi, la rotation d'IP via le gateway ProxyHat, le parsing des positions organiques, le stockage historique en SQLite, et les stratégies anti-blocage. Pour aller plus loin sur les cas d'usage, consultez notre guide sur le suivi de SERP.
Créer un Tracker de Positions Google en Python : Modèle de Données et Snapshots Quotidiens
Un tracker de positions repose sur un modèle simple mais structuré. Chaque entrée capture : le mot-clé recherché, le domaine cible, le pays (ex. US, FR), le device (desktop ou mobile), la position trouvée, et l'horodatage (captured_at). Ce modèle permet de comparer les positions dans le temps et d'identifier les tendances.
Pourquoi des snapshots quotidiens plutôt que des checks ponctuels ? Une seule vérification ne capture pas la volatilité des SERPs. Google teste différentes variations de résultats en continu. Un snapshot quotidien sur 30 jours révèle la position médiane, les écarts-type, et les sauts de position. C'est la différence entre savoir que vous êtes « en page 1 » et savoir que votre position oscille entre 4 et 12 avec une médiane à 7.
Schéma SQLite pour le stockage historique
CREATE TABLE IF NOT EXISTS rank_snapshots (
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,
found_url TEXT,
captured_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_keyword_domain
ON rank_snapshots(keyword, target_domain, country, device, captured_at);
Ce schéma stocke chaque snapshot individuellement. Pour analyser les tendances, vous interrogez l'historique par mot-clé et domaine. L'index composite accélère les requêtes temporelles. La documentation Python sqlite3 détaille les options avancées.
Récupérer les SERPs Google : Pagination et Parsing des Résultats Organiques
Google a récemment restreint l'usage du paramètre num=100 qui permettait d'obtenir 100 résultats en une seule requête. Désormais, pour couvrir le top 100, il faut paginer avec start=0, start=10, start=20 jusqu'à start=90, soit 10 requêtes par mot-clé. Cela multiplie le volume de requêtes et rend la gestion des proxies encore plus critique.
Le parsing doit isoler les résultats organiques et ignorer les annonces publicitaires, les encarts « People Also Ask », les images, et autres fonctionnalités SERP qui ne sont pas des positions organiques.
Exemple : récupération d'une page SERP avec curl_cffi
La bibliothèque curl_cffi permet d'imiter l'empreinte TLS de Chrome, ce qui réduit les chances de détection par fingerprinting JA3/JA4. Voici un exemple avec le proxy ProxyHat en HTTP :
from curl_cffi import requests
import time
class RateLimitError(Exception):
pass
def fetch_serp(keyword: str, country: str = "US", start: int = 0,
session_id: str = None) -> str:
"""Récupère une page de résultats Google via proxy résidentiel ProxyHat."""
# Construire le username avec geo-targeting et session sticky
username = f"user-country-{country}"
if session_id:
username += f"-session-{session_id}"
proxy_url = f"http://{username}:YOUR_PASSWORD@gate.proxyhat.com:8080"
url = "https://www.google.com/search"
params = {
"q": keyword,
"num": "10",
"start": str(start),
"hl": "en",
"gl": country.lower(),
}
headers = {
"Accept": "text/html,application/xhtml+xml",
"Accept-Language": "en-US,en;q=0.9",
}
response = requests.get(
url,
params=params,
headers=headers,
proxies={"http": proxy_url, "https": proxy_url},
impersonate="chrome",
timeout=30,
)
if response.status_code == 429:
raise RateLimitError("Rate limit hit — back off")
if response.status_code != 200:
raise RuntimeError(f"HTTP {response.status_code}")
return response.text
Avec impersonate="chrome", curl_cffi reproduit l'empreinte TLS de Chrome, incluant l'ordre des cipher suites et les extensions TLS. Cela évite que Google détecte une empreinte Python/urllib immédiatement. Les en-têtes HTTP documentés sur MDN rappellent l'importance d'un User-Agent cohérent.
Exemple curl brut équivalent
curl -x "http://user-country-US-session-kw123:PASSWORD@gate.proxyhat.com:8080" \\
-H "Accept: text/html" \\
-H "Accept-Language: en-US,en;q=0.9" \\
"https://www.google.com/search?q=seo+tools&num=10&start=0&hl=en&gl=us"
Pourquoi les Proxies Résidentiels avec Geo-Targeting Sont Indispensables
Google emploie plusieurs couches de détection : fingerprinting TLS (JA3/JA4), scoring de réputation IP, et détection de patterns de requêtes. Les proxies datacenter ont des plages IP identifiables (AWS, OVH, DigitalOcean) que Google flag systématiquement. Les proxies résidentiels utilisent des IP d'ISP réels, indissociables du trafic d'un utilisateur normal.
Le geo-targeting au niveau ville est crucial : Google personnalise les SERPs selon la localisation de l'utilisateur. Pour suivre un mot-clé ciblant Chicago, vous devez utiliser une IP de Chicago, pas une IP générique américaine. ProxyHat permet cela directement dans le username :
# IP résidentielle à Chicago, session sticky par mot-clé
proxy_url = "http://user-country-US-city-chicago-session-seo01:PASSWORD@gate.proxyhat.com:8080"
# IP résidentielle à Berlin
proxy_url = "http://user-country-DE-city-berlin-session-seo02:PASSWORD@gate.proxyhat.com:8080"
La session sticky (-session-xxx) maintient la même IP pour toutes les requêtes d'un même mot-clé pendant la session. Cela évite que Google voie 10 IP différentes pour 10 pages consécutives du même mot-clé, ce qui serait suspect.
Comparaison des types de proxies pour le rank tracking
| Critère | Datacenter | Résidentiel | Mobile |
|---|---|---|---|
| Risque de blocage Google | Élevé (90%+) | Faible (<5%) | Très faible (<2%) |
| Geo-targeting ville | Non | Oui | Oui |
| Latence moyenne | ~50ms | ~200-500ms | ~300-800ms |
| Coût relatif | Bas | Moyen | Élevé |
| Recommandé pour SERP | Non | Oui | Optionnel |
Pour le rank tracking, les proxies résidentiels offrent le meilleur rapport qualité-prix. Les proxies mobiles sont plus sûrs mais plus chers et plus lents. Voir notre tarification pour les détails.
Exemple Complet : Parsing des Positions et Stockage SQLite
Maintenant, assemblons tout : fetch SERP, parsing des positions organiques, et stockage en SQLite. Pour le parsing, nous utilisons BeautifulSoup avec des sélecteurs CSS qui ciblent les conteneurs de résultats organiques.
from bs4 import BeautifulSoup
import sqlite3
import time
def parse_organic_results(html: str, target_domain: str) -> int | None:
"""Extrait la position du domaine cible depuis le HTML SERP."""
soup = BeautifulSoup(html, "html.parser")
results = soup.select("div.g")
position = None
for idx, result in enumerate(results, start=1):
link = result.select_one("a[href]")
if not link:
continue
href = link.get("href", "")
if not href.startswith("http"):
continue
if target_domain in href:
position = idx
break
return position
def store_snapshot(db_path: str, keyword: str, domain: str,
country: str, device: str, position: int | None):
"""Insère un snapshot de position dans SQLite."""
conn = sqlite3.connect(db_path)
conn.execute("""
INSERT INTO rank_snapshots
(keyword, target_domain, country, device, position, found_url)
VALUES (?, ?, ?, ?, ?, ?)
""", (keyword, domain, country, device, position, None))
conn.commit()
conn.close()
def track_keyword(db_path: str, keyword: str, domain: str,
country: str = "US", device: str = "desktop"):
"""Suit un mot-clé sur les 100 premiers résultats (10 pages)."""
session_id = f"kw-{hash(keyword) % 100000}"
position_found = None
for start in range(0, 100, 10):
try:
html = fetch_serp(keyword, country, start, session_id)
pos = parse_organic_results(html, domain)
if pos is not None:
position_found = start + pos
break
time.sleep(2)
except Exception as e:
print(f"Erreur page start={start}: {e}")
continue
store_snapshot(db_path, keyword, domain, country, device, position_found)
print(f"{keyword}: position {position_found}")
Ce code parcourt jusqu'à 10 pages de résultats. Dès qu'il trouve le domaine cible, il enregistre la position absolue (page × 10 + position dans la page) et s'arrête. Si le domaine n'apparaît pas dans le top 100, la position est None.
Export CSV de l'historique
import csv
def export_history_csv(db_path: str, output_path: str):
"""Exporte tout l'historique de positions en CSV."""
conn = sqlite3.connect(db_path)
cursor = conn.execute("""
SELECT keyword, target_domain, country, device, position, captured_at
FROM rank_snapshots
ORDER BY keyword, captured_at
""")
with open(output_path, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["keyword", "domain", "country", "device",
"position", "captured_at"])
writer.writerows(cursor.fetchall())
conn.close()
Durcissement Production : Retries, Détection CAPTCHA, Concurrence
En production, votre tracker doit gérer les échecs gracieusement. Voici les patterns essentiels.
Retries avec backoff exponentiel et détection CAPTCHA
import random
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("rank_tracker")
def fetch_with_retry(keyword: str, country: str, start: int,
max_retries: int = 3) -> str:
"""Fetch SERP avec retries et backoff exponentiel."""
for attempt in range(max_retries):
try:
html = fetch_serp(keyword, country, start)
if is_captcha(html):
logger.warning(f"CAPTCHA détecté pour '{keyword}' start={start}")
new_session = f"retry-{attempt}-{random.randint(0, 99999)}"
html = fetch_serp(keyword, country, start, new_session)
if is_captcha(html):
raise RuntimeError("CAPTCHA persistant")
return html
except RateLimitError:
wait = (2 ** attempt) + random.uniform(0, 1)
logger.info(f"Rate limit, retry dans {wait:.1f}s")
time.sleep(wait)
except Exception as e:
wait = (2 ** attempt) + random.uniform(0, 1)
logger.error(f"Erreur tentative {attempt+1}: {e}")
time.sleep(wait)
raise RuntimeError(f"Échec après {max_retries} tentatives pour '{keyword}'")
def is_captcha(html: str) -> bool:
"""Détecte les pages CAPTCHA de Google."""
signals = [
"unusual traffic from your computer network",
"detected unusual traffic",
"g-recaptcha",
"recaptcha",
]
html_lower = html.lower()
return any(s.lower() in html_lower for s in signals)
Concurrence avec ThreadPoolExecutor
Pour suivre 500 mots-clés, vous ne pouvez pas tout faire séquentiellement. Utilisez un pool de threads avec une limite stricte. Google tolère environ 10-20 requêtes simultanées par IP, mais avec des proxies résidentiels rotatifs, vous pouvez monter à 20-30 threads. Restez conservateur :
from concurrent.futures import ThreadPoolExecutor, as_completed
import threading
db_lock = threading.Lock()
def track_keyword_threaded(db_path: str, keyword: str, domain: str,
country: str = "US"):
"""Version thread-safe de track_keyword."""
session_id = f"kw-{hash(keyword) % 100000}"
position_found = None
for start in range(0, 100, 10):
try:
html = fetch_with_retry(keyword, country, start)
pos = parse_organic_results(html, domain)
if pos is not None:
position_found = start + pos
break
time.sleep(1.5)
except Exception as e:
logger.error(f"Échec '{keyword}' start={start}: {e}")
continue
with db_lock:
store_snapshot(db_path, keyword, domain, country,
"desktop", position_found)
def batch_track(keywords: list[str], domain: str, country: str = "US",
max_workers: int = 10):
"""Suit plusieurs mots-clés en parallèle."""
db_path = "rank_tracker.db"
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = {
executor.submit(track_keyword_threaded,
db_path, kw, domain, country): kw
for kw in keywords
}
for future in as_completed(futures):
kw = futures[future]
try:
future.result()
except Exception as e:
logger.error(f"Mot-clé '{kw}' a échoué: {e}")
Lissage de la volatilité des positions
Les positions Google fluctuent naturellement. Pour éviter de réagir à du bruit, appliquez une moyenne mobile sur 7 jours :
def get_smoothed_position(db_path: str, keyword: str, domain: str,
window: int = 7) -> float | None:
"""Calcule la position moyenne sur les N derniers jours."""
conn = sqlite3.connect(db_path)
cursor = conn.execute("""
SELECT position FROM rank_snapshots
WHERE keyword = ? AND target_domain = ?
ORDER BY captured_at DESC LIMIT ?
""", (keyword, domain, window))
positions = [row[0] for row in cursor.fetchall() if row[0] is not None]
conn.close()
if not positions:
return None
return sum(positions) / len(positions)
Une moyenne mobile sur 7 jours lisse les fluctuations journalières et révèle les tendances réelles. Si la position médiane passe de 7 à 5 sur une semaine, c'est un signal fiable. Un saut isolé de 7 à 3 puis retour à 7 n'est probablement que du bruit.
Éthique et Limites du Rank Tracking
Le scraping de SERPs Google soulève des questions légales et éthiques. Quelques principes :
- Respectez le
robots.txtde Google. Consultez les directives robots.txt de Google pour connaître les restrictions de crawling. - Limitez votre volume. Quelques centaines de mots-clés par jour avec des pauses entre les requêtes est raisonnable. Des milliers de requêtes par minute ressemblent à une attaque.
- Préférez une source officielle à bas volume. L'API Custom Search de Google offre 100 requêtes gratuites par jour. Pour un suivi limité, c'est plus fiable et conforme que le scraping.
- Trackez vos propres positions et des données publiques. Ne scrapez pas pour construire un service concurrent de Google Search.
- Conformité RGPD/CCPA. Le rank tracking ne collecte pas de données personnelles, mais si vous stockez des données utilisateurs (comptes clients), assurez-vous d'être conforme. Consultez les guides RGPD pour plus d'informations.
Pour un volume modéré (jusqu'à ~500 mots-clés/jour), le scraping avec proxies résidentiels est viable. Au-delà, envisagez une API SERP tierce ou l'API officielle de Google. Pour en savoir plus, consultez notre guide sur le web scraping et le suivi de SERP.
Points Clés à Retenir
- Modèle de données : stockez mot-clé, domaine, pays, device, position et timestamp pour chaque snapshot quotidien.
- Pagination : paginez avec
start=0,10,20...pour couvrir le top 100, une page à la fois.- Proxies résidentiels obligatoires : les proxies datacenter sont détectés par Google dans 90%+ des cas. Utilisez des IP résidentielles avec geo-targeting ville.
- Session sticky par mot-clé : gardez la même IP pour toutes les pages d'un même mot-clé via
-session-xxx.- curl_cffi avec
impersonate="chrome": reproduit l'empreinte TLS de Chrome et réduit la détection.- Durcissement : retries avec backoff exponentiel, détection CAPTCHA, concurrence limitée à 10-20 threads, et lissage par moyenne mobile sur 7 jours.
- Éthique : respectez robots.txt, limitez le volume, et préférez l'API officielle pour les petits volumes.
Prêt à suivre vos positions ? Consultez notre tarification pour choisir le plan de proxies résidentiels adapté à votre volume de tracking. Pour la documentation technique complète, visitez docs.proxyhat.com.





