Scrapear People Also Ask y Autocomplete de Google para Keyword Research (Guía Python)

Guía práctica para desarrolladores y SEOs: extrae sugerencias de Autocomplete, preguntas People Also Ask y Related Searches con Python, proxies residenciales y Playwright.

Scrape Google People Also Ask and Autocomplete for Keyword Research (Python Guide)

Cómo scrapear People Also Ask y Autocomplete de Google para keyword research

Si haces keyword research en serio, ya sabes que las herramientas SaaS de pago te cobran decenas o cientos de dólares al mes por datos que Google regala públicamente en su propia página de resultados. Scrapear People Also Ask y el endpoint de Autocomplete de Google te da acceso a tres minas de oro gratuitas: las sugerencias de autocompletado, las preguntas PAA expandibles y las búsquedas relacionadas. Esta guía te muestra cómo construir un pipeline en Python que convierte una keyword semilla en cientos de long-tails, usando proxies residenciales para evitar bloqueos y sesgos de localización.

El objetivo es scrapear People Also Ask y Google Autocomplete de forma fiable y reproducible, con código que puedes ejecutar hoy mismo. Cubrimos desde la llamada HTTP cruda al endpoint de sugerencias hasta la automatización con Playwright para expandir acordeones PAA de forma recursiva.

Las tres minas de oro gratuitas de keywords de Google

Google expone tres fuentes públicas de datos de intención de búsqueda que la mayoría de herramientas de keyword research simplemente reempaquetan y venden:

1. Autocomplete (suggestqueries.google.com)

El endpoint de autocompletado de Google devuelve sugerencias en tiempo real mientras el usuario escribe. La URL pública es:

https://suggestqueries.google.com/complete/search?client=chrome&q=keyword

Con client=chrome recibes un array JSON con hasta 10 sugerencias. Cada sugerencia refleja lo que la gente realmente está buscando en ese momento, lo que lo convierte en la fuente más fresca de long-tails disponible. Las sugerencias se ordenan por volumen e interés relativo en la región del IP del solicitante, de ahí la importancia del geolocalización que veremos más adelante.

Según la documentación de Mozilla Developer Network sobre cabeceras User-Agent, el servidor adapta la respuesta según el cliente declarado, por lo que usar un User-Agent de navegador real mejora la calidad de las sugerencias.

2. People Also Ask (PAA)

El bloque People Also Ask aparece en la mayoría de SERPs orgánicas. Contiene preguntas relacionadas que los usuarios hacen en Google, presentadas como un acordeón expandible. Cada vez que abres una pregunta, Google carga 2-3 preguntas adicionales de forma asíncrona, lo que permite una expansión recursiva: una sola pregunta semilla puede generar decenas de preguntas hijas.

Cada pregunta PAA incluye un fragmento de respuesta y un enlace a la fuente citada, lo que te da no solo la pregunta sino también la URL de la autoridad que Google considera relevante para responderla.

3. Related Searches

Al final de cada SERP, Google muestra 8 búsquedas relacionadas. Son menos que las sugerencias de Autocomplete (típicamente 8 frente a 10), pero están curadas a nivel de intención completa, no de prefijo. Combinar las tres fuentes da una cobertura mucho más amplia que cualquier herramienta individual.

FuenteVolumen por consultaTipo de datoNecesita render JS
Autocomplete~10 sugerenciasPrefijo long-tailNo (JSON nativo)
People Also Ask4 iniciales + recursivasPreguntas + fuentesSí (Playwright)
Related Searches~8 términosIntención completaSí (Playwright)

Construir un expansor de seed a long-tail

Una keyword semilla como "running shoes" por sí sola genera ~10 sugerencias. Pero si añades los prefijos de la A a la Z y modificadores comunes, multiplicas el resultado por 30-50x. La técnica consiste en iterar sobre cada letra del alfabeto y cada modificador, enviando una consulta al endpoint de Autocomplete para cada combinación.

import httpx
import itertools
import json
import time

# --- Expansor de seed a long-tail usando Autocomplete ---

AUTOCOMPLETE_URL = "https://suggestqueries.google.com/complete/search"

MODIFIERS = [
    "best", "how to", "why", "what is", "vs", "review",
    "cheap", "alternative", "for", "near me", "2025", "2026",
]

def expand_seed(keyword: str, proxy_url: str | None = None) -> list[str]:
    """Expande una keyword semilla en cientos de long-tails."""
    all_suggestions = set()
    
    # Configuración del proxy residencial
    client_kwargs = {
        "timeout": 15.0,
        "headers": {
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
            "Accept-Language": "en-US,en;q=0.9",
        },
    }
    if proxy_url:
        client_kwargs["prox"] = proxy_url
    
    # Combinaciones: keyword + letra, letra + keyword, keyword + modificador
    queries = []
    for c in "abcdefghijklmnopqrstuvwxyz":
        queries.append(f"{keyword} {c}")
        queries.append(f"{c} {keyword}")
    for mod in MODIFIERS:
        queries.append(f"{mod} {keyword}")
        queries.append(f"{keyword} {mod}")
    
    with httpx.Client(**client_kwargs) as client:
        for q in queries:
            try:
                resp = client.get(AUTOCOMPLETE_URL, params={
                    "client": "chrome",
                    "q": q,
                    "hl": "en",
                })
                resp.raise_for_status()
                data = resp.json()
                # data[1] contiene el array de sugerencias
                suggestions = data[1] if len(data) > 1 else []
                all_suggestions.update(suggestions)
                time.sleep(0.3)  # throttle educado
            except (httpx.HTTPError, json.JSONDecodeError) as e:
                print(f"Error para '{q}': {e}")
                continue
    
    return sorted(all_suggestions)

# Ejemplo de uso con ProxyHat
proxy = "http://user-country-US:pass@gate.proxyhat.com:8080"
results = expand_seed("running shoes", proxy_url=proxy)
print(f"Generadas {len(results)} sugerencias")
for r in results[:20]:
    print(f"  {r}")

Con una semilla de "running shoes", este expansor genera típicamente entre 300 y 600 sugerencias únicas dependiendo del nicho. Si repites el proceso para 10 semillas, obtienes 3.000-6.000 long-tails sin pagar un centavo a una herramienta SaaS.

Por qué necesitas proxies residenciales para scrapear Autocomplete y PAA

Google aplica rate limiting por IP en sus endpoints públicos. Si envías más de ~80-100 peticiones por minuto desde una sola IP, empezarás a ver respuestas vacías, timeouts o, en el peor caso, un bloqueo temporal de 24 horas. El problema se agrava con el expansor A-Z, que dispara 64+ consultas por semilla.

Además, las sugerencias de Autocomplete y los resultados PAA están fuertemente localizados. Una consulta desde una IP de Madrid devolverá sugerencias distintas a la misma consulta desde una IP de Berlín o de Nueva York. Si tu keyword research es para un mercado específico, necesitas IPs de ese país.

Los proxies de datacenter son fáciles de detectar porque sus rangos de IP pertenecen a centros de datos conocidos (AWS, DigitalOcean, etc.). Google los filtra más agresivamente. Los proxies residenciales, en cambio, provienen de ISPs reales y son indistinguibles de un usuario doméstico, lo que reduce drásticamente la tasa de bloqueo.

Según la documentación sobre rate limiting en Wikipedia, las técnicas de limitación de peticiones son una práctica estándar para proteger servicios web, y Google no es una excepción. La estrategia correcta es distribuir tus peticiones entre múltiples IPs residenciales con rotación.

Configuración de geo-targeting con ProxyHat

ProxyHat permite especificar país y ciudad directamente en el usuario del proxy, lo que es crítico para obtener sugerencias localizadas:

# Alemania, Berlín
http://user-country-DE-city-berlin:pass@gate.proxyhat.com:8080

# Estados Unidos, Nueva York
http://user-country-US-city-newyork:pass@gate.proxyhat.com:8080

# España, Madrid
http://user-country-ES-city-madrid:pass@gate.proxyhat.com:8080

# Sesión fija (sticky) para mantener la misma IP entre peticiones
http://user-country-US-session-mykwresearch01:pass@gate.proxyhat.com:8080

Para keyword research en un mercado concreto, usa una sesión sticky del país objetivo. Para expansión masiva sin sesgo local, rota IPs por petición cambiando el identificador de sesión en cada llamada.

Implementación completa: Autocomplete + PAA con Python y Playwright

El siguiente ejemplo combina dos enfoques: httpx para el endpoint JSON de Autocomplete (rápido, sin render) y Playwright para expandir los acordeones PAA de forma recursiva (necesita JavaScript). Ambos usan proxies residenciales de ProxyHat.

Paso 1: Autocomplete con httpx y rotación de proxies

import httpx
import json
import random
import string
import logging
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_user: str, proxy_pass: str):
        self.user = proxy_user
        self.passw = proxy_pass
        self.session_counter = 0
    
    def _proxy_url(self, country: str = "US", rotate: bool = True) -> str:
        """Genera URL de proxy ProxyHat con rotación opcional."""
        if rotate:
            session_id = f"sess{''.join(random.choices(string.ascii_lowercase, k=6))}"
            username = f"{self.user}-country-{country}-session-{session_id}"
        else:
            username = f"{self.user}-country-{country}"
        return f"http://{username}:{self.passw}@gate.proxyhat.com:8080"
    
    def fetch_suggestions(
        self,
        keyword: str,
        country: str = "US",
        hl: str = "en",
        max_retries: int = 3,
    ) -> list[str]:
        """Obtiene sugerencias de Autocomplete con reintentos."""
        proxy = self._proxy_url(country, rotate=True)
        
        headers = {
            "User-Agent": (
                "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                "AppleWebKit/537.36 (KHTML, like Gecko) "
                "Chrome/120.0.0.0 Safari/537.36"
            ),
            "Accept-Language": f"{hl},{hl.split('-')[0]};q=0.9",
        }
        
        for attempt in range(max_retries):
            try:
                with httpx.Client(proxy=proxy, timeout=15.0, headers=headers) as client:
                    resp = client.get(self.BASE_URL, params={
                        "client": "chrome",
                        "q": keyword,
                        "hl": hl,
                        "gl": country.lower(),
                    })
                    resp.raise_for_status()
                    data = resp.json()
                    suggestions = data[1] if len(data) > 1 else []
                    logger.info(f"'{keyword}' → {len(suggestions)} sugerencias")
                    return suggestions
            except (httpx.HTTPError, json.JSONDecodeError) as e:
                logger.warning(f"Intento {attempt+1}/{max_retries} fallido para '{keyword}': {e}")
                # Rotar a nueva IP en cada reintento
                proxy = self._proxy_url(country, rotate=True)
        
        logger.error(f"Todas las búsquedas fallaron para '{keyword}'")
        return []
    
    def expand_with_alphabet(self, seed: str, country: str = "US") -> list[str]:
        """Expande una semilla con prefijos A-Z y modificadores."""
        all_suggestions = set()
        prefixes = list(string.ascii_lowercase) + [
            "how", "what", "why", "when", "where", "best", "cheap",
            "vs", "review", "alternative", "near me", "2025", "2026",
        ]
        
        for prefix in prefixes:
            query = f"{seed} {prefix}"
            suggestions = self.fetch_suggestions(query, country=country)
            all_suggestions.update(suggestions)
        
        return sorted(all_suggestions)

# Uso
scraper = AutocompleteScraper(proxy_user="user", proxy_pass="pass")
longtails = scraper.expand_with_alphabet("vpn", country="US")
print(f"Total long-tails generados: {len(longtails)}")

Paso 2: PAA recursivo con Playwright

Para las preguntas People Also Ask necesitamos un navegador real porque las preguntas se cargan dinámicamente al hacer clic en cada acordeón. Playwright nos permite automatizar esto:

import asyncio
import json
import logging
from playwright.async_api import async_playwright, Page, TimeoutError

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

PROXY_SERVER = "gate.proxyhat.com:8080"
PROXY_USER = "user-country-US-session-paa01"
PROXY_PASS = "pass"

async def scrape_paa(keyword: str, max_depth: int = 3, max_questions: int = 50) -> list[dict]:
    """
    Scrapea preguntas PAA de forma recursiva expandiendo acordeones.
    Devuelve lista de {question, answer_snippet, source_url}.
    """
    questions = []
    seen = set()
    
    async with async_playwright() as p:
        browser = await p.chromium.launch(
            headless=True,
            proxy={
                "server": f"http://{PROXY_SERVER}",
                "username": PROXY_USER,
                "password": PROXY_PASS,
            },
        )
        context = await browser.new_context(
            user_agent=(
                "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                "AppleWebKit/537.36 (KHTML, like Gecko) "
                "Chrome/120.0.0.0 Safari/537.36"
            ),
            locale="en-US",
            viewport={"width": 1280, "height": 1024},
        )
        page = await context.new_page()
        
        url = f"https://www.google.com/search?q={keyword}&gl=us&hl=en"
        logger.info(f"Navegando a: {url}")
        await page.goto(url, wait_until="domcontentloaded", timeout=30000)
        await page.wait_for_timeout(2000)
        
        async def extract_paa_block(page: Page) -> list[dict]:
            """Extrae preguntas PAA visibles en la página actual."""
            return await page.evaluate("""() => {
                const results = [];
                const paaItems = document.querySelectorAll(
                    '[jsname] .related-question-pair, '
                    + 'div[jscontroller] .wDYxhc:not([data-question])'
                );
                // Selector más robusto para PAA
                const allBlocks = document.querySelectorAll('.related-question-pair');
                allBlocks.forEach(block => {
                    const qEl = block.querySelector('.JlZRey, .CSkcDe, [role=button] span');
                    if (qEl) {
                        const question = qEl.textContent.trim();
                        results.push({question: question});
                    }
                });
                return results;
            }""")
        
        async def click_and_expand(page: Page, index: int):
            """Hace clic en el acordeón PAA número `index` para expandirlo."""
            try:
                paa_buttons = await page.query_selector_all('.related-question-pair [role=button]')
                if index < len(paa_buttons):
                    await paa_buttons[index].click()
                    await page.wait_for_timeout(1500)
                    return True
            except TimeoutError:
                logger.warning(f"Timeout expandiendo acordeón {index}")
            return False
        
        # Extraer preguntas iniciales
        initial = await extract_paa_block(page)
        for item in initial:
            q = item.get("question", "")
            if q and q not in seen:
                seen.add(q)
                questions.append(item)
                logger.info(f"PAA encontrada: {q}")
        
        # Expansión recursiva
        for depth in range(max_depth):
            if len(questions) >= max_questions:
                break
            
            current_count = len(questions)
            for i in range(current_count):
                if len(questions) >= max_questions:
                    break
                expanded = await click_and_expand(page, i)
                if expanded:
                    new_items = await extract_paa_block(page)
                    for item in new_items:
                        q = item.get("question", "")
                        if q and q not in seen:
                            seen.add(q)
                            questions.append(item)
                            logger.info(f"PAA anidada (depth {depth+1}): {q}")
            
        await browser.close()
    
    return questions[:max_questions]

# Ejecutar
async def main():
    paa_questions = await scrape_paa("what is vpn", max_depth=4, max_questions=80)
    print(json.dumps(paa_questions[:10], indent=2, ensure_ascii=False))

asyncio.run(main())

Paso 3: Extracción de fuentes citadas en PAA

Cada bloque PAA expandido incluye un enlace a la fuente de la respuesta. Podemos extraer ese dato para identificar qué sitios Google considera autoridades para cada tema:

async def extract_paa_with_sources(page: Page) -> list[dict]:
    """Extrae preguntas PAA con fragmento de respuesta y URL fuente."""
    return await page.evaluate("""() => {
        const results = [];
        const blocks = document.querySelectorAll('.related-question-pair');
        blocks.forEach(block => {
            const qEl = block.querySelector('[role=button] span, .JlZRey');
            const aEl = block.querySelector('.ILfuVd, .IZ6rdc');
            const linkEl = block.querySelector('a[href]');
            
            if (qEl) {
                results.push({
                    question: qEl.textContent.trim(),
                    answer: aEl ? aEl.textContent.trim().substring(0, 300) : null,
                    source_url: linkEl ? linkEl.href : null,
                    source_text: linkEl ? linkEl.textContent.trim() : null,
                });
            }
        });
        return results;
    }""")

Deduplicación, clustering por intención y exportación

Una vez que tienes cientos de keywords de Autocomplete y decenas de preguntas PAA, necesitas procesarlas para que sean accionables. Los pasos clave son:

  • Deduplicación: normaliza a minúsculas, elimina espacios extra y compara.
  • Clustering por intención: agrupa keywords por intención de búsqueda (informacional, transaccional, navegacional, local).
  • Exportación a CSV: para integrar con herramientas de contenido o calendarios editoriales.
import csv
import re
from collections import defaultdict

# Patrones de intención
INTENT_PATTERNS = {
    "informacional": [
        r"^what is", r"^how does", r"^why", r"^when", r"^who",
        r"^what are", r"^difference between",
    ],
    "transaccional": [
        r"best", r"buy", r"cheap", r"discount", r"deal",
        r"review", r"vs", r"alternative",
    ],
    "navegacional": [
        r"login", r"download", r"official", r"site:",
    ],
    "local": [
        r"near me", r"in ", r"location", r"store",
    ],
}

def classify_intent(keyword: str) -> str:
    kw_lower = keyword.lower().strip()
    for intent, patterns in INTENT_PATTERNS.items():
        if any(re.search(p, kw_lower) for p in patterns):
            return intent
    return "otro"

def deduplicate(keywords: list[str]) -> list[str]:
    seen = set()
    unique = []
    for kw in keywords:
        normalized = re.sub(r"\s+", " ", kw.lower().strip())
        if normalized not in seen:
            seen.add(normalized)
            unique.append(kw)
    return unique

def cluster_by_intent(keywords: list[str]) -> dict[str, list[str]]:
    clusters = defaultdict(list)
    for kw in deduplicate(keywords):
        intent = classify_intent(kw)
        clusters[intent].append(kw)
    return dict(clusters)

def export_to_csv(
    autocomplete_keywords: list[str],
    paa_questions: list[dict],
    output_path: str = "keyword_research.csv",
):
    """Exporta keywords y preguntas PAA a CSV con clustering por intención."""
    clusters = cluster_by_intent(autocomplete_keywords)
    
    with open(output_path, "w", newline="", encoding="utf-8") as f:
        writer = csv.writer(f)
        writer.writerow([
            "keyword", "fuente", "intencion", "respuesta_snippet",
            "fuente_url", "fuente_texto",
        ])
        
        # Keywords de Autocomplete
        for intent, kws in clusters.items():
            for kw in kws:
                writer.writerow([kw, "autocomplete", intent, "", "", ""])
        
        # Preguntas PAA
        for item in paa_questions:
            q = item.get("question", "")
            intent = classify_intent(q)
            writer.writerow([
                q,
                "paa",
                intent,
                item.get("answer", ""),
                item.get("source_url", ""),
                item.get("source_text", ""),
            ])
    
    total = len(autocomplete_keywords) + len(paa_questions)
    print(f"Exportadas {total} filas a {output_path}")
    print(f"  Autocomplete: {len(autocomplete_keywords)}")
    print(f"  PAA: {len(paa_questions)}")
    print(f"  Clusters: {dict((k, len(v)) for k, v in clusters.items())}")

# Ejecutar pipeline completo
if __name__ == "__main__":
    # Simular datos obtenidos
    autocomplete_kws = [
        "what is vpn", "best vpn 2026", "vpn free", "vpn vs proxy",
        "how does vpn work", "vpn for gaming", "cheap vpn",
    ]
    paa_data = [
        {"question": "Is VPN legal?", "answer": "Yes in most countries...", "source_url": "https://example.com", "source_text": "Example"},
        {"question": "What does a VPN hide?", "answer": "Your IP address...", "source_url": None, "source_text": None},
    ]
    
    export_to_csv(autocomplete_kws, paa_data, "keyword_research.csv")

El CSV resultante es directamente importable en Google Sheets, Notion o cualquier herramienta de planificación de contenido. Las preguntas PAA con intención informacional son ideales para planificación de FAQs, mientras que las transaccionales alimentan páginas de comparación y guías de compra.

Errores comunes y casos límite

  • Respuestas vacías de Autocomplete: Google devuelve [] cuando detecta scraping. Solución: reduce la velocidad a 1 petición cada 2-3 segundos por IP y rota proxies.
  • Selectores PAA cambiantes: Google actualiza su HTML frecuentemente. Usa selectores múltiples como fallback y registra el HTML cuando la extracción devuelva 0 resultados.
  • Sesgo de localización: sin geo-targeting, las sugerencias reflejan el país del IP del proxy, no necesariamente tu mercado objetivo. Especifica -country-XX en el usuario del proxy.
  • Preguntas duplicadas en PAA recursivo: el mismo set de preguntas puede reaparecer al expandir acordeones profundos. Usa un set() de preguntas vistas.
  • Captchas intermitentes: si Google muestra un captcha, cambia de IP inmediatamente. Con ProxyHat, rotar el session ID en el username te da una nueva IP residencial al instante.

Configuración de ProxyHat y enlaces útiles

Para empezar a scrapear con proxies residenciales, configura tu cuenta en ProxyHat y usa los siguientes parámetros de conexión:

ParámetroValor
Gatewaygate.proxyhat.com
Puerto HTTP8080
Puerto SOCKS51080
Geo-targeting-country-XX-city-yyy en username
Sesión sticky-session-abc123 en username

Recursos adicionales para tu pipeline de keyword research:

Ética y consideraciones legales

Los datos de Autocomplete y PAA son información pública que Google muestra a cualquier usuario. Sin embargo, el scraping a escala implica responsabilidades:

  • Respeta robots.txt: revisa las reglas de Google para los paths que scrapeas. Aunque el endpoint de Autocomplete no está cubierto por robots.txt de forma estándar, la página de búsqueda sí.
  • Throttle educado: mantén tus peticiones por debajo de ~60/minuto por IP. No necesitas más velocidad para keyword research.
  • Preferir APIs oficiales a escala: si necesitas millones de keywords, considera el Google Search Console API o la Google Keyword Planner API. El scraping es ideal para proyectos de pequeña y mediana escala.
  • GDPR y CCPA: los datos de sugerencias no contienen información personal, pero ten cuidado de no combinarlos con datos de usuarios identificables.

Consejo práctico: Para keyword research de un solo mercado (ej. España), usa una sesión sticky con -country-ES y mantén la misma IP durante toda la sesión. Para investigación multi-país, rota IPs por petición cambiando el session ID.

Puntos clave (Key Takeaways)

  • Las tres fuentes gratuitas —Autocomplete, PAA y Related Searches— cubren distintos niveles de intención de búsqueda y se complementan entre sí.
  • El expansor A-Z + modificadores convierte una semilla en 300-600 long-tails con una sola pasada.
  • Los proxies residenciales con geo-targeting son imprescindibles para evitar rate limits (~80-100 peticiones/min por IP) y sesgos de localización.
  • Playwright es necesario para PAA recursivo porque las preguntas se cargan dinámicamente al expandir acordeones.
  • El clustering por intención (informacional, transaccional, local, navegacional) convierte listas planas de keywords en planes de contenido accionables.
  • Para volúmenes masivos, prefiere APIs oficiales; el scraping es ideal para proyectos de hasta ~10.000 keywords.

¿Listo para empezar?

Accede a más de 50M de IPs residenciales en más de 148 países con filtrado impulsado por IA.

Ver preciosProxies residenciales
← Volver al Blog