Guide

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.

预付费,无订阅REST 或 MCP一个密钥搞定模型、数据和智能体

本页尚未翻译,暂以英文显示。

Google MapsWebEmaillovelycsvleads.csvnamephoneemailSmile Co+1 512hi@smiBright+1 512desk@bDentl+1 737care@dLake+1 512info@lOak+1 737team@oPearl+1 512yes@pe

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.

01

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:

Per place

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.

About 60 places per search term

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.

Only places with a website

"websiteFilter": "withWebsite" skips listings that have no site to read emails from.

02

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.

Python
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 []
03

Search, deduplicate, verify and write the CSV

Python
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.

04

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:

Python
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:

05

Keep the cost under control

Start small

Run the script with max_results=10 and one area, check the CSV, then scale up.

Know the maximum before you start

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.

Give the script its own key with a spend limit

A run that could take the key past its limit is refused with 402 before anything is charged. See API keys.

Don't pay twice

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.

06

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.

07

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.

一个预付费密钥,搞定模型、数据和智能体

无需订阅。充值后按需使用,每笔费用都能在日志中查看。