Si construyes herramientas de keyword research, Google te ofrece tres minas gratuitas que la mayoría de SEOs tocan manualmente y pocos automatizan a escala: Autocomplete, People Also Ask (PAA) y Related Searches. En esta guía vas a scrapear People Also Ask y Autocomplete de Google para investigación de palabras clave con Python, usando proxies residenciales para evitar rate limits y sesgos de localización.
El objetivo es convertir una semilla como "keyword research" en cientos de longtails, extraer preguntas reales de usuarios y exportar un CSV listo para planificar contenido y FAQs. Todo con código ejecutable, manejo de errores y patrones de producción.
Las tres minas de oro gratuitas para scrapear People Also Ask y Autocomplete
Google expone sugerencias en tres puntos distintos del journey de búsqueda, y cada uno mapea a una fase de intención:
- Autocomplete (suggestqueries.google.com): aparece mientras el usuario escribe. Refleja intención exploratoria y consultas longtail emergentes. El endpoint
complete/search?client=chrome&q=devuelve JSON puro con hasta 10 sugerencias por consulta. - People Also Ask: cajas de preguntas relacionadas que aparecen dentro del SERP. Cada acordeón, al expandirse, revela una nueva pregunta anidada — ideal para contenido de respuesta y FAQs.
- Related Searches: lista al final del SERP. Representa consultas vecinas semánticamente, útiles para clustering temático.
Autocomplete captura intención antes del click, PAA captura intención durante la exploración del resultado, y Related Searches captura intención después. Combinados, cubren el embudo completo. Puedes leer más sobre cómo funciona el mecanismo de autocompletado en la entrada de Wikipedia sobre Autocomplete.
Por qué el scraping rápido choca con rate limits y sesgo local
El problema técnico central: Google aplica rate limits por IP a suggestqueries.google.com y al SERP. Si lanzas 200 peticiones por segundo desde un solo IP, en minutos recibes 429 Too Many Requests o respuestas vacías. Además, Autocomplete y PAA son sensibles a la geolocalización: la misma consulta devuelve sugerencias distintas en Alemania vs. México, porque Google personaliza por país, idioma e historial.
Esto significa dos cosas para tu pipeline:
- Necesitas rotación de IP para escalar sin bans.
- Necesitas geo-targeting preciso (país y ciudad) para capturar sugerencias localizadas reales, no las de un datacenter en Virginia.
Los proxies datacenter son rápidos pero sus rangos IP están catalogados como no residenciales y Google los degrada o bloquea con más frecuencia. Los proxies residenciales, al venir de ISPs reales, tienen huellas mucho más limpias. Para escala seria, Google ofrece la Custom Search JSON API oficial, pero está limitada a 100 consultas/día en el tier gratuito y no expone PAA ni Autocomplete — por eso el scraping sigue siendo la opción práctica para keyword research a volumen.
Configuración de ProxyHat: geo-targeting en el username
ProxyHat permite pasar flags de país, ciudad y sesión directamente en el username del proxy. El formato es:
http://user-country-DE-city-berlin:PASSWORD@gate.proxyhat.com:8080
socks5://user-country-DE-city-berlin:PASSWORD@gate.proxyhat.com:1080Esto te da control granular: usa country-US para sugerencias estadounidenses, country-MX para mexicanas, o country-DE-city-berlin para ultra-localización. La flag session-abc123 mantiene la misma IP entre peticiones (útil para Playwright, donde necesitas coherencia durante una sesión de navegador). Consulta la documentación de ProxyHat para el listado completo de países y ciudades soportadas, y revisa el mapa de ubicaciones antes de planificar campañas multi-país.
A continuación, un wrapper que encapsula la configuración y genera URLs de proxy listas para usar, mostrando uso raw y estilo SDK lado a lado:
from dataclasses import dataclass
from typing import Optional, List
@dataclass
class ProxyHatConfig:
username: str
password: str
country: str = "US"
city: Optional[str] = None
session: Optional[str] = None
def _build_user(self) -> str:
flags: List[str] = []
if self.country:
flags.append(f"country-{self.country}")
if self.city:
flags.append(f"city-{self.city.lower()}")
if self.session:
flags.append(f"session-{self.session}")
if not flags:
return self.username
return f"{self.username}-" + "-".join(flags)
def http_url(self) -> str:
return f"http://{self._build_user()}:{self.password}@gate.proxyhat.com:8080"
def socks5_url(self) -> str:
return f"socks5://{self._build_user()}:{self.password}@gate.proxyhat.com:1080"
# Uso raw
cfg = ProxyHatConfig(
username="user", password="pass",
country="DE", city="berlin", session="kw-de-1"
)
print(cfg.http_url())
# http://user-country-DE-city-berlin-session-kw-de-1:pass@gate.proxyhat.com:8080
# Uso con httpx
import httpx
client = httpx.AsyncClient(proxy=cfg.http_url())Este patrón te permite intercambiar país por consulta y mantener sesiones estables para Playwright. Para ver opciones de plan y límites de concurrencia, visita la página de precios de ProxyHat.
Paso 1: Scraper de Autocomplete con httpx y proxy
El endpoint de Autocomplete en formato Chrome devuelve JSON con la estructura [query, [suggestions], [types], ...]. Es ligero, rápido (~150-300 ms por consulta) y no requiere renderizar JavaScript, lo que lo hace ideal para concurrencia alta.
import httpx
import asyncio
from typing import List
AUTOCOMPLETE_URL = "https://suggestqueries.google.com/complete/search"
async def fetch_suggestions(client: httpx.AsyncClient, query: str) -> List[str]:
params = {"client": "chrome", "q": query}
try:
r = await client.get(AUTOCOMPLETE_URL, params=params, timeout=10.0)
r.raise_for_status()
data = r.json()
if isinstance(data, list) and len(data) > 1:
return data[1]
return []
except (httpx.HTTPError, ValueError) as exc:
print(f"Error para '{query}': {exc}")
return []
async def main():
proxy = "http://user-country-US:PASSWORD@gate.proxyhat.com:8080"
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}
async with httpx.AsyncClient(proxy=proxy, headers=headers) as client:
suggestions = await fetch_suggestions(client, "keyword research")
print(suggestions)
asyncio.run(main())Cada llamada devuelve hasta 10 sugerencias. Si combinas esto con expansión de semillas, una keyword se convierte en cientos.
Paso 2: Expansor de semillas a longtail (a-z + modificadores)
La técnica clásica para multiplicar semillas: añade cada letra del alfabeto y modificadores de intención como prefijos/sufijos. Así, "keyword research" genera "keyword research a", "keyword research b", ..., "que es keyword research", "keyword research gratis", etc.
import string
from typing import List
def expand_seed(seed: str, modifiers: List[str] = None) -> List[str]:
if modifiers is None:
modifiers = [
"que es", "como", "mejor", "gratis", "2025",
"herramientas", "ejemplos", "vs", "tutorial", "definicion"
]
queries = set()
# Prefijos a-z (autocompletado letra a letra)
for letter in string.ascii_lowercase:
queries.add(f"{seed} {letter}")
# Modificadores como prefijo y sufijo
for mod in modifiers:
queries.add(f"{mod} {seed}")
queries.add(f"{seed} {mod}")
return sorted(queries)
seeds = ["keyword research", "seo python", "scraping google"]
all_queries = []
for s in seeds:
all_queries.extend(expand_seed(s))
print(f"Generadas {len(all_queries)} consultas desde {len(seeds)} semillas")
# Generadas 243 consultas desde 3 semillasCada una de esas 243 consultas se pasa al scraper de Autocomplete, que devuelve ~10 sugerencias → potencialmente 2,000+ longtails. Para ejecutarlas sin saturar, usa concurrencia limitada con un semáforo:
import asyncio
import httpx
CONCURRENCY = 50 # 50 sesiones concurrentes
PROXY = "http://user-country-US:PASSWORD@gate.proxyhat.com:8080"
async def bounded(sem, client, q):
async with sem:
return q, await fetch_suggestions(client, q)
async def expand_concurrent(queries: List[str]) -> dict:
sem = asyncio.Semaphore(CONCURRENCY)
async with httpx.AsyncClient(proxy=PROXY) as client:
tasks = [bounded(sem, client, q) for q in queries]
results = await asyncio.gather(*tasks, return_exceptions=True)
out = {}
for r in results:
if isinstance(r, tuple):
out[r[0]] = r[1]
return out
suggestions_map = asyncio.run(expand_concurrent(all_queries))
print(f"Recogidas sugerencias para {len(suggestions_map)} consultas")Con 50 conexiones concurrentes y ~200 ms por petición, procesar 243 consultas toma ~1 segundo. Ajusta CONCURRENCY según tu plan y el comportamiento observado.
Paso 3: Extracción de People Also Ask con Playwright
El PAA requiere renderizar JavaScript y hacer click en los acordeones para revelar preguntas anidadas. Aquí entra Playwright con proxy. La flag session mantiene la misma IP durante toda la sesión del navegador, evitando que Google detecte saltos sospechosos.
from playwright.async_api import async_playwright
from typing import List
import asyncio
PROXY = {
"server": "http://gate.proxyhat.com:8080",
"username": "user-country-US-session-paa-1",
"password": "PASSWORD",
}
PAA_QUESTION_SELECTOR = 'div.related-question-pair'
async def scrape_paa(query: str, max_clicks: int = 6) -> List[str]:
questions = set()
async with async_playwright() as p:
browser = await p.chromium.launch(proxy=PROXY, headless=True)
page = await browser.new_page()
await page.goto(f"https://www.google.com/search?q={query}")
await page.wait_for_selector(PAA_QUESTION_SELECTOR, timeout=10000)
for _ in range(max_clicks):
els = await page.query_selector_all(PAA_QUESTION_SELECTOR)
for el in els:
text = (await el.inner_text()).strip().split("\n")[0]
if text.endswith("?"):
questions.add(text)
# Click en el primer acordeón cerrado para revelar preguntas anidadas
closed = await page.query_selector(f'{PAA_QUESTION_SELECTOR}:not([expanded])')
if not closed:
break
try:
await closed.click(timeout=2000)
await page.wait_for_timeout(800)
except Exception:
break
await browser.close()
return sorted(questions)
paa_questions = asyncio.run(scrape_paa("keyword research"))
print(f"Extraídas {len(paa_questions)} preguntas PAA")Cada click revela una nueva pregunta y, a veces, un sub-acordeón. Repitiendo 6 iteraciones puedes capturar 15-40 preguntas por consulta. Las preguntas del PAA suelen venir con una fuente citada (un dominio que Google extrae como respuesta) — útil para link-building y análisis competitivo.
Paso 4: Recursión para expansión profunda de PAA
Para escalar, convierte cada pregunta extraída en una nueva consulta de búsqueda, que a su vez genera su propio bloque PAA. Esto es recursión de profundidad controlada:
from typing import Set
async def recursive_paa(seed: str, depth: int = 2, max_per_level: int = 10) -> Set[str]:
seen: Set[str] = set()
frontier = [seed]
for _ in range(depth):
next_frontier = []
for q in frontier:
if q in seen:
continue
seen.add(q)
questions = await scrape_paa(q, max_clicks=4)
next_frontier.extend(questions[:max_per_level])
await asyncio.sleep(2) # throttle educado
frontier = next_frontier
return seen
all_questions = asyncio.run(recursive_paa("keyword research", depth=2))
print(f"Total preguntas únicas: {len(all_questions)}")Con profundidad 2 y 10 preguntas por nivel, una semilla produce ~100 preguntas únicas. Subir a profundidad 3 multiplica exponencialmente, pero incrementa el riesgo de bloqueos — equilibra con throttling y rotación de sesión.
Paso 5: Deduplicación, clustering por intención y exportación a CSV
Tras recoger sugerencias de Autocomplete y preguntas de PAA, tendrás duplicados y ruido. Normaliza, deduplica y agrupa por intención para priorizar contenido:
import csv
import re
from collections import defaultdict
from typing import List, Dict
def normalize(q: str) -> str:
return re.sub(r"[^\w\sáéíóúñ]", "", q.lower()).strip()
def cluster_by_intent(questions: List[str]) -> Dict[str, List[str]]:
buckets = defaultdict(list)
markers = {
"informacional": ["que es", "que son", "definicion", "significado", "diferencia"],
"transaccional": ["comprar", "precio", "barato", "mejor", "review", "top"],
"howto": ["como", "tutorial", "pasos", "guia", "aprender"],
"comparativa": ["vs", "alternativa", "comparativa"],
}
seen = set()
for q in questions:
n = normalize(q)
if not n or n in seen:
continue
seen.add(n)
intent = "otro"
for label, kws in markers.items():
if any(k in n for k in kws):
intent = label
break
buckets[intent].append(q)
return buckets
def export_csv(questions: List[str], path: str = "keywords.csv") -> None:
clusters = cluster_by_intent(questions)
with open(path, "w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(["keyword", "intent", "source"])
for intent, qs in clusters.items():
for q in qs:
w.writerow([q, intent, "paa"])
print(f"Exportadas {sum(len(v) for v in clusters.values())} keywords a {path}")
# Combinar Autocomplete + PAA y exportar
combined = list(all_questions) + [s for subs in suggestions_map.values() for s in subs]
export_csv(combined, "keyword_research.csv")El CSV resultante es directamente utilizable: filtra por intención howto para guías, informacional para definiciones, comparativa para páginas de alternativas. Es la base de un calendario editorial orientado a búsqueda.
Errores comunes y casos límite
- No respetar el locale: si pides Autocomplete sin geo-targeting, obtienes sugerencias del país del IP del datacenter, no del mercado objetivo. Usa siempre
country-XXen el username. - Sesiones inestables en Playwright: sin la flag
session-, cada petición puede rotar IP y Google muestra un challenge de verificación. Mantén una sesión por navegador. - No manejar 429: añade backoff exponencial y reintentos. Un 429 aislado no es fatal; ignorarlo sí lo es.
- Selectores frágiles de PAA: Google cambia el DOM con frecuencia. Valida selectores antes de cada campaña y ten fallbacks.
- Olvidar deduplicar: la recursión de PAA genera mucho solapamiento. Normaliza y deduplica antes de clustering.
Para casos de uso más amplios, revisa las guías de web scraping y SERP tracking.
Ética y consideraciones legales
Las sugerencias de Autocomplete y las preguntas de PAA son datos públicos visibles en cualquier navegador. Aun así, el scraping a escala tiene consideraciones éticas:
- Throttlea educadamente: 2-5 segundos entre clics de PAA, concurrencia razonable en Autocomplete. No es una carrera, es una cosecha.
- Respeta robots.txt: revisa google.com/robots.txt antes de automatizar.
- Prefer APIs oficiales a escala: si vas a procesar decenas de miles de consultas diarias, valora la Custom Search API o herramientas de keyword research con licencia, aunque no cubran PAA.
- Cumple ToS y GDPR/CCPA: no almacenes datos personales; las sugerencias agregadas no lo son, pero conviene revisar las condiciones de uso de Google.
El scraping de datos públicos para investigación de palabras clave es una práctica establecida en SEO, pero la sostenibilidad requiere moderación: mejor 1,000 consultas limpias que 50,000 que acaban en ban.
Puntos clave
- Autocomplete, PAA y Related Searches cubren tres fases de intención: exploración, exploración del resultado y consulta vecina.
- El endpoint
suggestqueries.google.com/complete/search?client=chrome&q=devuelve JSON ligero ideal para concurrencia. - Los proxies residenciales con geo-targeting (país/ciudad) son necesarios para sugerencias localizadas y evitar rate limits por IP.
- La expansión a-z + modificadores convierte una semilla en cientos de longtails; la recursión de PAA multiplica las preguntas.
- Normaliza, deduplica y clusteriza por intención antes de exportar a CSV para contenido accionable.
- Throttlea, respeta robots.txt y considera APIs oficiales a gran escala.
Conclusión y siguiente paso
Con ~200 líneas de Python tienes un pipeline completo de keyword research que combina las tres minas gratuitas de Google. El siguiente paso es integrarlo con tu stack de contenido: alimenta el CSV a tu herramienta de planificación editorial o a un generador de FAQs. Si necesitas escalar a múltiples mercados, rota países en ProxyHat y compara volúmenes de sugerencias por región para priorizar expansiones internacionales.






