Scrape Google Maps With Python: Full Tutorial With Working Code (2026)

Learn how to scrape data from Google Maps using Python: a working Selenium scraper with infinite scroll, plus an API method that does it in one request.

Half the Google Maps scraping tutorials on the internet are broken. While researching this post we ran the code from a top-ranking article, and the CSS selectors it relies on stopped existing years ago. The Reddit thread ranking #1 for this exact question: six years old, no working answer.

So we wrote the tutorial we wanted to find: a real answer to how to scrape data from Google Maps using Python. Everything below ran successfully this month, on the current interface, and pulled real data from 42 Austin coffee shops. You get two complete methods, and you can pick your fight:

Method 1: Selenium (DIY)

A full browser-automation scraper you build step by step: search, infinite scroll, card parsing, detail pages, CSV. Free, educational, and yours to maintain.

Method 2: API (one request)

The same data, plus fields Selenium can't reach, in about 20 lines of Python. If you're on a deadline, jump straight to the API method →

This guide takes you from the first pip install to a clean CSV: names, ratings, categories, addresses, phone numbers, and websites. We run scraping infrastructure for a living at FlyByAPIs, and the DIY section is honest about what breaks, because we’ve broken all of it ourselves.

42

Businesses scraped in our test run

~2 min

Selenium test run (42 + 5 details)

200

Results per API request

60

Max results, official Places API

In short: Google Maps renders everything with JavaScript, so you need Selenium to drive a real browser, scroll the results feed, and parse each card. That works, and we show every line, but it’s slow and fragile at scale, which is why the second method uses the FlyByAPIs Google Maps scraper API that returns up to 200 structured results per request. All the code is on GitHub: flybyapis/blog-web-scraping-code → scrape-google-maps-python . Clone it and run it before you read another word.


Why Google Maps is harder to scrape than a normal website

Open a Google Maps search, hit view-source, and look for a business name. It’s not there. The HTML your browser receives is a nearly empty shell, and every listing you see gets rendered by JavaScript after the page loads.

That single fact kills the classic requests + BeautifulSoup approach that works on static sites (the one we use in our general Python web scraping guide ). No JavaScript engine, no data.

Three more obstacles stack on top:

1

Infinite scroll instead of pagination

There is no page 2. Results load as you scroll a side panel, about 6 at a time, and stop at a hidden "end of list" marker.

2

Obfuscated, rotating CSS classes

Class names like Nv2PK are machine-generated and change without notice. Every tutorial hardcoding them has an expiry date.

3

Anti-bot detection

Google fingerprints automation and rate-limits IPs. Works fine for 50 pages, then the CAPTCHAs start.

Our Selenium scraper deals with the first two properly, and we’ll be straight with you about the third. Let’s build it.


How to scrape data from Google Maps using Python and Selenium

The plan: drive a real Chrome browser, search Google Maps, scroll the results feed until we have enough businesses, parse each card, then visit individual place pages for phone numbers and websites. Six steps, all runnable.

Step 1: Set up Selenium and ChromeDriver

You need Python 3.10+ and two packages. Selenium 4.6+ can resolve drivers on its own via Selenium Manager, but we still install webdriver-manager because it pins the ChromeDriver download explicitly, which behaves more predictably on CI:

1
pip install selenium webdriver-manager

Now the driver factory. The flags matter: --headless=new is the current headless mode (the old one is detectable and renders differently), and a realistic user agent avoids the most basic bot filter:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager

def build_driver(headless: bool = True) -> webdriver.Chrome:
    opts = Options()
    if headless:
        opts.add_argument("--headless=new")
    opts.add_argument("--window-size=1280,900")
    opts.add_argument("--lang=en-US")
    opts.add_argument(
        "user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
        "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
    )
    service = Service(ChromeDriverManager().install())
    return webdriver.Chrome(service=service, options=opts)

Google Maps accepts search queries directly in the URL, which saves us from automating the search box. One detail that broke our first test run: Google localizes the interface based on your IP and account, and our results came back in Catalan. Force hl=en or your parsing will fail in fun ways:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import time
from selenium.webdriver.common.by import By
from selenium.common.exceptions import NoSuchElementException

def accept_consent(driver) -> None:
    """Click through Google's cookie consent page if it shows up (EU IPs mostly)."""
    for label in ("Accept all", "Reject all"):
        try:
            driver.find_element(By.XPATH, f'//button[.//span[text()="{label}"]]').click()
            time.sleep(2)
            return
        except NoSuchElementException:
            continue

driver = build_driver()
query = "coffee shops in Austin"
driver.get(f"https://www.google.com/maps/search/{query.replace(' ', '+')}?hl=en")
time.sleep(4)
accept_consent(driver)

Those last five lines are a quick smoke test to confirm the page loads for you. The final script’s main() in Step 6 repeats them properly, so drop them before assembling the full file (or you’ll launch two Chrome instances).

Selector strategy that survives redesigns:

We never use Google's obfuscated class names. Everything below targets semantic attributes: div[role="feed"], a[href*="/maps/place/"], aria-label. Google changes cosmetic classes constantly but rarely touches accessibility attributes, because screen readers depend on them.

Step 3: Beat infinite scroll (the part every tutorial skips)

A fresh search shows about 6 to 12 results. The rest only exist after you scroll the results panel, and this is where most tutorials quietly stop. The trick is that you must scroll the feed element, not the page: window.scrollTo does nothing on Google Maps.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
FEED = 'div[role="feed"]'
CARD_LINK = 'a[href*="/maps/place/"]'
END_MARKER = "You've reached the end of the list."

def scroll_feed(driver, target: int, max_rounds: int = 40) -> None:
    """Scroll the results panel until we have `target` cards or hit the end."""
    feed = driver.find_element(By.CSS_SELECTOR, FEED)
    seen = 0
    stale_rounds = 0

    for _ in range(max_rounds):
        driver.execute_script("arguments[0].scrollTop = arguments[0].scrollHeight", feed)
        time.sleep(2)

        count = len(driver.find_elements(By.CSS_SELECTOR, f"{FEED} {CARD_LINK}"))
        if END_MARKER in feed.text:
            print(f"Reached the end of the list at {count} results.")
            return
        if count >= target:
            print(f"Collected {count} result cards.")
            return
        stale_rounds = stale_rounds + 1 if count == seen else 0
        if stale_rounds >= 3:
            print(f"No new results after 3 scrolls, stopping at {count}.")
            return
        seen = count
        print(f"Scrolled: {count} results loaded...")

Three exit conditions, and all of them matter in practice:

  • The end-of-list marker catches exhausted searches where fewer businesses exist than you asked for.
  • The target count stops you from collecting more than you need.
  • The stale counter saves you when Google throttles loading and the feed just stops growing.

In our test, reaching 40 coffee shops took 8 scroll rounds, about 20 seconds. Each round loads roughly 6 new businesses, and one search only goes so far: the feed exhausts itself at roughly 100 to 120 results, so a 5,000-business list means dozens of separate queries, each with its own scrolling session.

Step 4: Parse the result cards

Each card in the feed is a div containing a place link. The business name sits in the link’s aria-label, the star rating in a span[role="img"], and category plus street address share a text line separated by a middle dot:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import re

def parse_cards(driver) -> list[dict]:
    """Extract structured data from each loaded result card."""
    rows = []
    cards = driver.find_elements(By.CSS_SELECTOR, f"{FEED} > div")

    for card in cards:
        try:
            link = card.find_element(By.CSS_SELECTOR, CARD_LINK)
        except NoSuchElementException:
            continue  # spacer divs and sponsored slots have no place link

        name = link.get_attribute("aria-label") or ""
        row = {"name": name.strip(), "url": link.get_attribute("href")}

        # The star rating lives in an aria-label like "4.5 stars"
        try:
            stars = card.find_element(By.CSS_SELECTOR, 'span[role="img"]')
            m = re.search(r"[\d.]+", stars.get_attribute("aria-label") or "")
            if m:
                row["rating"] = float(m.group())
        except NoSuchElementException:
            pass

        # Category and address share a line: "Coffee shop · 507 Pressler St"
        for line in card.text.split("\n"):
            if "·" in line and "star" not in line.lower():
                parts = [p.strip(" ·,") for p in line.split("·") if p.strip(" ·,")]
                if parts and not any(ch.isdigit() for ch in parts[0]):
                    row["category"] = parts[0]
                    if len(parts) > 1:
                        row["address"] = parts[-1]
                    break

        if row["name"]:
            rows.append(row)
    return rows

Notice what’s not here: review counts. The current list view doesn’t render them in a parseable way, and review text lives behind another click and another infinite scroll. This is the point where DIY costs start compounding, and where the business reviews endpoint of a Google Maps API turns a scraping subproject into one GET request.

Step 5: Visit each place page for phone and website

The cards give you name, rating, category, and street. Phone numbers and websites need a visit to each place page. The good news: place pages use data-item-id attributes, which are semantic and stable:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
def fetch_details(driver, rows: list[dict], limit: int = 10) -> None:
    """Visit each place page to add phone, website, and full address."""
    for row in rows[:limit]:
        driver.get(row["url"])
        time.sleep(4)

        fields = {
            "phone": 'button[data-item-id^="phone"]',
            "website": 'a[data-item-id="authority"]',
            "full_address": 'button[data-item-id="address"]',
        }
        for key, selector in fields.items():
            try:
                label = driver.find_element(By.CSS_SELECTOR, selector).get_attribute("aria-label") or ""
                row[key] = label.split(":", 1)[-1].strip()
            except NoSuchElementException:
                row[key] = ""
        print(f"Details: {row['name']} | {row['phone']} | {row['website']}")

The bad news is the cost. Every business is now a full page load, roughly 5 to 6 seconds each. Enriching 100 businesses adds 10 minutes of browser time to your run, and 10,000 businesses is a 16-hour job, per search, per city.

Step 6: Write the CSV and run it

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import csv, sys

def write_csv(rows: list[dict], path: str = "google_maps_results.csv") -> None:
    columns = ["name", "rating", "category", "address", "phone", "website", "full_address", "url"]
    with open(path, "w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=columns, extrasaction="ignore")
        writer.writeheader()
        writer.writerows(rows)
    print(f"Wrote {len(rows)} rows to {path}")

def main() -> None:
    query = sys.argv[1] if len(sys.argv) > 1 else "coffee shops in Austin"
    target = int(sys.argv[2]) if len(sys.argv) > 2 else 60

    driver = build_driver(headless=True)
    try:
        driver.get(f"https://www.google.com/maps/search/{query.replace(' ', '+')}?hl=en")
        time.sleep(4)
        accept_consent(driver)
        scroll_feed(driver, target)
        rows = parse_cards(driver)
        fetch_details(driver, rows, limit=5)
        write_csv(rows)
    finally:
        driver.quit()

if __name__ == "__main__":
    main()

Here’s real output from our verification run this month:

1
2
3
4
5
6
7
8
9
Scrolled: 26 results loaded...
Scrolled: 32 results loaded...
Scrolled: 38 results loaded...
Collected 42 result cards.
Details: Epoch Coffee | +1 512-454-3762 | epochcoffee.com
Details: Jo's Coffee – South Congress | +1 512-852-2300 | joscoffee.com
Details: Mozart's Coffee Roasters | +1 512-477-2900 | mozartscoffee.com
... (2 more)
Wrote 42 rows to google_maps_results.csv

It works! A real scraper, running against the live site, with names, ratings, categories, addresses, phones, and websites in a spreadsheet. If you only need a one-off list of 50 businesses, you can genuinely stop here.


Why this scraper will eventually break (and when)

We’d be lying if we ended the DIY section on “it works!”. We run scraping infrastructure for thousands of daily requests, and here is what happens to this exact script at scale.

Google will flag your IP. Past a few hundred page loads a day from one IP, you start seeing CAPTCHAs and empty responses. We wrote a whole post on why web scrapers get blocked , but the short version: fingerprinting plus rate patterns, and residential proxies to work around it start around $50/month and climb fast.

Selectors rot silently. Our aria-based selectors are the most durable choice available, but Google ships UI experiments constantly: the GeeksforGeeks tutorial ranking on page one for this keyword still references class names from an interface that no longer exists.

"Nobody emails you when your scraper dies; your pipeline just delivers zero rows."

The data has holes. No review counts in the list view, no emails, no opening-hours structure without more parsing, no historical data. Each missing field is another sub-scraper to build and babysit.

Rule of thumb from our own migrations:

A DIY Maps scraper is the right call below ~500 businesses per month and one target country. Above that, browser time, proxy bills, and selector maintenance quietly overtake the cost of a Google Maps extractor API subscription.

This same trade-off applies anywhere serious anti-bot walls exist. We’ve documented the identical pattern scraping product data behind Amazon’s defenses with our Amazon scraping API , and on search results pages with our Google Search API . Build the toy version yourself, buy the production version.


The API method: the same data in one request

Method 2. Everything the Selenium scraper collected in minutes of browser time, plus the fields it couldn’t reach, from one HTTP request. It comes back in seconds.

We built the FlyByAPIs Google Maps extractor because we got tired of maintaining exactly the script you just read. Grab a free key from the Google Maps API on RapidAPI (100 requests/month, no credit card), and you’re two minutes from structured data.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import os
import requests

API_HOST = "google-maps-extractor2.p.rapidapi.com"
HEADERS = {
    "X-RapidAPI-Key": os.environ["RAPIDAPI_KEY"],
    "X-RapidAPI-Host": API_HOST,
}

response = requests.get(
    f"https://{API_HOST}/locate_and_search",
    headers=HEADERS,
    params={
        "query": "coffee shops in Austin",
        "country": "us",
        "language": "en",
        "limit": 200,      # up to 200 businesses per request
        "offset": 0,       # pagination: 200, 400, 600...
    },
    timeout=30,
)
response.raise_for_status()
businesses = response.json()["data"]

for biz in businesses[:3]:
    print(biz["name"], "|", biz["rating"], f"({biz['reviews_count']} reviews)", "|", biz["full_phone"])

That’s the entire scraper. No ChromeDriver, no scrolling loop, no consent wall, no selectors to maintain. Each business in data comes back as rich JSON:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  "google_id": "0x8644b5b80a0ee693:0xe15d438a8f4ed8a",
  "name": "Figure 8 Coffee Purveyors",
  "full_address": "1111 Chicon St, Austin, TX 78702",
  "detailed_address": { "street": "1111 Chicon St", "city": "Austin", "state": "Texas", "zip_code": "78702", "country": "US" },
  "full_phone": "+1 512-693-7241",
  "website_url": "https://figure8coffeepurveyors.com",
  "rating": 4.6,
  "reviews_count": 1204,
  "main_category": "Coffee shop",
  "latitude": 30.266914,
  "longitude": -97.719947
}

Notice reviews_count is just there, the field our Selenium scraper couldn’t reach at all. Compare the pagination story too: instead of scrolling and praying, you page deterministically with offset until the API stops returning new rows. Our CSV export tutorial builds a complete paginated collector in about 60 lines if that’s your end goal.

Going deeper: details and reviews

Two more endpoints cover what would have been entire Selenium subprojects. Pass any google_id from the search results:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Full business profile: hours, about, attributes, photos count...
details = requests.get(
    f"https://{API_HOST}/business_details",
    headers=HEADERS,
    params={"business_id": "0x8644b5b80a0ee693:0xe15d438a8f4ed8a"},
    timeout=30,
).json()["data"]

# Paginated review text, sortable by most recent or rating
reviews = requests.get(
    f"https://{API_HOST}/business_reviews",
    headers=HEADERS,
    params={"business_id": "0x8644b5b80a0ee693:0xe15d438a8f4ed8a",
            "limit": 20, "sort_by": "mostRecent"},
    timeout=30,
).json()
print(reviews["next_token"])  # pass as next_page_token for page 2

If you’re doing lead generation across borders, review text arrives in whatever language customers wrote it; our translation API normalizes it in the same pipeline. And teams enriching those leads further usually join in firmographics from our Crunchbase scraper API or hiring signals from the Jobs Search API .

Try the Google Maps API free on RapidAPI →

100 requests/month free · No credit card required


Selenium vs API vs official Places API: the honest comparison

Three ways to get this data, three very different bills. The official Google Places API deserves a mention because everyone asks: it’s legitimate and reliable, but Text Search caps out at 60 results per query and pricing runs about $32 per 1,000 requests before per-field detail charges.

FactorSelenium DIYFlyByAPIs extractorOfficial Places API
Results per queryUntil end of list (slow)200/request + offset paging60 max
Time for 1,000 businessesHours: ~10 searches, each scrolled + detail visits5 requests, seconds~17 sub-area queries + grid logic
Reviews + review textSeparate scraper to buildIncluded endpoint5 most recent only
MaintenanceYours, foreverProvider's problemNone
Cost at 10K businesses/moProxies $50+/mo + your hours~50 requests, entry plan~$32/1K searches + per-field detail charges
Blocking riskHigh at scaleHandled server-sideNone

Do you see the pattern? The official API is safe but capped at 60 results, which disqualifies it for any serious lead list, and Selenium is free until your time and proxies aren’t. The extractor route via a Google Maps data API built for scale is what’s left when you need volume, completeness, and your weekends.


Which method should you pick?

Pick Selenium

One-off lists under ~500 businesses, learning projects, or when the budget is exactly zero. You now have a working scraper for it.

Pick the API

Recurring pipelines, review data, multi-city lead generation, or anything a client is waiting on. The Google Maps business data API returns 200 results per request, reviews included, zero maintenance.

Pick the official Places API

You need Google's blessing for a consumer-facing product and 60 results per query is enough. Budget for per-field pricing.

Combine both

Prototype selectors and logic with Selenium locally, run production volume through the extractor. Same schema thinking, different engines.


Wrapping up

We started with a promise: working code, verified this month. The Selenium scraper pulled 42 real businesses with ratings, addresses, phones, and websites, and every one of its limitations is now documented instead of discovered at 2 AM. The API method got the same data, plus review counts and text, in one request.

All of it, both scrapers plus a requirements file, is ready to clone:

1
2
git clone https://github.com/flybyapis/blog-web-scraping-code.git
cd blog-web-scraping-code/scrape-google-maps-python

Start with the free tier, point the Google Maps data extraction API at your city, and see what 200 structured results per request feels like after babysitting a browser.

Get your free API key →

100 requests/month free · No credit card required

P.S. If your next stop after collecting businesses is ranking data, the same “build vs buy” logic applies to search results; that story is in our SERP API comparison .

Oriol.

FAQ

Frequently Asked Questions

Q Is it legal to scrape data from Google Maps?

Extracting public business listings (names, addresses, phones, ratings) is widely practiced, and courts have found that scraping public data doesn't violate the CFAA (hiQ v. LinkedIn), though hiQ still lost on breach-of-terms grounds. Google's Terms of Service restrict automated access to its properties, so a browser bot technically violates them. Using a third-party provider like FlyByAPIs shifts the collection to their infrastructure, and you should always respect privacy laws like GDPR when contacting the businesses you collect.

Q Why not just use the official Google Places API?

Two reasons: result caps and price. Text Search returns a maximum of 60 results per query no matter how many businesses exist, and pricing runs around $32 per 1,000 requests with extra per-field charges for details. A dedicated extractor returns up to 200 results per request with every field included, at a fraction of that cost.

Q Does Google Maps block Selenium scrapers?

Yes, eventually. Google fingerprints browser automation and rate-limits aggressive IPs, so a scraper that works today can start hitting CAPTCHAs once you scale past a few hundred page loads per day. Slower delays, rotating proxies, and up-to-date user agents delay the ban, but they don't remove the maintenance burden.

Q How do I scrape more than 20 results from a Google Maps search?

In a browser you have to scroll the results panel repeatedly, because Google Maps loads listings with infinite scroll instead of pages. The Selenium script in this tutorial automates that with execute_script until the end-of-list marker appears. With the FlyByAPIs endpoint it's simpler: pass limit=200 and page with the offset parameter.

Q Can I get business reviews from Google Maps with Python?

Card-level scraping won't give you review text, and even review counts are missing from the list view. The practical route is the business_reviews endpoint, which returns full review text, ratings, and timestamps as JSON, paginated with a next_page_token and sortable by most recent or rating.

Q Can I use BeautifulSoup instead of Selenium for Google Maps?

Not on its own. Google Maps renders everything with JavaScript, so a plain requests call returns a nearly empty HTML shell with no business data in it. You need a real browser (Selenium or Playwright) to execute the JavaScript, or an API that returns the finished JSON directly.

Q How much does it cost to scrape Google Maps at scale?

DIY looks free but isn't: at roughly 6 seconds per detail page, 10,000 businesses is 16+ hours of browser compute, plus proxies at $50 to several hundred dollars per month once Google starts blocking your IP. For comparison, 10,000 businesses through the FlyByAPIs extractor is about 50 requests with results delivered in seconds.
Share this article
Oriol Marti
Oriol Marti
Founder & CEO

Computer engineer and entrepreneur based in Andorra. Founder and CEO of FlyByAPIs, building reliable web data APIs for developers worldwide.

Free tier available

Ready to stop maintaining scrapers?

Production-ready APIs for web data extraction. Whatever you're building, up and running in minutes.

Start for free on RapidAPI