Cos'è lo scraping di People Also Ask e Autocomplete di Google per la ricerca keyword
Se fai keyword research, sai che i dati più preziosi spesso non si trovano nei tool costosi ma direttamente nelle SERP di Google. Lo scraping di People Also Ask e Autocomplete di Google per ricerca keyword (guida Python) è la pratica di estrarre i suggerimenti di ricerca di Google — Autocomplete, domande PAA e Related Searches — per costruire liste di long-tail keyword, cluster di intent e contenuti FAQ a costo zero. In questa guida mostreremo come farlo con Python, proxy residenziali e codice production-ready.
Google offre tre fonti gratuite di dati di intent che i tool commerciali spesso riempacchettano: l'endpoint Autocomplete (suggestqueries.google.com), i blocchi People Also Ask nelle SERP organiche e i Related Searches a fondo pagina. Combinandole, un singolo seed come "scarpe da running" può generare centinaia di long-tail in pochi minuti — ma richiede gestione degli IP, rotazione intelligente e rispetto dei limiti di frequenza.
Le tre miniere d'oro gratuite: Autocomplete, PAA e Related Searches
Ogni fonte mappa a un diverso livello di intent di ricerca:
| Fonte | Endpoint / posizione | Tipo di intent | Volume tipico per query |
|---|---|---|---|
| Autocomplete (Suggest) | suggestqueries.google.com/complete/search?client=chrome&q= | Long-tail esplorativo, discovery | 10 suggerimenti per richiesta |
| People Also Ask | Blocco accordion nelle SERP organiche | Domande informative, intent interrogativo | 4–8 domande iniziali, espandibili ricorsivamente |
| Related Searches | Fondo pagina risultati | Intent adiacenti, varianti semantiche | 8–10 termini per pagina |
L'endpoint Autocomplete di Chrome restituisce JSON puro con suggerimenti in tempo reale basati su popolarità reale. Le domande PAA, invece, vivono nel DOM della SERP come accordion cliccabili che rivelano ulteriori domande annidate — ogni click genera nuove domande figlio. I Related Searches, infine, offrono varianti semantiche che Google stesso associa alla query originaria.
Perché queste fonti battono i tool commerciali (a volte)
I dati di Autocomplete riflettono volumi reali di ricerca corrente, non stime campionate. Una query che spunta in Autocomplete ha effettivamente traffico. I tool come Ahrefs o SEMrush campionano i loro indici; Autocomplete è aggiornato quotidianamente da Google stesso. Lo svantaggio? Non hai volumi numerici esatti — ma per il discovery di long-tail, la presenza/assenza in Autocomplete è già un segnale forte.
Costruire un espansore da seed a long-tail
La tecnica classica per moltiplicare un seed consiste nel combinare prefissi alfabetici e modificatori. Per ogni seed, generi varianti anteponendo ogni lettera a–z, numeri 0–9 e modificatori comuni come "come", "perché", "migliore", "vs", "economico". Questo trasforma un singolo termine in centinaia di query da passare ad Autocomplete.
Espansione con prefissi a–z e modificatori
Il pattern è semplice: per ogni combinazione seed + prefisso, chiami l'endpoint Autocomplete e raccogli i suggerimenti. Con 26 lettere + 10 cifre + 10 modificatori = 46 richieste per seed, ottieni fino a 460 suggerimenti grezzi (con duplicati).
import string
import itertools
def generate_prefixed_seeds(seed: str) -> list[str]:
"""Genera tutte le varianti con prefissi a-z, 0-9 e modificatori."""
prefixes = list(string.ascii_lowercase) + list(string.digits)
modifiers = [
"come", "perché", "quando", "dove", "quanto",
"migliore", "migliori", "vs", "economico", "gratis",
"recensione", "alternative", "differenza", "guida", "tutorial"
]
all_prefixes = prefixes + [f"{m} " for m in modifiers]
return [f"{p}{seed}" for p in all_prefixes]
# Esempio
seed = "scarpe da running"
variants = generate_prefixed_seeds(seed)
print(f"Generati {len(variants)} seed da '{seed}'")
# Output: Generati 51 seed da 'scarpe da running'
Espansione ricorsiva delle domande PAA
Le domande People Also Ask sono accordion: cliccandone una, Google carica dinamicamente nuove domande figlio. Questo crea una struttura ad albero che puoi attraversare ricorsivamente. Partendo da 4 domande iniziali, ogni click ne rivela altre 3–4, portando rapidamente a decine di domande con un solo seed.
from dataclasses import dataclass
@dataclass
class PAANode:
question: str
answer_snippet: str = ""
source_url: str = ""
children: list['PAANode'] = None
def __post_init__(self):
if self.children is None:
self.children = []
def expand_paa_tree(root_questions: list[str], max_depth: int = 3, max_children: int = 4) -> list[PAANode]:
"""Espande ricorsivamente l'albero PAA fino a max_depth."""
tree = [PAANode(question=q) for q in root_questions]
def _expand(nodes: list[PAANode], depth: int):
if depth >= max_depth:
return
for node in nodes[:max_children]:
# Qui inserirai la logica di click con Playwright
# che genera nuove domande figlio
child_questions = fetch_child_questions(node.question)
node.children = [PAANode(question=cq) for cq in child_questions]
_expand(node.children, depth + 1)
_expand(tree, 0)
return tree
Perché le chiamate rapide ad Autocomplete e PAA innescano rate limit e bias locale
Google applica rate limit per IP su suggestqueries.google.com e sulle SERP. Se invii decine di richieste al secondo dallo stesso IP, riceverai errori 429 (Too Many Requests) o risposte vuote. Inoltre, Autocomplete è fortemente localizzato: una query da un IP tedesco restituisce suggerimenti diversi rispetto allo stesso IP statunitense, perché Google inferisce la posizione dell'utente dall'IP per personalizzare i suggerimenti.
Questo significa che per ottenere suggerimenti localizzati e puliti hai bisogno di proxy residenziali con geo-targeting paese/città. Un datacenter IP viene spesso flaggato come traffico automatizzato, mentre un IP residenziale appare come un utente reale. Secondo la RFC 7231, il codice 429 indica proprio questo scenario di limitazione.
Con ProxyHat puoi specificare paese e città nel username del proxy:
# Proxy residenziale con geo-targeting Germania/Berlino
PROXY_HTTP = "http://user-country-DE-city-berlin:pass@gate.proxyhat.com:8080"
# Proxy residenziale con geo-targeting Italia/Milano
PROXY_HTTP_IT = "http://user-country-IT-city-milan:pass@gate.proxyhat.com:8080"
# Proxy SOCKS5 con sessione sticky (stesso IP per tutte le richieste)
PROXY_SOCKS5 = "socks5://user-country-US-session-abc123:pass@gate.proxyhat.com:1080"
La geo-targeting è cruciale: se fai keyword research per il mercato italiano, devi usare IP italiani per ottenere suggerimenti rilevanti. Un IP statunitense restituirebbe suggerimenti anglofoni o con intent diverso.
Esempio Python: Autocomplete con httpx e proxy ProxyHat
Vediamo un esempio completo che usa httpx con proxy residenziali ProxyHat per estrarre suggerimenti Autocomplete in JSON, con gestione degli errori, retry e logging.
import httpx
import asyncio
import logging
import json
from typing import Optional
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
class AutocompleteScraper:
BASE_URL = "https://suggestqueries.google.com/complete/search"
def __init__(self, proxy_url: str, timeout: float = 15.0, max_retries: int = 3):
self.proxy_url = proxy_url
self.timeout = timeout
self.max_retries = max_retries
async def fetch_suggestions(self, query: str, gl: str = "it", hl: str = "it") -> list[str]:
"""Recupera suggerimenti Autocomplete per una singola query."""
params = {
"client": "chrome",
"q": query,
"gl": gl, # geo-location (paese)
"hl": hl, # language
}
async with httpx.AsyncClient(
proxy=self.proxy_url,
timeout=self.timeout,
headers={
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Accept-Language": f"{hl},{gl};q=0.9",
},
) as client:
for attempt in range(1, self.max_retries + 1):
try:
resp = await client.get(self.BASE_URL, params=params)
if resp.status_code == 429:
logger.warning(f"Rate limit su '{query}', attesa {attempt * 2}s")
await asyncio.sleep(attempt * 2)
continue
resp.raise_for_status()
data = resp.json()
# Formato Chrome: [query, [suggestions], ...]
suggestions = data[1] if len(data) > 1 else []
logger.info(f"'{query}' → {len(suggestions)} suggerimenti")
return suggestions
except (httpx.HTTPError, json.JSONDecodeError) as e:
logger.error(f"Errore su '{query}' (tentativo {attempt}): {e}")
if attempt < self.max_retries:
await asyncio.sleep(attempt * 2)
return []
async def expand_seed(self, seed: str, gl: str = "it", hl: str = "it") -> list[str]:
"""Espande un seed con prefissi a-z e modificatori."""
import string
prefixes = list(string.ascii_lowercase) + list(string.digits)
modifiers = ["come", "perché", "migliore", "vs", "economico", "guida", "recensione"]
all_queries = [f"{p}{seed}" for p in prefixes] + [f"{m} {seed}" for m in modifiers]
tasks = [self.fetch_suggestions(q, gl, hl) for q in all_queries]
results = await asyncio.gather(*tasks, return_exceptions=True)
all_suggestions = set()
for r in results:
if isinstance(r, list):
all_suggestions.update(r)
return sorted(all_suggestions)
# Utilizzo
PROXY = "http://user-country-IT-city-milan:pass@gate.proxyhat.com:8080"
async def main():
scraper = AutocompleteScraper(proxy_url=PROXY)
suggestions = await scraper.expand_seed("scarpe da running", gl="it", hl="it")
print(f"Totale suggerimenti unici: {len(suggestions)}")
for s in suggestions[:20]:
print(f" - {s}")
asyncio.run(main())
Estrarre domande PAA con Playwright e proxy residenziali
Per le domande People Also Ask serve un browser headless perché gli accordion sono interattivi. Playwright è la scelta ideale: supporta proxy nativamente e può cliccare gli accordion per rivelare domande annidate.
from playwright.async_api import async_playwright
import asyncio
import re
import logging
logger = logging.getLogger(__name__)
class PAAScraper:
GOOGLE_URL = "https://www.google.com/search?q={query}&hl={hl}&gl={gl}"
def __init__(self, proxy_server: str, proxy_username: str, proxy_password: str):
self.proxy_server = proxy_server
self.proxy_username = proxy_username
self.proxy_password = proxy_password
async def scrape_paa(self, query: str, hl: str = "it", gl: str = "it", max_clicks: int = 10) -> list[dict]:
"""Estrae domande PAA e fonti citate per una query."""
questions_data = []
url = self.GOOGLE_URL.format(query=query, hl=hl, gl=gl)
async with async_playwright() as p:
browser = await p.chromium.launch(
headless=True,
proxy={
"server": self.proxy_server,
"username": self.proxy_username,
"password": self.proxy_password,
},
)
context = await browser.new_context(
locale=f"{hl}-{gl.upper()}",
viewport={"width": 1920, "height": 1080},
)
page = await context.new_page()
try:
await page.goto(url, wait_until="networkidle", timeout=30000)
# Seleziona il blocco PAA
paa_block = page.locator("[jsname='Cpkphd']").first
if not await paa_block.count():
logger.info(f"Nessun blocco PAA per '{query}'")
return []
for click_idx in range(max_clicks):
# Trova tutte le domande visibili nel blocco PAA
question_elements = page.locator("div[jsname='Cpkphd'] .related-question-pair")
count = await question_elements.count()
for i in range(count):
try:
q_text = await question_elements.nth(i).inner_text()
q_text = q_text.strip()
if q_text and q_text not in [q["question"] for q in questions_data]:
# Cerca la fonte citata nella risposta
source_el = question_elements.nth(i).locator("a[data-ved]").first
source_url = await source_el.get_attribute("href") if await source_el.count() else ""
source_text = await source_el.inner_text() if await source_el.count() else ""
questions_data.append({
"question": q_text,
"source_url": source_url or "",
"source_name": source_text or "",
})
except Exception as e:
logger.debug(f"Errore estrazione domanda {i}: {e}")
# Click sull'ultimo accordion per generare nuove domande
if click_idx < max_clicks - 1:
last_question = question_elements.nth(count - 1)
await last_question.click()
await page.wait_for_timeout(1500)
except Exception as e:
logger.error(f"Errore durante scraping PAA: {e}")
finally:
await browser.close()
return questions_data
# Utilizzo
async def run_paa():
scraper = PAAScraper(
proxy_server="http://gate.proxyhat.com:8080",
proxy_username="user-country-IT-city-milan",
proxy_password="pass",
)
results = await scraper.scrape_paa("scarpe da running", hl="it", gl="it", max_clicks=8)
print(f"Estratte {len(results)} domande PAA")
for r in results[:10]:
print(f" Q: {r['question']}")
print(f" Fonte: {r['source_name']} ({r['source_url'][:60]})")
asyncio.run(run_paa())
Dedup, clustering per intent ed export CSV
Dopo aver raccolto centinaia di suggerimenti e domande, il passo successivo è deduplicare, clusterizzare per intent e preparare i dati per la pianificazione dei contenuti e delle FAQ.
import csv
import re
from collections import defaultdict
from typing import Optional
def normalize_keyword(kw: str) -> str:
"""Normalizza keyword: lowercase, rimuove spazi multipli e punteggiatura."""
kw = kw.lower().strip()
kw = re.sub(r'\s+', ' ', kw)
kw = re.sub(r'[^\w\sàèéìòù-]', '', kw)
return kw
def classify_intent(kw: str) -> str:
"""Classifica l'intent di ricerca basato su pattern linguistici."""
kw_lower = kw.lower()
informational_patterns = [r'^come\b', r'^perché\b', r'^quando\b', r'^dove\b', r'^cosa\b', r'\?$', r'^guida\b', r'^tutorial\b']
transactional_patterns = [r'compra', r'acquista', r'prezzo', r'offerta', r'sconto', r'economico', r'gratis']
navigational_patterns = [r'login', r'sito ufficiale', r'recensione', r'opinioni']
for pattern in informational_patterns:
if re.search(pattern, kw_lower):
return "informational"
for pattern in transactional_patterns:
if re.search(pattern, kw_lower):
return "transactional"
for pattern in navigational_patterns:
if re.search(pattern, kw_lower):
return "navigational"
return "commercial"
def cluster_keywords(keywords: list[str]) -> dict[str, list[str]]:
"""Clusterizza keyword per intent e per root term."""
clusters = defaultdict(list)
seen = set()
for kw in keywords:
normalized = normalize_keyword(kw)
if not normalized or normalized in seen:
continue
seen.add(normalized)
intent = classify_intent(normalized)
clusters[intent].append(normalized)
return dict(clusters)
def export_to_csv(keywords: list[str], paa_questions: list[dict], output_path: str = "keyword_research.csv"):
"""Esporta keyword e domande PAA in CSV per content planning."""
clusters = cluster_keywords(keywords)
with open(output_path, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["tipo", "keyword", "intent", "source"])
for intent, kws in clusters.items():
for kw in kws:
writer.writerow(["autocomplete", kw, intent, "google_suggest"])
for paa in paa_questions:
kw = normalize_keyword(paa["question"])
intent = classify_intent(kw)
writer.writerow(["paa", kw, intent, paa.get("source_url", "")])
logger.info(f"Esportate {len(keywords)} keyword + {len(paa_questions)} domande PAA in {output_path}")
return output_path
# Esempio di utilizzo completo
async def full_pipeline():
# 1. Autocomplete scraping
ac_scraper = AutocompleteScraper(proxy_url=PROXY)
autocomplete_kws = await ac_scraper.expand_seed("scarpe da running", gl="it", hl="it")
# 2. PAA scraping
paa_scraper = PAAScraper(
proxy_server="http://gate.proxyhat.com:8080",
proxy_username="user-country-IT-city-milan",
proxy_password="pass",
)
paa_results = await paa_scraper.scrape_paa("scarpe da running", hl="it", gl="it")
# 3. Export CSV
csv_path = export_to_csv(autocomplete_kws, paa_results, "scarpe_running_research.csv")
print(f"CSV salvato: {csv_path}")
# Statistiche
clusters = cluster_keywords(autocomplete_kws)
for intent, kws in clusters.items():
print(f" {intent}: {len(kws)} keyword")
asyncio.run(full_pipeline())
ProxyHat: configurazione e best practice
Per scalare lo scraping di Autocomplete e PAA, la scelta del proxy è determinante. I proxy residenziali di ProxyHat offrono IP reali di provider ISP, con geo-targeting per paese e città. Ecco la configurazione raccomandata:
| Parametro | Valore | Uso |
|---|---|---|
| Gateway HTTP | gate.proxyhat.com:8080 | Autocomplete, richieste API-like |
| Gateway SOCKS5 | gate.proxyhat.com:1080 | Playwright, browser headless |
| Geo-targeting paese | user-country-IT | Suggerimenti localizzati Italia |
| Geo-targeting città | user-country-IT-city-milan | Suggerimenti Milano-specifici |
| Sessione sticky | user-session-abc123 | Stesso IP per sessione Playwright |
Per il browser Playwright, usa una sessione sticky per mantenere lo stesso IP durante l'intera sessione (necessario per evitare che Google rilevi cambio-IP mid-session). Per Autocomplete, usa rotazione per-request per distribuire il carico.
Puoi esplorare tutte le locazioni disponibili e consultare il documentazione ufficiale ProxyHat per dettagli su autenticazione e parametri avanzati.
Best practice per la produzione
- Throttling: mantieni 2–5 richieste concurrent per IP, con delay di 1–3 secondi tra batch.
- Retry con backoff esponenziale: al 429, aspetta 2^n secondi dove n è il tentativo.
- Circuit breaker: dopo 5 errori consecutivi su un IP, cambia sessione proxy.
- Logging strutturato: registra query, timestamp, status code, proxy IP per debugging.
- Cache locale: salva i risultati in JSON/SQLite per evitare re-scraping delle stesse query.
Errori comuni e casi limite
1. Locale bias non gestito
Se non specifichi gl e hl nei parametri della richiesta, Google inferisce la locale dall'IP del proxy. Questo può portare a suggerimenti in lingua sbagliata. Usa sempre gl=it e hl=it per l'Italia, combinati con un IP italiano.
2. CAPTCHA nelle SERP
Con proxy datacenter o troppe richieste, Google può servire una pagina CAPTCHA invece della SERP. I proxy residenziali riducono drasticamente questo rischio, ma non lo eliminano. Implementa un rilevamento: se la risposta HTML contiene "captcha o "unusual traffic", fermati e cambia IP.
3. Struttura DOM di Google che cambia
I selettori CSS di Google cambiano frequentemente. Non hardcodare jsname='Cpkphd' senza fallback. Usa selettori multipli e test regolari. Un approccio robusto combina selettori CSS con match testuali sul contenuto.
4. Dedup incompleto
Suggerimenti Autocomplete possono apparire identici ma con spaziatura o capitalizzazione diversa. Normalizza sempre prima di deduplicare. Usa normalize_keyword() come mostrato sopra.
Considerazioni etiche
I dati di Autocomplete e PAA sono pubblicamente accessibili a qualsiasi utente che digiti su Google. Tuttavia, lo scraping su larga scala impone un carico sui server di Google. Linee guida:
- Throttle polite: non superare 1 richiesta al secondo per IP, preferibilmente meno.
- Preferisci API ufficiali: per uso commerciale su larga scala, valuta il Custom Search API di Google che offre 100 query/giorno gratuite.
- Rispetta robots.txt: controlla
https://www.google.com/robots.txtper le direttive correnti. - GDPR: i dati di suggerimento non contengono dati personali, ma evita di associare query a individui.
- Cache dei risultati: salva localmente per ridurre richieste ripetute.
Key Takeaways
Riepilogo dei punti chiave:
- Autocomplete, PAA e Related Searches sono tre fonti gratuite di dati di intent con caratteristiche complementari.
- Un singolo seed può generare 400+ long-tail combinando prefissi a–z e modificatori con Autocomplete.
- Le domande PAA si espandono ricorsivamente cliccando gli accordion, creando alberi di domande informative.
- I proxy residenziali con geo-targeting paese/città sono essenziali per suggerimenti localizzati e per evitare rate limit.
- Usa sessioni sticky per Playwright (stesso IP per sessione) e rotazione per-request per Autocomplete.
- Dedup, clustering per intent e export CSV trasformano dati grezzi in un piano contenuti actionable.
- Throttle polite (≤1 req/s/IP) e preferenza per API ufficiali su scala commerciale sono principi etici fondamentali.
Pronto a costruire il tuo pipeline di keyword research? Esplora i piani ProxyHat, consulta le use case di web scraping e i casi d'uso per SERP tracking per approfondire.





