Volver al Blog

Scrapear Google Maps con Python: qué se rompe y qué funciona

Ilyas Yıldırım
Ilyas Yıldırım
8 min de lectura

Escribe "scrapear Google Maps con Python" en un buscador y te salen cien tutoriales con más o menos la misma forma. Importa requests, importa BeautifulSoup, busca el div, recorre, escribe el CSV. Copias el código, lo ejecutas y obtienes una lista vacía.

El código no está mal exactamente. Está escrito para una web de la que Google Maps dejó de formar parte hace años. Este artículo recorre los tres caminos que la gente prueba de verdad, explica dónde termina cada uno y acaba con la versión que sigue funcionando cuando dejas de mirarla.

Camino uno: requests y BeautifulSoup

Es el de casi todos los tutoriales, y falla al instante.

requests.get() descarga el HTML que envió el servidor. Google Maps manda un esqueleto pequeño y después construye toda la lista de resultados en el navegador con JavaScript. Los negocios no están en el documento que descargaste. No existen hasta que un motor de navegador ejecuta la página.

Así que BeautifulSoup analiza una página sin ninguna ficha dentro, tu bucle se ejecuta cero veces y obtienes un CSV vacío con las cabeceras correctas. Nada en el error te dice por qué, y por eso la gente se pasa una tarde ajustando selectores que nunca iban a coincidir con nada.

Si un tutorial enseña esto funcionando, mira la fecha y después mira si la captura de pantalla es real.

Camino dos: Selenium o Playwright

Este funciona. Esa es la trampa.

Un navegador sin interfaz dibuja la página correctamente, así que las fichas están de verdad y se pueden leer. Lanzas Chrome, buscas, haces scroll en el panel hasta que deja de añadir filas y sacas los campos. La primera tarde parece que lo has resuelto.

Los meses siguientes son así.

Los selectores se mueven. Google cambia la interfaz constantemente y los nombres de clase generados no son ningún compromiso público. Cuando cambian, tu script no da error. Devuelve cero filas, o peor, devuelve filas con una columna vacía en silencio.

El scroll es todo el trabajo. Las fichas nuevas se cargan a medida que recorres el panel. Demasiado lento y una exportación grande tarda una hora. Demasiado rápido y las filas no llegan a cargarse, lo que produce huecos en vez de errores. Ajustar ese tiempo de espera se convierte en parte permanente de tu vida.

La lista se acaba. Google deja de dar resultados nuevos pasado cierto número por búsqueda. Ese número no se acerca al número de negocios de una ciudad real, así que cubrir un área metropolitana significa partirla tú en barrios y categorías y juntar los archivos después.

El tráfico automático se trata como tráfico automático. Peticiones repetidas, rápidas y sin interfaz desde una misma dirección se ralentizan, se cuestionan o se bloquean. Rodear eso es un segundo proyecto con su propio coste, y es el punto en el que un script de recogida de datos se convierte en algo sobre lo que conviene pensar con calma.

Nadie lo mantiene. El script es de quien lo escribió. Cuando se rompa en el cuarto mes, esa persona estará ocupada, y la lista que nadie ha actualizado desde marzo estará mal sin que nadie se entere.

Nada de esto significa que un navegador sin interfaz sea mala herramienta en general. Significa que Google Maps, a escala de construir listas, es un objetivo especialmente malo para uno.

Camino tres: la Places API

La propia Places API de Google es estable, está documentada y no se rompe cuando cambia la interfaz. Para un localizador de tiendas o un autocompletado de direcciones dentro de tu producto es la respuesta correcta y no hay nada que se le acerque.

Las listas masivas de prospectos son otro trabajo y el modelo de precios lo refleja. Pagas por llamada, y el nivel de precio lo marca el campo más caro que pidas, así que añadir un teléfono o una valoración a una petición puede mover la llamada entera a un nivel superior. Además hay términos que limitan cuánto tiempo puedes conservar lo que recibes.

Repasamos los niveles de campos y las cuentas en precios de la Google Places API, y la cuestión de la licencia en Places API frente a scraping. En corto: es una API para enseñar sitios a un usuario, no para llenar un CRM.

Lo que sí sigue funcionando

La versión que sobrevive es aquella en la que tú no mantienes la recogida. Llamas a una API que ya la ejecuta, y tu Python hace tres cosas: enviar un trabajo, esperar y leer los resultados.

Todo lo que viene funciona contra la API REST de BasedOnBusiness, con la librería requests. Crea una clave en Ajustes, en API y Webhooks. Las claves empiezan por bdb_live_.

Enviar el trabajo

import os
import requests

BASE = "https://www.basedonb.com/api/v1"
KEY = os.environ["BDB_API_KEY"]
HEADERS = {"Authorization": f"Bearer {KEY}"}

response = requests.post(
    f"{BASE}/scrapes",
    headers={**HEADERS, "Idempotency-Key": "dentistas-madrid-2026-08"},
    json={
        "query": "dentista",
        "country": "ES",
        "city": "Madrid",
        "target_leads": 200,
    },
    timeout=30,
)
response.raise_for_status()
job = response.json()
print(job["id"], job["status"])

Dos detalles que conviene conocer. El campo state usa el formato con punto de GeoNames, es decir ES.29, US.CA, DE.BE. Los países sin subdivisión aceptan solo country. Si no sabes qué espera un país, los endpoints /geodata/countries, /geodata/states y /geodata/cities devuelven los valores válidos.

La cabecera Idempotency-Key importa más de lo que parece. Si tu petición agota el tiempo y la reintentas, esa clave evita que se cree un segundo trabajo y que se gasten tus créditos dos veces.

Esperar a que termine

import time

def wait_for(job_id, poll_seconds=5, timeout_seconds=1800):
    deadline = time.time() + timeout_seconds
    while time.time() < deadline:
        job = requests.get(f"{BASE}/scrapes/{job_id}", headers=HEADERS, timeout=30).json()
        if job["status"] == "done":
            return job
        if job["status"] in ("failed", "cancelled"):
            raise RuntimeError(f"El trabajo termino como {job['status']}")
        print(f"{job['status']} {job.get('progress', 0):.0%} · {job['leads_found']} encontrados")
        time.sleep(poll_seconds)
    raise TimeoutError("El trabajo no termino a tiempo")

wait_for(job["id"])

Cinco segundos es un intervalo educado y queda holgado dentro del límite, que es de 100 peticiones por minuto y clave. No preguntes cada 200 milisegundos solo porque el bucle te deje.

Recorrer los resultados

Los resultados llegan por páginas con un cursor. El objeto page te dice si hay que seguir.

def fetch_all(job_id, page_size=500):
    rows, cursor = [], None
    while True:
        params = {"limit": page_size}
        if cursor:
            params["cursor"] = cursor
        page = requests.get(
            f"{BASE}/scrapes/{job_id}/results",
            headers=HEADERS,
            params=params,
            timeout=60,
        ).json()
        rows.extend(page["results"])
        if not page["page"]["has_more"]:
            return rows
        cursor = page["page"]["next_cursor"]

limit acepta hasta 500. Cada fila trae place_id, title, category, address, phone, website, rating, reviews_count, latitude, longitude, price_level y business_status, además de enrich_status y email_enrich_status, que te dicen si el enriquecimiento desde el sitio web terminó para ese registro.

Escribir el archivo

import pandas as pd

frame = pd.DataFrame(fetch_all(job["id"]))
with_site = frame[frame["website"].notna()]
print(f"{len(frame)} registros, {len(with_site)} con sitio web")
frame.to_csv("dentistas-madrid.csv", index=False)

Ese es el programa entero. Sin selectores, sin tiempos de scroll, sin navegador y sin nada que cambie cuando Google rediseñe un panel.

Deja de preguntar, usa un webhook

El bucle está bien para un script que lanzas tú. Dentro de un proceso automático quieres que el trabajo te avise.

Registra un endpoint de webhook y recibirás un evento scrape.done al terminar, con una cabecera X-Webhook-Signature que verificas contra el secreto de firma que se muestra una sola vez al crearlo. Las entregas fallidas se reintentan al minuto, a los 5 minutos, a los 30 minutos y a las 2 horas. Guarda el X-Webhook-Event-Id e ignora los eventos que ya procesaste, porque un reintento puede llegar después de que atendieras la primera entrega.

Una nota sobre la parte legal

Elegir Python no cambia ninguna regla. La información pública de empresas se puede recoger en general en casi todas partes, los datos personales necesitan una base legal y los términos de Google limitan la recogida automática desde sus propios servicios, sea cual sea la herramienta. El contacto comercial se rige por otras leyes distintas, y se aplican a la lista venga de donde venga.

La diferencia práctica entre escribir tu propio scraper y llamar a un proveedor es quién carga con esa responsabilidad. Lo detallamos en ¿es legal scrapear Google Maps?.

Cuál construir

Si estás aprendiendo, escribe la versión con Selenium. Es un ejercicio muy bueno y después entenderás mejor cualquier herramienta que compres.

Si algo depende del resultado, no lo hagas. El coste de mantenimiento es real, llega siempre en mal momento y no aparece en la estimación que le diste a tu equipo.

Una prueba rápida: si la lista alimenta algo de forma periódica, o la lee alguien más que tú, quieres una API. Si es algo puntual para tu propia investigación, sirve cualquier cosa, incluida una exportación sin escribir código.

Pruébalo con datos reales

Las cuentas nuevas reciben 50 créditos de exportación por una vez, sin tarjeta, donde 1 crédito equivale a 1 registro de empresa. Da para ejecutar los scripts de arriba de principio a fin: enviar un trabajo pequeño, consultarlo, recorrer los resultados y abrir el CSV.

A los negocios con sitio web se les añaden los correos publicados ahí, los perfiles sociales y las señales de tecnología sin coste extra de créditos, y se guarda la página de origen, así que los registros llegan listos para lo que venga después en tu proceso. La referencia completa de la API tiene todos los endpoints, códigos de error y formatos de webhook.