La recherche de mots-clés ne se limite plus aux outils payants. Si vous savez scraper Google People Also Ask et l'endpoint Autocomplete, vous accédez à trois sources gratuites de données d'intention de recherche que la plupart des SEO ignorent. Ce guide vous montre comment construire un pipeline Python complet — du seed keyword à un fichier CSV de centaines de questions exploitable pour votre contenu et vos FAQ.
Pourquoi scraper Google People Also Ask et Autocomplete change la donne
Google expose publiquement trois mines d'or pour la recherche de mots-clés : l'Autocomplete (suggestions de recherche), les People Also Ask (PAA) et les Related Searches (recherches associées). Chacune révèle un niveau différent d'intention de recherche :
- Autocomplete — reflète l'intention de recherche avant le clic. Les suggestions sont triées par popularité et fraîcheur. Idéal pour découvrir des variantes long-tail.
- People Also Ask — révèle les questions que les utilisateurs se posent après une première recherche. Chaque accordéon contient une question, un snippet de réponse et une source citée.
- Related Searches — montre les requêtes adjacentes en bas de page. Utile pour élargir un thème ou identifier des cooccurrences sémantiques.
L'endpoint Autocomplete de Google est particulièrement intéressant car il renvoie du JSON natif via https://suggestqueries.google.com/complete/search?client=chrome&q=VOTRE_MOT. Pas de HTML à parser, pas de JavaScript à exécuter. Selon la documentation MDN sur CORS, cet endpoint accepte les requêtes cross-origin, ce qui le rend encore plus accessible.
Le contexte technique : pourquoi Google limite ces endpoints
L'endpoint Autocomplete de Google est conçu pour répondre en moins de 200ms à chaque frappe de touche dans la barre de recherche. Pour maintenir cette latence, Google applique des limites strictes par adresse IP. Si vous envoyez plus de ~100 requêtes/min depuis une seule IP, vous commencez à recevoir des réponses vides, des CAPTCHA ou des erreurs 429.
Le problème est encore plus subtil avec la locale bias : les suggestions Autocomplete et les questions PAA varient selon la géolocalisation de l'IP qui fait la requête. Une recherche « pizza » depuis une IP à Paris ne renverra pas les mêmes suggestions que depuis une IP à Berlin. Si vous voulez des suggestions localisées pour un marché allemand, vous devez utiliser une IP allemande.
C'est là que les proxies résidentiels entrent en jeu. Contrairement aux proxies datacenter qui sont facilement détectés par les systèmes anti-bot de Google, les proxies résidentiels utilisent de vraies adresses IP d'ISP. Avec ProxyHat, vous pouvez cibler un pays et une ville précis via le nom d'utilisateur :
# Proxy résidentiel allemand, Berlin
http://user-country-DE-city-berlin:pass@gate.proxyhat.com:8080
# Proxy résidentiel français, Paris
http://user-country-FR-city-paris:pass@gate.proxyhat.com:8080
# Session sticky pour réutiliser la même IP
http://user-country-US-session-abc123:pass@gate.proxyhat.com:8080
Pour en savoir plus sur les localisations disponibles et le tarification, consultez les pages dédiées.
Construire un expandeur de mots-clés : du seed au long-tail
Un seul mot-clé seed peut générer des centaines de variantes long-tail. La technique consiste à ajouter des préfixes alphabétiques (a-z) et des modificateurs sémantiques (comment, pourquoi, meilleur, prix, etc.) devant votre mot-clé, puis à interroger l'endpoint Autocomplete pour chaque combinaison.
Exemple : expansion avec préfixes a-z et modificateurs
import httpx
import string
import json
import time
from urllib.parse import quote
AUTOCOMPLETE_URL = "https://suggestqueries.google.com/complete/search"
PROXY = "http://user-country-FR-city-paris:pass@gate.proxyhat.com:8080"
MODIFIERS = [
"comment", "pourquoi", "quand", "ou", "quel", "quelle",
"meilleur", "prix", "gratuit", "avis", "difference", "guide"
]
def fetch_autocomplete(query: str, proxy: str = PROXY) -> list[str]:
"""Récupère les suggestions Autocomplete pour une requête."""
params = {
"client": "chrome",
"q": query,
"hl": "fr",
"gl": "fr"
}
try:
with httpx.Client(proxy=proxy, timeout=10.0) as client:
resp = client.get(AUTOCOMPLETE_URL, params=params)
resp.raise_for_status()
data = resp.json()
# data[1] contient la liste des suggestions
if isinstance(data, list) and len(data) > 1:
return data[1]
return []
except (httpx.HTTPError, json.JSONDecodeError) as e:
print(f"Erreur pour '{query}': {e}")
return []
def expand_seed(seed: str) -> list[str]:
"""Transforme un seed keyword en centaines de variantes."""
all_suggestions = set()
# 1. Préfixes alphabétiques : "a pizza", "b pizza", ...
for letter in string.ascii_lowercase:
query = f"{letter} {seed}"
suggestions = fetch_autocomplete(query)
all_suggestions.update(suggestions)
time.sleep(0.5) # throttle poli
# 2. Modificateurs : "comment pizza", "meilleur pizza", ...
for mod in MODIFIERS:
query = f"{mod} {seed}"
suggestions = fetch_autocomplete(query)
all_suggestions.update(suggestions)
time.sleep(0.5)
# 3. Suffixes alphabétques : "pizza a", "pizza b", ...
for letter in string.ascii_lowercase:
query = f"{seed} {letter}"
suggestions = fetch_autocomplete(query)
all_suggestions.update(suggestions)
time.sleep(0.5)
return sorted(all_suggestions)
if __name__ == "__main__":
results = expand_seed("proxy")
print(f"{len(results)} suggestions trouvées")
for s in results[:20]:
print(f" - {s}")
Avec 26 lettres × 2 (préfixe + suffixe) + 12 modificateurs = 64 requêtes, vous récupérez typiquement entre 300 et 600 suggestions uniques pour un seul seed keyword. C'est l'équivalent d'un mois de données Ubersuggest gratuit en 2 minutes.
Scraper les People Also Ask avec Playwright
Les questions PAA ne sont pas disponibles via un endpoint JSON. Elles sont rendues dans le HTML des résultats de recherche et nécessitent d'ouvrir les accordéons pour révéler les questions imbriquées. Chaque clic sur un accordéon PAA charge de nouvelles questions, ce qui permet une expansion récursive.
Voici un script Playwright qui ouvre les accordéons PAA de manière récursive et extrait les questions ainsi que leurs sources citées :
from playwright.sync_api import sync_playwright
import time
import json
import re
PROXY_SERVER = "gate.proxyhat.com:8080"
PROXY_USER = "user-country-FR-city-paris"
PROXY_PASS = "pass"
def scrape_paa(query: str, max_clicks: int = 10) -> list[dict]:
"""Scrape les questions PAA et leurs sources pour une requête."""
questions = []
with sync_playwright() as p:
browser = p.chromium.launch(
headless=True,
proxy={
"server": f"http://{PROXY_SERVER}",
"username": PROXY_USER,
"password": PROXY_PASS
}
)
context = browser.new_context(
locale="fr-FR",
viewport={"width": 1280, "height": 800}
)
page = context.new_page()
# Naviguer vers Google Search
page.goto(f"https://www.google.com/search?q={query}&hl=fr&gl=fr")
page.wait_for_timeout(2000)
clicks = 0
while clicks < max_clicks:
# Sélectionner les accordéons PAA non encore ouverts
paa_items = page.query_selector_all("div[jsname='CWE2Te']")
if not paa_items:
break
for item in paa_items:
try:
# Extraire le texte de la question
q_text = item.inner_text().strip()
if q_text and q_text not in [q["question"] for q in questions]:
questions.append({"question": q_text, "source": None})
# Cliquer pour ouvrir l'accordéon et révéler la source
item.click()
page.wait_for_timeout(800)
clicks += 1
# Extraire la source citée (lien dans le snippet de réponse)
source_link = page.query_selector("div[jsname='r5aAsf'] a")
if source_link:
href = source_link.get_attribute("href")
if questions:
questions[-1]["source"] = href
except Exception as e:
print(f"Erreur clic: {e}")
continue
time.sleep(1)
browser.close()
return questions
if __name__ == "__main__":
paa_results = scrape_paa("proxy résidentiel", max_clicks=15)
print(f"{len(paa_results)} questions PAA trouvées")
for q in paa_results[:10]:
print(f" Q: {q['question']}")
print(f" Source: {q['source']}")
print()
Chaque accordéon ouvert révèle généralement 3 à 4 nouvelles questions, ce qui signifie qu'avec 15 clics vous pouvez récupérer 40 à 60 questions uniques pour une seule requête. Les sources citées sont précieuses : elles vous indiquent quels sites Google considère comme faisant autorité sur le sujet.
Scraper les Related Searches
Les Related Searches sont les plus simples à extraire — elles se trouvent en bas de la page de résultats dans une section identifiable. Voici comment les récupérer avec httpx :
import httpx
from selectolax.parser import HTMLParser
PROXY = "http://user-country-FR-city-paris:pass@gate.proxyhat.com:8080"
def scrape_related_searches(query: str) -> list[str]:
"""Récupère les Related Searches depuis une page de résultats Google."""
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Accept-Language": "fr-FR,fr;q=0.9"
}
url = f"https://www.google.com/search?q={query}&hl=fr&gl=fr"
try:
with httpx.Client(proxy=PROXY, headers=headers, timeout=15.0) as client:
resp = client.get(url)
resp.raise_for_status()
tree = HTMLParser(resp.text)
related = []
# Les Related Searches sont dans des liens avec /search?q= en bas de page
for node in tree.css("a"):
href = node.attributes.get("href", "")
if "/search?q=" in href and "spell=" not in href:
text = node.text(strip=True)
if text and len(text) > 3 and text not in related:
related.append(text)
return related[:20] # Google affiche généralement ~8-12
except httpx.HTTPError as e:
print(f"Erreur: {e}")
return []
if __name__ == "__main__":
related = scrape_related_searches("proxy résidentiel")
print(f"{len(related)} Related Searches:")
for r in related:
print(f" - {r}")
Pipeline complet : asynchrone avec rotation de proxies
Pour traiter un grand nombre de seed keywords en parallèle sans déclencher de blocages, il faut combiner async/await avec une rotation de sessions proxy. Voici un pipeline production-ready avec httpx async et gestion d'erreurs :
import asyncio
import httpx
import json
import csv
import random
import string
from dataclasses import dataclass, asdict
AUTOCOMPLETE_URL = "https://suggestqueries.google.com/complete/search"
GATEWAY = "gate.proxyhat.com:8080"
BASE_USER = "user-country-{country}-session-{session}"
PASSWORD = "pass"
@dataclass
class KeywordResult:
seed: str
suggestion: str
source: str # "autocomplete" | "paa" | "related"
country: str
def build_proxy_url(country: str, session_id: str) -> str:
"""Construit une URL proxy avec géo-targeting et session sticky."""
username = BASE_USER.format(country=country, session=session_id)
return f"http://{username}:{PASSWORD}@{GATEWAY}"
async def fetch_suggestions_async(
client: httpx.AsyncClient,
query: str,
country: str,
session_id: str,
max_retries: int = 3
) -> list[str]:
"""Récupère les suggestions Autocomplete avec retry et circuit breaker."""
proxy_url = build_proxy_url(country, session_id)
params = {
"client": "chrome",
"q": query,
"hl": country.lower(),
"gl": country.lower()
}
for attempt in range(max_retries):
try:
# Rotation de proxy à chaque retry
proxy = build_proxy_url(country, f"{session_id}-r{attempt}")
async with httpx.AsyncClient(proxy=proxy, timeout=10.0) as c:
resp = await c.get(AUTOCOMPLETE_URL, params=params)
if resp.status_code == 429:
await asyncio.sleep(2 ** attempt) # backoff exponentiel
continue
resp.raise_for_status()
data = resp.json()
if isinstance(data, list) and len(data) > 1:
return data[1]
return []
except (httpx.HTTPError, json.JSONDecodeError) as e:
if attempt == max_retries - 1:
print(f"Échec définitif pour '{query}': {e}")
return []
await asyncio.sleep(1.5 ** attempt)
return []
async def expand_keyword_pipeline(
seeds: list[str],
countries: list[str],
output_csv: str = "keywords.csv"
) -> None:
"""Pipeline asynchrone complet : seed → suggestions → CSV."""
all_results: list[KeywordResult] = []
seen = set() # pour déduplication
async with httpx.AsyncClient(timeout=15.0) as client:
tasks = []
for seed in seeds:
for country in countries:
# Préfixes a-z
for letter in string.ascii_lowercase:
query = f"{letter} {seed}"
session_id = f"{seed}-{country}-{letter}-pre"
tasks.append((query, country, session_id, seed))
# Modificateurs courants
for mod in ["comment", "meilleur", "prix", "avis", "gratuit"]:
query = f"{mod} {seed}"
session_id = f"{seed}-{country}-{mod}"
tasks.append((query, country, session_id, seed))
# Semaphore pour limiter la concurrence (évite de saturer)
semaphore = asyncio.Semaphore(20)
async def bounded_fetch(query, country, session_id, seed):
async with semaphore:
suggestions = await fetch_suggestions_async(
client, query, country, session_id
)
for s in suggestions:
key = (s, country)
if key not in seen:
seen.add(key)
all_results.append(KeywordResult(
seed=seed,
suggestion=s,
source="autocomplete",
country=country
))
await asyncio.sleep(0.3) # throttle poli
await asyncio.gather(*[
bounded_fetch(q, c, s, seed)
for q, c, s, seed in tasks
])
# Export CSV
with open(output_csv, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=["seed", "suggestion", "source", "country"])
writer.writeheader()
for r in all_results:
writer.writerow(asdict(r))
print(f"{len(all_results)} mots-clés uniques exportés vers {output_csv}")
if __name__ == "__main__":
seeds = ["proxy", "vpn", "scraping"]
countries = ["FR", "DE", "US"]
asyncio.run(expand_keyword_pipeline(seeds, countries))
Avec un semaphore à 20 requêtes concurrentes et un throttle de 300ms, ce pipeline traite environ 60 requêtes/seconde tout en restant sous les seuils de détection de Google. Pour 3 seeds × 3 pays × 31 combinaisons = 279 requêtes, le pipeline complet s'exécute en moins de 5 secondes.
Déduplication et clustering par intention de recherche
Une fois vos suggestions collectées, vous aurez des doublons et des variantes proches. Le clustering par intention vous aide à regrouper les questions similaires pour planifier votre contenu :
import re
import csv
from collections import defaultdict
def normalize_keyword(kw: str) -> str:
"""Normalise un mot-clé pour la déduplication."""
kw = kw.lower().strip()
kw = re.sub(r"[^a-z0-9\sàâäéèêëïîôöùûüç-]", "", kw)
kw = re.sub(r"\s+", " ", kw)
return kw
def cluster_by_intent(keywords: list[str]) -> dict[str, list[str]]:
"""Regroupe les mots-clés par intention de recherche détectée."""
clusters = defaultdict(list)
intent_patterns = {
"informationnel": [r"^comment", r"^pourquoi", r"^quand", r"^ou ", r"^quel", r"^quelle", r"^c'est quoi"],
"transactionnel": [r"prix", r"acheter", r"gratuit", r"tarif", r"abonnement", r"devis"],
"comparatif": [r"vs", r"ou ", r"meilleur", r"alternative", r"difference"],
"navigationnel": [r"login", r"site officiel", r"connexion", r"avis"],
}
for kw in keywords:
norm = normalize_keyword(kw)
matched = False
for intent, patterns in intent_patterns.items():
for p in patterns:
if re.search(p, norm):
clusters[intent].append(kw)
matched = True
break
if matched:
break
if not matched:
clusters["autre"].append(kw)
return dict(clusters)
if __name__ == "__main__":
# Charger les mots-clés depuis le CSV
keywords = []
with open("keywords.csv", "r", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
keywords.append(row["suggestion"])
# Dédupliquer
unique = list(set(normalize_keyword(k) for k in keywords))
print(f"{len(keywords)} mots-clés → {len(unique)} uniques après normalisation")
# Clustering
clusters = cluster_by_intent(unique)
for intent, kws in clusters.items():
print(f"\n{intent} ({len(kws)} mots-clés):")
for k in kws[:5]:
print(f" - {k}")
# Export par cluster
with open("keywords_clustered.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["intent", "keyword"])
for intent, kws in clusters.items():
for kw in kws:
writer.writerow([intent, kw])
Erreurs courantes et cas limites
1. Réponses vides sans erreur HTTP
Google peut renvoyer un statut 200 avec un JSON vide ["votre requête", []] lorsqu'il détecte un comportement automatisé. Ce n'est pas une erreur — c'est un blocage silencieux. Solution : surveillez le ratio de réponses vides et changez d'IP si ce ratio dépasse 20%.
2. Locale bias non maîtrisée
Si vous utilisez une IP américaine avec gl=fr, Google peut ignorer le paramètre et renvoyer des suggestions US. La géolocalisation IP prime sur le paramètre URL. Utilisez toujours un proxy du pays cible.
3. Accordéons PAA qui ne se chargent pas
Parfois, la section PAA n'apparaît pas du tout pour certaines requêtes très commerciales. Vérifiez la présence de la section avant de tenter des clics. Ajoutez un timeout et un fallback gracieux.
4. Rate limiting sur l'endpoint Autocomplete
L'endpoint suggestqueries.google.com a un rate limit plus strict que google.com/search. Si vous scrapez les deux en parallèle, séparez vos pools de proxies.
Tableau comparatif : Autocomplete vs PAA vs Related Searches
| Critère | Autocomplete | People Also Ask | Related Searches |
|---|---|---|---|
| Format de réponse | JSON natif | HTML (Playwright requis) | HTML (parsing simple) |
| Latence typique | ~200ms | ~2-5s (rendu JS) | ~1-2s |
| Volume par requête | 10 suggestions | 3-4 questions/clic | 8-12 suggestions |
| Expansion récursive | Non (statique) | Oui (accordéons imbriqués) | Non |
| Source citée | Non | Oui (URL de la réponse) | Non |
| Rate limit (1 IP) | ~100 req/min | ~30 req/min | ~50 req/min |
| Besoin de proxy | Oui (locale bias) | Oui (anti-bot) | Recommandé |
Éthique et bonnes pratiques
Les données Autocomplete et PAA sont publiques — elles sont affichées à tout utilisateur de Google. Cependant, le scraping à grande échelle doit respecter certaines règles :
- Throttlez poliment : 0.3-0.5s entre les requêtes minimum. N'envoyez jamais de bursts de centaines de requêtes sans délai.
- Respectez robots.txt : vérifiez
https://www.google.com/robots.txtpour les règles actuelles. L'endpoint Autocomplete n'est pas explicitement interdit, mais cela peut changer. - Préférez les APIs officielles à grande échelle : pour des volumes importants, l'API Custom Search de Google ou l'API Search Console sont plus fiables et conformes.
- Conformité RGPD : les suggestions Autocomplete ne contiennent pas de données personnelles, mais si vous croisez avec d'autres données, assurez-vous de respecter le RGPD.
Pour aller plus loin sur les bonnes pratiques de scraping, consultez notre guide sur le web scraping avec proxies et le SERP tracking. La documentation ProxyHat détaille toutes les options de configuration disponibles.
Points clés à retenir
1. Trois sources complémentaires. Autocomplete pour le volume, PAA pour les questions et les sources, Related Searches pour l'élargissement thématique. Utilisez les trois en combinaison.
2. L'expansion a-z + modificateurs transforme 1 seed en 300-600 variantes. L'expansion récursive PAA ajoute 40-60 questions par seed.
3. Les proxies résidentiels sont indispensables pour le géo-targeting des suggestions et l'évitement des blocages. Utilisez
gate.proxyhat.com:8080aveccountry-XX-city-yyydans le nom d'utilisateur.4. Le clustering par intention transforme une liste brute en plan de contenu actionnable : pages informationnelles, comparatifs, pages transactionnelles.
5. Throttlez et préférez les APIs officielles pour les volumes importants. Le scraping est un complément, pas un remplacement.
FAQ
Qu'est-ce que le scraping Google People Also Ask et Autocomplete pour la recherche de mots-clés ?
C'est la collecte automatisée des suggestions de recherche Google (Autocomplete), des questions People Also Ask et des Related Searches pour découvrir des mots-clés long-tail et des questions utilisateur. Ces trois sources sont gratuites et publiques, accessibles respectivement via un endpoint JSON pour l'Autocomplete et via le rendu HTML pour les PAA et Related Searches.
Pourquoi le scraping PAA et Autocomplete nécessite-t-il des proxies ?
Google applique des limites de débit par IP (~100 req/min pour Autocomplete, ~30 req/min pour les PAA) et localise les suggestions selon l'IP de l'appelant. Sans proxy résidentiel géo-ciblé, vous obtenez soit des blocages (429, CAPTCHA), soit des suggestions non pertinentes pour votre marché cible. Les proxies résidentiels avec géo-targeting pays/ville résolvent ces deux problèmes.
Quel type de proxy fonctionne le mieux pour scraper Google PAA et Autocomplete ?
Les proxies résidentiels sont le meilleur choix car ils utilisent de vraies adresses IP d'ISP, non détectées par les systèmes anti-bot de Google. Les proxies datacenter sont rapidement identifiés et bloqués. Pour l'Autocomplete, des sessions rotatives suffisent ; pour les PAA avec Playwright, des sessions sticky (même IP pendant toute la session) sont préférables pour éviter de réinitialiser le contexte du navigateur.
Comment éviter les blocages en scrapant Google People Also Ask et Autocomplete ?
Combinez quatre stratégies : (1) utilisez des proxies résidentiels avec rotation de sessions, (2) throttlez à 0.3-0.5s entre les requêtes, (3) implémentez un backoff exponentiel sur les erreurs 429, et (4) surveillez le ratio de réponses vides — un ratio supérieur à 20% indique un blocage silencieux qui nécessite un changement d'IP. Limitez la concurrence à 20 requêtes parallèles maximum.
Peut-on utiliser l'endpoint Autocomplete de Google sans proxy ?
Oui pour de petits volumes (moins de 100 requêtes), mais les suggestions seront biaisées par votre géolocalisation IP. Pour obtenir des suggestions localisées pour un marché spécifique (ex : Allemagne, Japon), un proxy résidentiel du pays cible est nécessaire car Google priorise la géolocalisation IP sur les paramètres URL gl et hl.





