Zurück zum Blog

Google Maps mit Python scrapen: Was bricht und was hält

Ilyas Yıldırım
Ilyas Yıldırım
8 Min. Lesezeit

Tippen Sie "Google Maps mit Python scrapen" in ein Suchfeld, bekommen Sie hundert Anleitungen mit ungefähr derselben Form. requests importieren, BeautifulSoup importieren, das div finden, durchlaufen, CSV schreiben. Code kopieren, ausführen, leere Liste bekommen.

Der Code ist nicht direkt falsch. Er wurde für ein Web geschrieben, zu dem Google Maps seit Jahren nicht mehr gehört. Dieser Text geht die drei Wege durch, die Leute tatsächlich probieren, erklärt wo jeder endet, und schließt mit der Fassung, die weiterläuft, wenn Sie nicht mehr hinsehen.

Weg eins: requests und BeautifulSoup

Der Weg aus den meisten Anleitungen, und er scheitert sofort.

requests.get() lädt das HTML herunter, das der Server gesendet hat. Google Maps schickt ein kleines Gerüst und baut die gesamte Trefferliste danach im Browser mit JavaScript auf. Die Unternehmen stehen nicht in dem Dokument, das Sie geladen haben. Sie existieren erst, wenn eine Browser Engine die Seite ausgeführt hat.

BeautifulSoup zerlegt also eine Seite ohne Einträge, Ihre Schleife läuft null Mal, und Sie bekommen eine leere CSV mit korrekten Spaltenüberschriften. Nichts im Fehlerbild sagt Ihnen warum. Genau deshalb verbringen Leute einen Abend damit, Selektoren zu korrigieren, die nie hätten passen können.

Zeigt eine Anleitung das funktionierend, prüfen Sie das Datum und danach, ob die Ausgabe im Screenshot echt ist.

Weg zwei: Selenium oder Playwright

Dieser Weg funktioniert. Das ist die Falle.

Ein Browser ohne Oberfläche zeichnet die Seite richtig, die Einträge sind also wirklich da und wirklich lesbar. Sie starten Chrome, suchen, scrollen das Panel bis keine Zeilen mehr kommen und ziehen die Felder heraus. Am ersten Nachmittag fühlt es sich gelöst an.

So sehen die nächsten Monate aus.

Die Selektoren wandern. Google ändert die Oberfläche ständig, und generierte Klassennamen sind keine Zusage. Wenn sie sich ändern, wirft Ihr Skript keinen Fehler. Es liefert null Zeilen, oder schlimmer, Zeilen mit einer stillschweigend leeren Spalte.

Das Scrollen ist die eigentliche Arbeit. Neue Einträge laden erst beim Scrollen im Panel. Zu langsam, und ein großer Export dauert eine Stunde. Zu schnell, und Zeilen laden nicht, was Lücken statt Fehler erzeugt. Diesen Wartewert zu justieren wird zu einem Dauerthema.

Die Liste geht aus. Google liefert pro Suche nur eine feste Anzahl neuer Treffer. Diese Zahl liegt nirgends in der Nähe der Unternehmenszahl einer echten Stadt. Eine Metropole abzudecken heißt also, sie selbst in Stadtteile und Kategorien zu zerlegen und die Dateien danach zusammenzuführen.

Automatischer Verkehr wird als automatischer Verkehr behandelt. Wiederholte, schnelle Anfragen ohne Oberfläche von einer Adresse werden gebremst, geprüft oder blockiert. Das zu umgehen ist ein zweites Projekt mit eigenen laufenden Kosten, und es ist der Punkt, an dem aus einem Datenskript etwas wird, über das Sie in Ruhe nachdenken sollten.

Niemand pflegt es. Das Skript gehört dem, der es geschrieben hat. Wenn es im vierten Monat bricht, hat diese Person keine Zeit, und die Liste, die seit März niemand erneuert hat, ist still und leise falsch.

Nichts davon heißt, dass ein Browser ohne Oberfläche generell das falsche Werkzeug wäre. Es heißt, dass Google Maps im Listenmaßstab ein ungewöhnlich schlechtes Ziel dafür ist.

Weg drei: die Places API

Googles eigene Places API ist stabil, dokumentiert und bricht nicht, wenn sich die Oberfläche ändert. Für eine Filialsuche oder eine Adressvervollständigung in Ihrem Produkt ist sie die richtige Antwort, und nichts kommt in die Nähe.

Große Interessentenlisten sind eine andere Aufgabe, und das Preismodell zeigt das. Sie zahlen pro Aufruf, und die Preisstufe richtet sich nach dem teuersten Feld, das Sie anfragen. Ein Telefonfeld oder eine Bewertung kann den ganzen Aufruf in eine höhere Stufe heben. Dazu kommen Bedingungen, die begrenzen, wie lange Sie das Ergebnis aufbewahren dürfen.

Die Feldstufen und die Rechnung haben wir in Google Places API Preise durchgearbeitet, die Lizenzfrage in Places API oder Scraping. Kurz gesagt: eine API, um einem Nutzer Orte zu zeigen, nicht um ein CRM zu füllen.

Was tatsächlich hält

Die Fassung, die überlebt, ist die, in der Sie die Erhebung gar nicht pflegen. Sie rufen eine API auf, die sie bereits ausführt, und Ihr Python tut drei Dinge: Job senden, warten, Ergebnisse lesen.

Alles Folgende läuft mit der requests Bibliothek gegen die BasedOnBusiness REST API. Erstellen Sie einen Schlüssel in den Einstellungen unter API und Webhooks. Schlüssel beginnen mit bdb_live_.

Job senden

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": "zahnarzt-berlin-2026-08"},
    json={
        "query": "Zahnarzt",
        "country": "DE",
        "state": "DE.BE",
        "city": "Berlin",
        "target_leads": 200,
    },
    timeout=30,
)
response.raise_for_status()
job = response.json()
print(job["id"], job["status"])

Zwei Details lohnen sich. Das Feld state nutzt das GeoNames Format mit Punkt, also DE.BE, US.CA, TR.34. Länder ohne Untergliederung nehmen nur country. Wenn Sie unsicher sind, liefern die Endpunkte /geodata/countries, /geodata/states und /geodata/cities die gültigen Werte.

Der Header Idempotency-Key ist wichtiger, als er aussieht. Läuft Ihre Anfrage in eine Zeitüberschreitung und Sie wiederholen sie, verhindert dieser Schlüssel, dass ein zweiter Job entsteht und Ihre Credits doppelt verbraucht werden.

Auf das Ende warten

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"Job endete als {job['status']}")
        print(f"{job['status']} {job.get('progress', 0):.0%} · {job['leads_found']} gefunden")
        time.sleep(poll_seconds)
    raise TimeoutError("Job wurde nicht rechtzeitig fertig")

wait_for(job["id"])

Fünf Sekunden sind höflich und liegen bequem im Limit, das bei 100 Anfragen pro Minute und Schlüssel liegt. Fragen Sie nicht alle 200 Millisekunden, nur weil die Schleife es zulässt.

Ergebnisse durchblättern

Ergebnisse kommen seitenweise mit einem Cursor. Das page Objekt sagt Ihnen, ob es weitergeht.

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 erlaubt bis zu 500. Jede Zeile enthält place_id, title, category, address, phone, website, rating, reviews_count, latitude, longitude, price_level und business_status, dazu enrich_status und email_enrich_status, die anzeigen, ob die Anreicherung über die Website für diesen Datensatz fertig ist.

Datei schreiben

import pandas as pd

frame = pd.DataFrame(fetch_all(job["id"]))
with_site = frame[frame["website"].notna()]
print(f"{len(frame)} Datensaetze, {len(with_site)} mit Website")
frame.to_csv("zahnarzt-berlin.csv", index=False)

Das ist das ganze Programm. Keine Selektoren, keine Scrollzeiten, kein Browser, und nichts, das sich ändert, wenn Google ein Panel neu gestaltet.

Statt Warteschleife lieber Webhook

Für ein Skript, das Sie selbst starten, ist die Warteschleife in Ordnung. In einer Pipeline soll der Job Ihnen Bescheid geben.

Registrieren Sie einen Webhook Endpunkt, dann trifft beim Abschluss ein scrape.done Ereignis ein, mit einem X-Webhook-Signature Header, den Sie gegen das einmalig angezeigte Signaturgeheimnis prüfen. Fehlgeschlagene Zustellungen werden nach 1 Minute, 5 Minuten, 30 Minuten und 2 Stunden wiederholt. Speichern Sie die X-Webhook-Event-Id und überspringen Sie bereits verarbeitete Ereignisse, denn ein Wiederholungsversuch kann eintreffen, nachdem Sie die erste Zustellung verarbeitet haben.

Kurz zum rechtlichen Teil

Python zu wählen ändert keine Regel. Öffentliche Unternehmensdaten dürfen fast überall breit erhoben werden, personenbezogene Daten brauchen eine Rechtsgrundlage, und Googles Bedingungen begrenzen die automatische Erhebung von seinen eigenen Diensten, unabhängig vom Werkzeug. Die Ansprache folgt wieder eigenen Gesetzen, und die gelten für die Liste, egal woher sie stammt.

Der praktische Unterschied zwischen eigenem Scraper und Anbieter ist, wer diese Verantwortung trägt. Die Abgrenzungen stehen in Ist Google Maps Scraping legal.

Was Sie bauen sollten

Wenn Sie lernen, schreiben Sie die Selenium Fassung. Das ist eine wirklich gute Übung, und Sie verstehen danach jedes Werkzeug besser, das Sie kaufen.

Wenn etwas vom Ergebnis abhängt, lassen Sie es. Die Wartungskosten sind real, sie kommen zu ungünstigen Zeitpunkten, und in der Schätzung, die Sie Ihrem Team gegeben haben, tauchen sie nicht auf.

Ein grober Test: speist die Liste regelmäßig etwas, oder liest sie jemand außer Ihnen, dann wollen Sie eine API. Ist es eine einmalige Recherche für Sie selbst, geht alles, auch ein Export ganz ohne Code.

An echten Daten ausprobieren

Neue Konten bekommen einmalig 50 Export Credits ohne Karte, wobei 1 Credit einem Unternehmensdatensatz entspricht. Das reicht für die Skripte oben von vorne bis hinten: kleinen Job senden, Status abfragen, Ergebnisse blättern und die CSV öffnen.

Unternehmen mit Website erhalten die dort veröffentlichten E-Mail-Adressen, Social Profile und Technologiesignale ohne zusätzliche Credits, samt Quellseite. Die Datensätze kommen also fertig für den nächsten Schritt Ihrer Pipeline an. Die vollständige API Referenz enthält jeden Endpunkt, jeden Fehlercode und jedes Webhook Format.