How to scrape Google Maps leads to CSV with Python
Search Google Maps for local businesses, add the emails and social links from their websites, verify every address and save a deduplicated CSV in Python.
このページはまだ翻訳されていないため、英語で表示しています。
A Google Maps lead list is a spreadsheet of local businesses: name, address, phone, website, rating and, ideally, an email address. This guide builds one with a Google Maps scraper in Python. It searches several areas, adds the contacts found on each business's website, removes duplicates, verifies every email and writes a CSV you can open in any spreadsheet or import into a CRM.
Where the data comes from
A Google Maps listing has the business name, category, address, phone, website, rating and review count. It doesn't list email addresses. Those are on the business's own website, so a lead tool has to read the site too.
The Google Maps leads tool does both in one run: it finds places for your search terms and location, then reads each website for emails, phones and social profile links. Each item has the place fields plus emails (a list) and socialProfiles (lists of facebook, instagram, linkedin and other links).
How a run is billed and bounded:
You pay for each place returned. max_results caps the run at up to 200 places, and starting the run holds the per-place fee times max_results from your balance. When it ends, it settles to what it returned.
A nearby search returns about 60 places at most for each term. Pass several related terms in searchTerms to cover a broad category; the results are merged and deduplicated within the run.
"websiteFilter": "withWebsite" skips listings that have no site to read emails from.
Set up the run helper
You need an API key in LOVELY_API_KEY and requests. This helper follows Run a tool: it starts a run, polls while the answer is 202, and returns the items.
import csv
import os
import requests
BASE = "https://lovelyrouter.com/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['LOVELY_API_KEY']}"}
def run_tool(tool, tool_input, max_results):
res = requests.post(
f"{BASE}/tools/{tool}/run",
headers=HEADERS,
json={"input": tool_input, "max_results": max_results},
timeout=90,
)
run = res.json()
if not res.ok:
raise RuntimeError(run["error"]["message"])
# 202 = still running: poll, letting the server wait up to 60 s per request.
while res.status_code == 202:
res = requests.get(f"{BASE}/tools/runs/{run['id']}", headers=HEADERS, params={"wait": 60}, timeout=90)
res.raise_for_status()
run = res.json()
print(f"{tool}: {run['status']}, {run['results']} results, charged {run['charge_usd']} USD")
return run["items"] or []Search, deduplicate, verify and write the CSV
TERMS = ["dentist", "dental clinic"]
AREAS = ["Miami, FL, USA", "Fort Lauderdale, FL, USA"]
# 1. Search each area. Areas can overlap, so keep each place once.
places = {}
for area in AREAS:
items = run_tool(
"google-maps-leads",
{"searchTerms": TERMS, "location": area, "websiteFilter": "withWebsite"},
max_results=100,
)
for p in items:
places.setdefault(p["placeId"], p)
print(len(places), "unique places")
# 2. Verify each distinct address once, up to 500 per run.
emails = sorted({e.strip().lower() for p in places.values() for e in p.get("emails") or []})
checks = {}
for i in range(0, len(emails), 500):
batch = set(emails[i : i + 500])
for check in run_tool("email-verify", {"emails": sorted(batch)}, max_results=len(batch)):
# Each check includes the address it tested: index it by that address.
addr = next((v.lower() for v in check.values() if isinstance(v, str) and v.lower() in batch), None)
if addr:
checks[addr] = check
# 3. One CSV row per business and email address.
check_fields = sorted({k for c in checks.values() for k, v in c.items() if not isinstance(v, (dict, list))})
fields = ["name", "category", "address", "phone", "website", "email", "rating", "reviews",
"instagram", "facebook", "linkedin", "maps_url", "place_id"] + [f"check_{k}" for k in check_fields]
with open("leads.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=fields)
writer.writeheader()
for p in places.values():
social = p.get("socialProfiles") or {}
row = {
"name": p.get("title"),
"category": (p.get("categories") or [""])[0],
"address": p.get("formattedAddress"),
"phone": p.get("phoneIntl") or p.get("phone"),
"website": p.get("website"),
"rating": p.get("rating"),
"reviews": p.get("reviewCount"),
"instagram": ";".join(social.get("instagram") or []),
"facebook": ";".join(social.get("facebook") or []),
"linkedin": ";".join(social.get("linkedin") or []),
"maps_url": p.get("googleMapsUri"),
"place_id": p["placeId"],
}
for email in sorted({e.strip().lower() for e in p.get("emails") or []}) or [""]:
check = checks.get(email, {})
writer.writerow({**row, "email": email, **{f"check_{k}": check.get(k) for k in check_fields}})Deduplication happens at three levels. placeId is stable, so a place found in two overlapping areas is kept once. Emails are lowercased and stripped before they're verified, so you pay for each address once. To keep one row per company instead of one per location, key places by websiteDomain instead of placeId: branches of a chain usually share a website.
Verification. Email verification checks each address's syntax, domain, MX records and mailbox status. The script copies every top-level field of the check into check_ columns. Print one check to see which field holds the verdict, then filter the CSV on it before you send anything.
The CSV. The csv module quotes commas and line breaks in names and addresses for you. newline="" stops blank lines on Windows, and UTF-8 keeps accented business names intact.
Places first, contacts second
When you want to filter places before paying for contacts, split the job. Google Maps places returns the same place fields without the website crawl and adds filters such as minRating and openNow. Then send only the websites you want to website contacts, which reads up to 5 pages per site and returns emails, phones and links such as linkedIns and instagrams:
places = run_tool(
"google-maps-places",
{"searchTerms": ["dentist"], "location": "Miami, FL, USA", "minRating": 4.5},
max_results=50,
)
sites = [{"url": p["website"]} for p in places if p.get("website")][:50]
contacts = run_tool("website-contacts", {"startUrls": sites, "maxDepth": 1}, max_results=len(sites))
for c in contacts:
print(c["domain"], c["emails"])website-contacts takes up to 50 sites per run and bills per website plus a per-call fee, so it pays to filter first. Compare today's prices:
Google マップの店舗・事業者と、その Web サイトから取得したメール・電話・SNS リンク。地域のリードリスト作成向け。
キーワードと地域で Google マップ上の店舗・事業者を検索:名前、住所、電話番号、Web サイト、評価、レビュー数、営業時間、座標。
Web サイトをクロールし、メールアドレス、電話番号、SNS プロフィールのリンクを抽出します。
メールアドレスが届くか確認:形式、ドメイン、MX レコード、メールボックスの状態。
Keep the cost under control
Run the script with max_results=10 and one area, check the CSV, then scale up.
A run can never cost more than its hold: the per-call fee plus the per-result fee times max_results. The loop above runs at most one leads search per area and one verification per 500 addresses.
A run that could take the key past its limit is refused with 402 before anything is charged. See API keys.
Deduplicate addresses before verification, and save the CSV or the run ids: reading a finished run's results again with GET /v1/tools/runs/{run_id} is free.
There's no subscription. Every run comes out of one prepaid balance, listed with its charge in your dashboard. See pricing.
Use the data responsibly
Business listings are public, but a lead list can still hold personal data, such as a named person's work email. You are responsible for using it lawfully: have a lawful basis under the privacy laws that apply to you and the people in your list, honor opt-out and deletion requests, and don't send unsolicited bulk messages. Read the Acceptable Use Policy before you build an outreach list.
Frequently asked questions
How many businesses can one Google Maps run return?+
Up to 200 places per run, capped by the max_results you set. A nearby search finds about 60 places per search term, so pass several related terms and split large regions into several areas.
Why do some businesses have no email address?+
Emails come from the business's website, not from Google Maps. A site that only has a contact form, or no address on the pages that are read, gives no email. The place still comes back with its Google Maps details, such as the phone number and website.
How do I remove duplicate leads?+
Key places by placeId when you combine runs, lowercase email addresses before you verify them, and key by websiteDomain instead if you want one row per company rather than one per location.
Should I verify emails before I send to them?+
Yes. The email-verify tool checks syntax, domain, MX records and mailbox status for each address and bills per email, so verify the deduplicated list and pay once per address.
Can an AI agent build this CSV for me?+
Yes. Connect the MCP server to Claude Code or another MCP client and ask for the list in plain words. The agent plans with suggest_tools, runs the same tools with run_tool and can write the CSV itself.
For more lead sources, including LinkedIn companies and people, see lead generation. The Google Maps tools page lists reviews and places tools, and the MCP server lets an agent run this whole job from a prompt.