
Scraper Google Maps avec Python : ce qui casse et ce qui tient
Tapez « scraper Google Maps avec Python » dans un moteur et vous obtenez cent tutoriels de la même forme. Importer requests, importer BeautifulSoup, trouver le div, boucler, écrire le CSV. Vous copiez le code, vous l'exécutez, vous obtenez une liste vide.
Le code n'est pas exactement faux. Il a été écrit pour un web dont Google Maps ne fait plus partie depuis des années. Cet article parcourt les trois voies que les gens essaient vraiment, explique où chacune s'arrête, et se termine par la version qui continue de tourner quand vous ne la regardez plus.
Voie une : requests et BeautifulSoup
Celle de la plupart des tutoriels, et elle échoue tout de suite.
requests.get() télécharge le HTML envoyé par le serveur. Google Maps envoie une petite coquille puis construit toute la liste de résultats dans le navigateur avec JavaScript. Les entreprises ne sont pas dans le document que vous avez téléchargé. Elles n'existent qu'une fois qu'un moteur de navigateur a exécuté la page.
BeautifulSoup analyse donc une page sans aucune fiche, votre boucle tourne zéro fois, et vous obtenez un CSV vide avec les bons en-têtes. Rien dans l'erreur ne vous dit pourquoi, et c'est pour cela que les gens passent une soirée à ajuster des sélecteurs qui n'allaient jamais correspondre à quoi que ce soit.
Si un tutoriel montre cela en train de fonctionner, regardez la date, puis regardez si la sortie de la capture d'écran est réelle.
Voie deux : Selenium ou Playwright
Celle-ci fonctionne. C'est là le piège.
Un navigateur sans interface dessine correctement la page, donc les fiches sont bien là et bien lisibles. Vous lancez Chrome, vous cherchez, vous faites défiler le panneau jusqu'à ce qu'il n'ajoute plus de lignes, et vous extrayez les champs. Le premier après-midi, vous avez l'impression d'avoir résolu le problème.
Voici à quoi ressemblent les mois suivants.
Les sélecteurs bougent. Google modifie son interface en permanence, et les noms de classes générés ne sont un engagement envers personne. Quand ils changent, votre script ne lève pas d'erreur. Il renvoie zéro ligne, ou pire, des lignes avec une colonne vide en silence.
Le défilement est tout le travail. Les nouvelles fiches se chargent au fur et à mesure du défilement. Trop lent, et un gros export prend une heure. Trop rapide, et les lignes ne se chargent pas, ce qui produit des trous et non des erreurs. Régler cette temporisation devient une part permanente de votre vie.
La liste s'épuise. Google cesse de fournir de nouveaux résultats après un certain nombre par recherche. Ce nombre est loin du compte réel des entreprises d'une ville, donc couvrir une agglomération veut dire la découper vous-même en quartiers et catégories puis fusionner les fichiers.
Le trafic automatisé est traité comme du trafic automatisé. Des requêtes répétées, rapides et sans interface depuis une même adresse sont ralenties, mises à l'épreuve ou bloquées. Contourner cela est un second projet avec son propre coût, et c'est le moment où un script de collecte devient quelque chose sur quoi il faut réfléchir posément.
Personne ne le maintient. Le script appartient à celui qui l'a écrit. Quand il casse au quatrième mois, cette personne est occupée, et la liste que personne n'a rafraîchie depuis mars est fausse sans bruit.
Rien de tout cela ne veut dire qu'un navigateur sans interface est un mauvais outil en général. Cela veut dire que Google Maps, à l'échelle d'une liste, est une cible particulièrement mauvaise pour ce type d'outil.
Voie trois : l'API Places
L'API Places de Google est stable, documentée et ne casse pas quand l'interface change. Pour un localisateur de magasins ou une saisie d'adresse dans votre produit, c'est la bonne réponse et rien ne s'en approche.
Les grandes listes de prospects sont un autre travail, et le modèle de prix le montre. Vous payez par appel, et le palier de prix est fixé par le champ le plus cher que vous demandez. Ajouter un téléphone ou une note à une requête peut donc faire basculer tout l'appel dans un palier supérieur. S'y ajoutent des conditions qui limitent la durée de conservation des données reçues.
Nous avons travaillé les paliers de champs et les calculs dans tarifs de l'API Google Places, et la question de la licence dans API Places ou scraping. En bref : c'est une API pour montrer des lieux à un utilisateur, pas pour remplir un CRM.
Ce qui tient vraiment
La version qui survit est celle où vous ne maintenez pas la collecte du tout. Vous appelez une API qui la fait déjà tourner, et votre Python fait trois choses : envoyer un travail, attendre, lire les résultats.
Tout ce qui suit tourne contre l'API REST de BasedOnBusiness, avec la bibliothèque requests. Créez une clé dans Réglages, sous API et Webhooks. Les clés commencent par bdb_live_.
Envoyer le travail
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": "dentistes-lyon-2026-08"},
json={
"query": "dentiste",
"country": "FR",
"city": "Lyon",
"target_leads": 200,
},
timeout=30,
)
response.raise_for_status()
job = response.json()
print(job["id"], job["status"])
Deux détails utiles. Le champ state utilise le format pointé de GeoNames, donc US.CA, DE.BE, TR.34. Les pays sans subdivision n'acceptent que country. Si vous ne savez pas ce qu'attend un pays, les points de terminaison /geodata/countries, /geodata/states et /geodata/cities renvoient les valeurs acceptées.
L'en-tête Idempotency-Key compte plus qu'il n'en a l'air. Si votre requête expire et que vous réessayez, cette clé empêche la création d'un second travail et la double dépense de vos crédits.
Attendre la fin
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"Travail termine en {job['status']}")
print(f"{job['status']} {job.get('progress', 0):.0%} · {job['leads_found']} trouves")
time.sleep(poll_seconds)
raise TimeoutError("Le travail n'a pas fini a temps")
wait_for(job["id"])
Cinq secondes est un intervalle poli et reste très en dessous de la limite, qui est de 100 requêtes par minute et par clé. N'interrogez pas toutes les 200 millisecondes juste parce que la boucle le permet.
Parcourir les résultats
Les résultats arrivent par pages avec un curseur. L'objet page vous dit s'il faut continuer.
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 accepte jusqu'à 500. Chaque ligne porte place_id, title, category, address, phone, website, rating, reviews_count, latitude, longitude, price_level et business_status, plus enrich_status et email_enrich_status qui indiquent si l'enrichissement depuis le site est terminé pour cet enregistrement.
Écrire le fichier
import pandas as pd
frame = pd.DataFrame(fetch_all(job["id"]))
with_site = frame[frame["website"].notna()]
print(f"{len(frame)} fiches, {len(with_site)} avec un site")
frame.to_csv("dentistes-lyon.csv", index=False)
C'est tout le programme. Pas de sélecteurs, pas de temporisation de défilement, pas de navigateur, et rien qui change quand Google refait un panneau.
Arrêtez d'interroger, utilisez un webhook
La boucle convient pour un script que vous lancez vous-même. Dans une chaîne automatisée, vous voulez que le travail vous prévienne.
Enregistrez un point de terminaison et un événement scrape.done arrive à la fin du travail, avec un en-tête X-Webhook-Signature que vous vérifiez contre le secret de signature affiché une seule fois à la création. Les livraisons échouées sont réessayées après 1 minute, 5 minutes, 30 minutes et 2 heures. Stockez le X-Webhook-Event-Id et ignorez les événements déjà traités, car un réessai peut arriver après que vous avez traité la première livraison.
Une note sur la partie légale
Choisir Python ne change aucune règle. Les informations publiques d'entreprise sont largement collectables dans la plupart des pays, les données personnelles exigent une base légale, et les conditions de Google limitent la collecte automatisée depuis ses propres services quel que soit l'outil. La prospection suit encore d'autres lois, et elles s'appliquent à la liste d'où qu'elle vienne.
La différence pratique entre écrire son propre scraper et appeler un fournisseur, c'est qui porte cette responsabilité. Les distinctions sont détaillées dans scraper Google Maps, est-ce légal.
Lequel construire
Si vous apprenez, écrivez la version Selenium. C'est un très bon exercice et vous comprendrez mieux chaque outil que vous achèterez ensuite.
Si quelque chose dépend du résultat, ne le faites pas. Le coût de maintenance est réel, il tombe toujours au mauvais moment, et il n'apparaît pas dans l'estimation que vous avez donnée à votre équipe.
Un test rapide : si la liste alimente quelque chose régulièrement, ou si quelqu'un d'autre que vous la lit, vous voulez une API. Si c'est un coup unique pour votre propre recherche, tout convient, y compris un export sans écrire une ligne de code.
Essayez sur des données réelles
Les nouveaux comptes reçoivent 50 crédits d'export une seule fois, sans carte, où 1 crédit vaut 1 fiche entreprise. De quoi exécuter les scripts ci-dessus de bout en bout : envoyer un petit travail, l'interroger, parcourir les résultats et ouvrir le CSV.
Les entreprises qui ont un site reçoivent aussi les e-mails publiés dessus, les profils sociaux et les signaux techniques sans crédit supplémentaire, avec la page source enregistrée. Les fiches arrivent donc prêtes pour l'étape suivante de votre chaîne. La référence complète de l'API contient chaque point de terminaison, chaque code d'erreur et chaque format de webhook.