Les annuaires d'entreprises locales constituent l'un des ensembles de données publiques les plus utiles sur le web. Une recherche dans un annuaire comme "plombiers à Austin" ou "restaurants à Denver" retourne une grille structurée d'entreprises, chacune portant un nom, une adresse, un numéro de téléphone, une catégorie et une note étoilée avec un nombre d'avis. Les équipes commerciales, marketing et de recherche exploitent ces données pour constituer des listes de prospects par ville, enrichir les enregistrements CRM avec des coordonnées vérifiées et cartographier la densité concurrentielle dans les marchés. Les collecter à la main ne passe pas à l'échelle au-delà d'une poignée de résultats, ce travail appartient donc à un script.
Ce guide vous montre comment scraper des annonces d'entreprises locales avec Python de manière fiable. Vous construirez un petit scraper fonctionnel qui récupère une page de résultats d'annuaire rendue via la Crawling API, parse chaque carte d'annonce avec BeautifulSoup et extrait un enregistrement propre par entreprise : nom, adresse, téléphone, catégorie, note, nombre d'avis et site web. L'ensemble du tutoriel se limite aux informations commerciales publiques, et la section légale proche de la fin n'est pas une clause de style : lisez-la avant de pointer ce scraper sur des volumes importants.
Ce que vous allez construire
Un script Python qui prend une catégorie et une ville, récupère la page d'annonces rendue via la Crawling API et extrait un enregistrement structuré par entreprise. Nous utilisons une recherche Yellow Pages comme exemple fil rouge et extrayons ces champs de chaque carte de résultat :
- Name le nom de l'entreprise, par exemple "Austin Plumbing Co".
- Address l'adresse postale affichée sur la carte.
- Phone le numéro de téléphone professionnel publiquement listé.
- Category la catégorie principale sous laquelle l'annuaire classe l'entreprise.
- Rating la note étoilée moyenne, lorsque l'entreprise en a une.
- Reviews le nombre d'avis derrière cette note.
- Website le lien vers le propre site de l'entreprise, lorsqu'il est listé.
Pourquoi une requête ordinaire échoue sur les sites d'annonces
Collecter des annonces à grande échelle n'est pas aussi simple qu'envoyer des requêtes et parser du HTML, pour deux raisons qui se cumulent dès que vous dépassez quelques requêtes.
Premièrement, les résultats dépendent de la géolocalisation. Une requête nue comme "plombiers" retourne des entreprises complètement différentes selon que la requête semble provenir d'Austin, de Denver ou de Phoenix. Pour obtenir un ensemble de données cohérent, vous devez contrôler à la fois la requête (inclure la ville) et la localisation de la requête (géo-ciblage), sinon les résultats varient de manière imprévisible d'une exécution à l'autre.
Deuxièmement, les annuaires modernes se défendent contre le trafic automatisé et affichent de plus en plus les annonces côté client. Beaucoup de plateformes retournent une enveloppe HTML légère et injectent les véritables cartes d'entreprises avec JavaScript ensuite, donc une requête HTTP standard vous remet une page sans aucune annonce. Une fois que vous dépassez quelques requêtes, la plateforme commence également à appliquer des blocages d'IP, des défis CAPTCHA et du throttling. Un scraper fonctionnel a donc besoin de deux choses en une seule requête : un navigateur qui rend la page, et une IP que la plateforme considère comme un visiteur réel. Vous pouvez assembler cela vous-même avec un navigateur headless et un pool de proxies résidentiels rotatifs, mais connecter ces éléments et les maintenir en bon état représente l'essentiel du travail. La Crawling API regroupe les deux en un seul appel : vous lui envoyez l'URL, elle rend la page derrière une IP résidentielle de confiance et renvoie un HTML finalisé à parser.
Crawlbase propose deux types de tokens. Le token normal récupère le HTML statique ; le token JavaScript (JS) rend d'abord la page dans un vrai navigateur. Les pages d'annuaire statiques se parsent correctement avec le token normal, mais les plateformes qui injectent les annonces côté client (Google Maps, Yelp) ont besoin du token JS. Adaptez le token à la page : utilisez le token normal pour les pages simples et le token JS pour les pages dynamiques.
Prérequis
Quelques éléments doivent être en place avant d'écrire du code. Aucun ne prend longtemps.
Python de base. Vous devez être à l'aise pour écrire et exécuter un script Python et installer des packages avec pip. Si vous êtes novice avec BeautifulSoup, le guide sur comment utiliser BeautifulSoup en Python couvre les bases des sélecteurs que ce tutoriel utilise.
Python 3.8 ou version ultérieure. Confirmez votre version avec python --version. Si vous ne l'avez pas, installez-la depuis python.org ou via une distribution comme Anaconda.
Un compte Crawlbase et un token. Inscrivez-vous, ouvrez votre tableau de bord et copiez votre token depuis la page de documentation de votre compte. Traitez le token comme un mot de passe : il authentifie vos requêtes, alors gardez-le hors du contrôle de version.
Configurer le projet
Créez un environnement virtuel pour isoler les dépendances du projet, puis installez les deux bibliothèques dont le scraper a besoin.
python --version python -m venv listings_env source listings_env/bin/activate pip install crawlbase beautifulsoup4
Sous Windows, activez l'environnement avec listings_env\Scripts\activate à la place de la ligne source. Deux dépendances font le travail : crawlbase est le client officiel pour la Crawling API, et beautifulsoup4 analyse le HTML renvoyé pour vous permettre d'extraire chaque champ d'une carte d'annonce par sélecteur CSS.
Comprendre la page d'annonces
Une page de résultats d'annuaire présente une colonne de cartes d'annonces, une par entreprise. Chaque carte porte les mêmes quelques champs : un nom d'entreprise, une adresse postale, un numéro de téléphone, la catégorie sous laquelle elle est classée, et une note avec un nombre d'avis. Un lien "visiter le site web" figure sur la carte lorsque l'entreprise en a fourni un. Sous la colonne se trouvent des contrôles de pagination permettant de parcourir des pages de résultats supplémentaires pour la même requête.
Avant d'écrire des sélecteurs, ouvrez une page de résultats dans votre navigateur, faites un clic droit sur une carte d'annonce et choisissez Inspecter. Sur Yellow Pages, chaque résultat est enveloppé dans un conteneur div.result, avec le nom dans a.business-name, l'adresse dans div.street-address et div.locality, le téléphone dans div.phones, la catégorie principale dans div.categories, la note exposée via la classe sur div.result-rating, le nombre d'avis dans span.count, et le site web dans a.track-visit-website. Ce sont les sélecteurs que vous ciblez.
Étape 1 : Récupérer la page d'annonces rendue
Commencez par obtenir la page finale. Importez la classe CrawlingAPI, initialisez-la avec votre token, construisez l'URL de recherche à partir d'une catégorie et d'une ville, et demandez-la. Vérifier le code de statut avant de parser permet de rendre les échecs visibles plutôt que silencieux.
from urllib.parse import quote_plus from crawlbase import CrawlingAPI api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"}) def build_url(category, city): terms = quote_plus(category) geo = quote_plus(city) return f"https://www.yellowpages.com/search?search_terms={terms}&geo_location_terms={geo}" def crawl(page_url): options = {"country": "US"} response = api.get(page_url, options) if response["status_code"] == 200: return response["body"].decode("latin1") print(f"Request failed: {response['status_code']}") return None if __name__ == "__main__": url = build_url("plumbers", "Austin, TX") html = crawl(url) print(html[:500] if html else "No HTML returned")
L'utilitaire build_url assemble l'URL de recherche à partir de deux paramètres de requête : search_terms pour la catégorie et geo_location_terms pour la ville, tous deux encodés en URL pour que les espaces et les virgules survivent au trajet. L'option country ancre la requête à une IP américaine, ce qui constitue la partie géo-ciblage : une requête pour "plumbers in Austin" ne retourne des résultats sensés que lorsque la requête semble aussi provenir du bon marché. Le corps est décodé en latin1 car les pages d'annuaire mélangent des caractères sur lesquels le décodage strict UTF-8 peut échouer. Exécutez le script et vous devriez voir de véritables balises d'annonces, pas une enveloppe vide ou une page de blocage. Cela confirme que la récupération fonctionne avant d'écrire un seul sélecteur.
Ce seul appel api.get a accompli la partie qui prend généralement une semaine : il a récupéré la page d'annonces derrière une IP résidentielle de confiance ancrée dans le bon pays, de sorte que l'annuaire a retourné de vraies cartes au lieu d'une page de blocage. La Crawling API gère le rendu, la rotation des IPs et le géo-ciblage pour vous, vous dispensant d'exploiter vous-même une flotte de navigateurs headless et un pool de proxies. Pointez-la sur une ville dans le cadre du niveau gratuit d'abord.
Étape 2 : Parser les cartes d'annonces avec BeautifulSoup
Une fois le HTML en main, chargez-le dans BeautifulSoup, trouvez chaque carte d'annonce et extrayez chaque champ par son sélecteur. Chaque entreprise est enveloppée dans un conteneur div.result, avec le nom, l'adresse, le téléphone, la catégorie, la note, le nombre d'avis et le site web exposés chacun via leur propre classe. Enveloppez chaque carte dans un try/except pour qu'une annonce mal formée ne fasse pas planter l'exécution.
from bs4 import BeautifulSoup def text_of(card, selector): el = card.select_one(selector) return el.get_text(strip=True) if el else None def parse_rating(card): el = card.select_one("div.result-rating") if not el: return None words = {"one": 1, "two": 2, "three": 3, "four": 4, "five": 5} rating = None for cls in el.get("class", []): base = cls.replace("-half", "") if base in words: rating = words[base] + (0.5 if "-half" in cls else 0) return rating def scrape_results(html): soup = BeautifulSoup(html, "html.parser") cards = soup.select("div.result") results = [] for card in cards: try: website = card.select_one("a.track-visit-website") results.append({ "name": text_of(card, "a.business-name"), "address": text_of(card, "div.street-address"), "locality": text_of(card, "div.locality"), "phone": text_of(card, "div.phones"), "category": text_of(card, "div.categories"), "rating": parse_rating(card), "reviews": text_of(card, "span.count"), "website": website["href"] if website else None, }) except Exception as e: print(f"Skipped a card: {e}") return results
L'utilitaire text_of interroge un seul élément à l'intérieur d'une carte et renvoie None lorsqu'il est absent, au lieu de lever une exception sur un appel .get_text() sur rien. Cela rend l'extraction robuste lorsqu'un champ est absent, ce qui est courant car toutes les annonces ne portent pas un site web ou une note. L'utilitaire parse_rating lit la note étoilée depuis la liste de classes sur div.result-rating, où l'annuaire écrit le score en mots comme four ou four half, et le convertit en nombre. Le nombre d'avis provient de span.count, et le site web est lu depuis le href de l'ancre plutôt que son texte. Les noms, adresses, téléphones et catégories sont chacun mappés sur leur propre sélecteur.
Les noms de classes des annuaires changent sans préavis. Traitez les sélecteurs ci-dessus comme un modèle de départ, pas comme un contrat. Quand un champ revient en None pour chaque carte, ré-inspectez une page de résultats en direct dans les outils de développement de votre navigateur et mettez à jour le sélecteur. La maintenance périodique des sélecteurs est normale pour tout scraper en production, ce n'est pas le signe que quelque chose est cassé.
Étape 3 : Assembler et exporter
Reliez maintenant la récupération et le parsing en un seul script fonctionnel, et écrivez les enregistrements en JSON et CSV pour qu'ils s'intègrent directement dans un tableur ou une base de données. Récupérez la page rendue, passez-la au parser, puis exportez les enregistrements structurés.
import csv import json from urllib.parse import quote_plus from crawlbase import CrawlingAPI from bs4 import BeautifulSoup api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"}) def build_url(category, city): terms = quote_plus(category) geo = quote_plus(city) return f"https://www.yellowpages.com/search?search_terms={terms}&geo_location_terms={geo}" def crawl(page_url): response = api.get(page_url, {"country": "US"}) if response["status_code"] == 200: return response["body"].decode("latin1") print(f"Request failed: {response['status_code']}") return None def text_of(card, selector): el = card.select_one(selector) return el.get_text(strip=True) if el else None def parse_rating(card): el = card.select_one("div.result-rating") if not el: return None words = {"one": 1, "two": 2, "three": 3, "four": 4, "five": 5} rating = None for cls in el.get("class", []): base = cls.replace("-half", "") if base in words: rating = words[base] + (0.5 if "-half" in cls else 0) return rating def scrape_results(html): soup = BeautifulSoup(html, "html.parser") results = [] for card in soup.select("div.result"): try: website = card.select_one("a.track-visit-website") results.append({ "name": text_of(card, "a.business-name"), "address": text_of(card, "div.street-address"), "locality": text_of(card, "div.locality"), "phone": text_of(card, "div.phones"), "category": text_of(card, "div.categories"), "rating": parse_rating(card), "reviews": text_of(card, "span.count"), "website": website["href"] if website else None, }) except Exception as e: print(f"Skipped a card: {e}") return results def save(rows, name): with open(f"{name}.json", "w") as f: json.dump(rows, f, indent=2) if rows: with open(f"{name}.csv", "w", newline="") as f: writer = csv.DictWriter(f, fieldnames=rows[0].keys()) writer.writeheader() writer.writerows(rows) def main(): url = build_url("plumbers", "Austin, TX") html = crawl(url) if not html: return data = scrape_results(html) save(data, "listings") print(json.dumps(data, indent=2)) if __name__ == "__main__": main()
À quoi ressemble le résultat
Exécutez le script complet avec python scraper.py et vous obtenez une liste propre d'enregistrements, un par entreprise, écrits dans listings.json et listings.csv et affichés dans la console.
[ { "name": "Austin Plumbing Co", "address": "1200 W 5th St", "locality": "Austin, TX 78703", "phone": "(512) 555-0142", "category": "Plumbers, Water Heaters", "rating": 4.5, "reviews": "(38)", "website": "https://www.austinplumbingco.example" }, { "name": "Lone Star Drain & Sewer", "address": "904 E Cesar Chavez St", "locality": "Austin, TX 78702", "phone": "(512) 555-0188", "category": "Plumbers", "rating": null, "reviews": null, "website": null } ]
Le deuxième enregistrement illustre la robustesse du code : cette entreprise n'a pas de note, pas de nombre d'avis et pas de site web enregistré, ces champs reviennent donc en null sans faire planter l'exécution. La version CSV porte les mêmes colonnes dans le même ordre, prête à ouvrir dans un tableur ou à charger dans une base de données. Si vous souhaitez un rappel sur l'aplatissement des enregistrements imbriqués en lignes, le guide sur scraper des tableaux depuis un site web couvre la même forme d'export.
Mise à l'échelle sur plusieurs villes et pages
Une requête dans une ville, c'est une démo. La vraie valeur des données d'annonces vient de l'exécution de la même catégorie dans de nombreux marchés, ce qui est aussi là où la cohérence importe le plus. Parcourez une liste de villes, paginez chacune avec le paramètre &page= jusqu'à ce qu'une page ne retourne plus de cartes, et collectez tout dans un seul ensemble de données.
import time def scrape_city(category, city, max_pages=5): base = build_url(category, city) collected = [] for page in range(1, max_pages + 1): html = crawl(f"{base}&page={page}") if not html: break rows = scrape_results(html) if not rows: break for row in rows: row["city"] = city collected.extend(rows) print(f"{city} page {page}: {len(rows)} listings") time.sleep(2) return collected def scrape_cities(category, cities): all_rows = [] for city in cities: all_rows.extend(scrape_city(category, city)) return all_rows data = scrape_cities("restaurants", ["Austin, TX", "Denver, CO", "Phoenix, AZ"]) save(data, "multi_city")
Le plafond max_pages maintient chaque ville bornée pour qu'une requête large ne tourne pas indéfiniment, et l'interruption sur résultats vides vous arrête tôt lorsque l'annuaire n'a plus de pages. Estampiller chaque ligne avec sa city garde les marchés séparables une fois tout réuni dans un seul fichier. Le time.sleep(2) entre les pages espace les requêtes pour ne pas surcharger l'annuaire dans une boucle serrée, ce qui est le moyen le plus rapide d'être limité. Cette boucle multi-villes est le même pattern derrière un outil de comparaison de prix : une requête, de nombreuses sources, normalisées en un seul ensemble de données.
Rester débloqué
Même avec la récupération prise en charge, les annuaires surveillent le trafic qui ressemble à des scrapers. Quelques habitudes maintiennent une exécution saine, et elles s'appliquent à toute cible commerciale.
-
Espacez vos requêtes. Répartissez les requêtes avec un délai entre les pages et variez vos requêtes plutôt que de crawler un terme à pleine vitesse. Le
time.sleepdans la boucle est le plancher, pas le plafond. - Misez sur la rotation. Un pool d'IPs résidentielles répartit les requêtes sur de nombreuses adresses d'utilisateurs réels, de sorte qu'aucune ne déclenche une limite de débit. La Crawling API s'en charge pour vous ; si vous gérez votre propre stack, c'est la partie à soigner.
- Adaptez la géolocalisation. Ancrez le pays de la requête au marché que vous interrogez pour que les résultats restent cohérents et que le trafic paraisse local plutôt que déplacé.
- Lisez les codes de statut. Une exécution qui commence à retourner des défis ou des erreurs vous indique que le débit ou le niveau d'IP actuel n'est plus suffisant. Traitez-le comme un signal pour ralentir, pas comme du bruit à ignorer.
Pour le guide de référence sur le maintien d'un scraper sain, consultez comment scraper des sites web sans être bloqué. Lorsque vous dépassez les requêtes à la demande et devez envoyer des milliers d'URLs ville-et-catégorie à la fois, le Crawler asynchrone traite de grands lots en arrière-plan et livre les résultats à un webhook ou dans Cloud Storage, vous permettant de réutiliser ce même parser sans gérer vous-même une file d'attente de requêtes.
Est-il légal de scraper des annonces d'entreprises ?
La légitimité du scraping d'un annuaire d'entreprises dépend des conditions d'utilisation de la plateforme, de votre juridiction et de ce que vous faites des données. La plupart des annuaires restreignent l'accès automatisé dans leurs conditions, le scraping peut donc aller à l'encontre de ces conditions quelle que soit la prudence de vos outils. Aucun code ici ne change cela ; il permet simplement que la partie technique fonctionne. Lisez les conditions d'utilisation de la plateforme et son fichier robots.txt, et traitez les deux comme la frontière de ce que vous collectez et à quelle vitesse.
Quelques lignes valent la peine d'être tenues. Collectez uniquement des informations commerciales publiques : les noms, adresses, numéros de téléphone, catégories, notes, nombres d'avis et liens de sites web que tout le monde peut voir sur une page de résultats sans compte. Les coordonnées listées d'une entreprise sont des données commerciales publiques, mais ne collectez pas de données personnelles sur des individus, y compris des coordonnées privées, des identités d'évaluateurs, ou quoi que ce soit lié à une personne nommée au-delà de ce que l'entreprise publie elle-même. Maintenez votre volume de requêtes suffisamment bas pour ne pas surcharger les serveurs de la plateforme, et respectez les attentes de débit qu'elle indique. Si vous prévoyez de réutiliser les données commercialement, obtenez une permission ou un accord officiel plutôt que de supposer que le silence est un consentement.
Ce guide est délibérément limité aux pages d'annonces publiques car c'est la ligne qui rend le travail défendable. Il ne couvre rien derrière une connexion, les données de compte, ou le scraping d'informations personnelles sur de vraies personnes. Lorsqu'une plateforme propose une voie sanctionnée, préférez-la : les fournisseurs de cartes et de lieux publient des APIs officielles qui retournent les mêmes champs d'annonces dans des conditions claires, et c'est le bon outil lorsque vous avez besoin de grands volumes, d'une structure garantie ou de droits commerciaux. Si votre projet nécessite plus que des annonces publiques, une API officielle ou un accord de données est la voie correcte, pas un scraper plus astucieux.
Points clés
- Les annonces dépendent de la géolocalisation. Incluez la ville dans la requête et ancrez le pays de la requête, sinon la même catégorie retourne des entreprises incohérentes d'une exécution à l'autre.
- La récupération est la partie difficile. La Crawling API récupère la page derrière une IP résidentielle de confiance et la rend si nécessaire, vous donnant de vraies cartes au lieu d'une page de blocage ou d'une enveloppe vide.
-
BeautifulSoup se charge de l'extraction. Bouclez sur les cartes
div.resultet mappez le nom, l'adresse, le téléphone, la catégorie, la note, les avis et le site web sur les sélecteurs actuels, en anticipant leur dérive. -
Mettez à l'échelle sur plusieurs villes et pages. Parcourez une liste de villes, paginez avec
&page=jusqu'à ce qu'une page soit vide, estampillez chaque ligne avec sa ville, et espacez les requêtes avec un délai. - Restez sur les données publiques. Respectez les CGU et le robots.txt de la plateforme, préférez une API officielle de cartes ou de lieux pour les données sous licence ou en volume, et ne collectez jamais d'informations personnelles sur des individus.
Foire aux questions
Ai-je besoin du token normal ou du token JS pour les annonces ?
Cela dépend de l'annuaire. Les pages de résultats statiques comme Yellow Pages se parsent correctement avec le token normal. Les plateformes qui injectent les annonces côté client, comme Google Maps et Yelp, ont besoin du token JS pour que la page soit rendue dans un vrai navigateur avant que le HTML soit retourné. Adaptez le token à la page : commencez avec le token normal et passez au token JS si les cartes sont absentes du corps.
Comment obtenir des résultats précis pour une ville spécifique ?
Deux choses ensemble. Mettez la ville dans la requête elle-même via le paramètre geo_location_terms, et ancrez la requête au bon marché avec l'option country sur la Crawling API. La recherche locale est liée à la localisation, donc une requête sans ces deux éléments retourne des résultats qui varient de manière imprévisible selon l'endroit d'où la requête semble provenir.
Puis-je scraper plusieurs villes en une seule exécution ?
Oui. Passez une liste de villes et bouclez la même catégorie sur chacune, en combinant les résultats dans un seul ensemble de données. Estampillez chaque enregistrement avec sa ville avant de les fusionner pour que les marchés restent séparables, et ajoutez un court délai entre les requêtes pour espacer l'exécution.
Mes sélecteurs retournent None. Qu'est-ce qui a changé ?
Presque certainement le balisage de l'annuaire. Les noms de classes comme a.business-name pour le nom ou div.result-rating pour la note changent sans préavis. Ré-inspectez une page de résultats en direct dans les outils de développement de votre navigateur, mettez à jour le sélecteur pour correspondre et relancez. La maintenance périodique des sélecteurs est normale pour tout scraper en production.
Puis-je scraper les coordonnées personnelles depuis les annonces ?
Non, et ce guide ne le couvre pas. Limitez-vous aux informations commerciales publiques : le nom de l'entreprise, l'adresse, le téléphone listé, la catégorie, la note, le nombre d'avis et le site web. Les données personnelles sur des individus, les coordonnées privées ou les identités d'évaluateurs sortent du périmètre et vont à l'encontre des conditions de la plupart des plateformes. Pour des données plus riches ou sous licence, la voie correcte est une API officielle de cartes ou de lieux.
Comment gérer de très grands travaux sur des centaines de villes ?
Pour le travail à la demande, la Crawling API suffit, mais lorsque vous envoyez des milliers d'URLs ville-et-catégorie à la fois, passez au Crawler asynchrone. Vous soumettez les URLs et recevez les résultats via un webhook ou Cloud Storage au lieu d'attendre chaque requête, ce qui améliore le débit et évite les goulots d'étranglement. Le même parser de ce guide gère le HTML retourné sans modification.
Crawlez n'importe quel site à grande échelle, sans combattre l'infrastructure.
Crawlbase gère les proxies, les empreintes et les CAPTCHA afin que votre équipe livre des pipelines de données au lieu de maintenir la plomberie de crawl. 1 000 requêtes gratuites, sans carte requise.

