HasData
Back to all posts

How to Scrape Google Maps Data Using Python

Google Maps guards its results with dynamic JavaScript loading, frequent DOM changes, and anti-bot protections that cover rate limits, fingerprinting and token-based requests.

Common tools like the Python Requests library or simple scraping libraries usually don’t cut it. They either miss data or quickly hit rate limits.

The scraper below handles those obstacles and pulls structured data out of the results feed, places, ratings and contact details, and the last section covers when an API is the saner route.

Building a Google Maps Scraper with Python

The scraper below drives a real Chrome, scrolls the results feed, and reads each card. The full code comes first, then the walkthrough.

Code Overview

Since Google frequently changes its class names and HTML structure, double-check the selectors and update them as needed before running the script.

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.chrome.options import Options
import time
import pandas as pd
import re
import json

def init_driver():
    options = Options()
    driver = webdriver.Chrome(options=options)
    return driver

def search_query(driver, query: str):
    driver.get("https://www.google.com/maps")
    time.sleep(5)
    search = driver.find_element(By.ID, "searchboxinput")
    search.send_keys(query)
    search.send_keys(Keys.ENTER)
    time.sleep(5)

def scroll_results(driver, max_scrolls: int = 10, scroll_pause: int = 2):
    scrollable = driver.find_element(By.CSS_SELECTOR, 'div[role="feed"]')
    for _ in range(max_scrolls):
        driver.execute_script('arguments[0].scrollTop = arguments[0].scrollHeight', scrollable)
        time.sleep(scroll_pause)

def parse_cards(driver):
    feed_container = driver.find_element(By.CSS_SELECTOR, 'div[role="feed"]')
    cards = feed_container.find_elements(By.CSS_SELECTOR, "div.Nv2PK.THOPZb.CpccDe")

    data = []
    for card in cards:
        name_el = card.find_elements(By.CLASS_NAME, "qBF1Pd")
        name = name_el[0].text if name_el else ""

        rating_el = card.find_elements(By.XPATH, './/span[contains(@aria-label, "stars")]')
        rating = ""
        if rating_el:
            match = re.search(r"([\d.]+)", rating_el[0].get_attribute("aria-label"))
            rating = match.group(1) if match else ""

        reviews_el = card.find_elements(By.CLASS_NAME, "UY7F9")
        reviews = ""
        if reviews_el:
            match = re.search(r"([\d,]+)", reviews_el[0].text)
            reviews = match.group(1).replace(",", "") if match else ""

        category_el = card.find_elements(By.XPATH, './/div[contains(@class, "W4Efsd")]/span[1]')
        category = category_el[0].text if category_el else ""

        services_el = card.find_elements(By.XPATH, './/div[contains(@class, "ah5Ghc")]/span')
        services = ", ".join([s.text for s in services_el]) if services_el else ""

        image_el = card.find_elements(By.XPATH, './/img[contains(@src, "googleusercontent")]')
        image_url = image_el[0].get_attribute("src") if image_el else ""

        link_el = card.find_elements(By.CSS_SELECTOR, 'a.hfpxzc')
        detail_url = link_el[0].get_attribute("href") if link_el else ""

        data.append({
            "Name": name,
            "Rating": rating,
            "Reviews": reviews,
            "Category": category,
            "Services": services,
            "Image": image_url,
            "Detail URL": detail_url
        })
    return data

def save_data(data, csv_filename="maps_data.csv", json_filename="maps_data.json"):
    df = pd.DataFrame(data)
    df.to_csv(csv_filename, index=False)
    print(f"Saved {len(df)} records to {csv_filename}")

    with open(json_filename, "w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=False, indent=4)
    print(f"Saved {len(data)} records to {json_filename}")

def main():
    query = "restaurants in New York"
    max_scrolls = 10
    scroll_pause = 2

    driver = init_driver()
    try:
        search_query(driver, query)
        scroll_results(driver, max_scrolls, scroll_pause)
        data = parse_cards(driver)
        save_data(data)
    finally:
        driver.quit()

if __name__ == "__main__":
    main()

The rest of this section unpacks that script piece by piece.

Tools and Setup

We recommend starting with our Python scraping introduction guide, if you’re new to web scraping. Otherwise, begin by installing the libraries:

pip install selenium pandas

Import required modules and libraries:

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.chrome.options import Options
import time
import pandas as pd
import re
import json

That’s the whole dependency list, Selenium for the browser and pandas for the export.

Page Structure Analysis

The easiest way to collect business data from Google Maps is to scrape the search results page, which already contains names, ratings, categories, and addresses:

Google Maps results for a coffee search, with the scrollable results feed on the left and the map on the right

Open DevTools (press F12 or right-click and Inspect), and find the relevant CSS selectors or XPath expressions for the data you want to extract.

Google often uses dynamic class names that change even after small interface updates. Where an ARIA attribute exists, prefer it. The results list is always div[role="feed"] while its six generated classes rotate. Both forms match the same element today, and only the role-based one survives the next class rotation.

There are tutorials on how to work with CSS selectors and XPath, so here, we’ll share a table with the ready-to-use selectors for this project.

FieldDescriptionCSSXPath
NameName of the place.qBF1Pd.//div[contains(@class, ‘qBF1Pd’)]
RatingStar rating (e.g., 4.7 stars)span[aria-label*=“stars”].//span[contains(@aria-label, ‘stars’)]
ReviewsReview count, when the card has one.UY7F9.//span[contains(@class, ‘UY7F9’)]
CategoryType of place (e.g., Restaurant,div.W4Efsd > span:first-child.//div[contains(@class, ‘W4Efsd’)]/span[1]
ImageImage preview of the placeimg[src*=“googleusercontent”].//img[contains(@src, ‘googleusercontent’)]
Feed ContainerContainer holding the list of resultdiv[role=“feed”]//div[@role=“feed”]
ScrollableScrollable div that loads more resultsdiv[role=“main”]//div[@role=“main”]//div[@tabindex=“-1”]
CardSingle business listing carddiv.Nv2PK.THOPZb.CpccDe.//div[contains(@class, ‘Nv2PK’)]
Search InputInput field for search queries#searchboxinput//*[@id=“searchboxinput”]

HasData’s Google Maps API provides structured access to map data through a consistent interface. This approach is generally easier to use and maintain, because it handles dynamic content loading, anti-bot protections, and data formatting for you.

Data Extraction

Start Chrome with Selenium and get the browser ready for scraping:

def init_driver():
    # Initialize Selenium Chrome driver with options.
    options = Options()
    driver = webdriver.Chrome(options=options)
    return driver

Open Google Maps, type your search term, and run the search.

def search_query(driver, query: str):
    # Open Google Maps and search for a query.
    driver.get("https://www.google.com/maps")
    time.sleep(5)
    search = driver.find_element(By.ID, "searchboxinput")
    search.send_keys(query)
    search.send_keys(Keys.ENTER)
    time.sleep(5)

Go through each result card and extract the name, rating, reviews, category, services, image, and link.

Google renders several feed layouts, and two of those fields appear in only some of them. The review count is one, so UY7F9 matches on one result set and returns nothing on the next. The services line behaves the same way and is absent more often than not. Every read below goes through find_elements rather than find_element for that reason, which turns an absent field into an empty string instead of an exception, and it also means an empty Reviews column is the page’s doing rather than a broken selector.

def parse_cards(driver):
    # Extract data from result cards
    feed_container = driver.find_element(By.CSS_SELECTOR, 'div[role="feed"]')
    cards = feed_container.find_elements(By.CSS_SELECTOR, "div.Nv2PK.THOPZb.CpccDe")

    data = []
    for card in cards:
        #  Name of the place
        name_el = card.find_elements(By.CLASS_NAME, "qBF1Pd")
        name = name_el[0].text if name_el else ""

        #  Rating (from aria-label, e.g. "4.5 stars")
        rating_el = card.find_elements(By.XPATH, './/span[contains(@aria-label, "stars")]')
        rating = ""
        if rating_el:
            match = re.search(r"([\d.]+)", rating_el[0].get_attribute("aria-label"))
            rating = match.group(1) if match else ""

        #  Number of reviews (e.g. "1,234 reviews")
        reviews_el = card.find_elements(By.CLASS_NAME, "UY7F9")
        reviews = ""
        if reviews_el:
            match = re.search(r"([\d,]+)", reviews_el[0].text)
            reviews = match.group(1).replace(",", "") if match else ""

        #  Category (e.g. "Italian restaurant")
        category_el = card.find_elements(By.XPATH, './/div[contains(@class, "W4Efsd")]/span[1]')
        category = category_el[0].text if category_el else ""

        #  Services (e.g. "Dine-in, Takeout, Delivery")
        services_el = card.find_elements(By.XPATH, './/div[contains(@class, "ah5Ghc")]/span')
        services = ", ".join([s.text for s in services_el]) if services_el else ""

        #  Image (URL of the thumbnail from Google Maps)
        image_el = card.find_elements(By.XPATH, './/img[contains(@src, "googleusercontent")]')
        image_url = image_el[0].get_attribute("src") if image_el else ""

        #  Detail page link
        link_el = card.find_elements(By.CSS_SELECTOR, 'a.hfpxzc')
        detail_url = link_el[0].get_attribute("href") if link_el else ""

        # Collect all fields into one record
        data.append({
            "Name": name,
            "Rating": rating,
            "Reviews": reviews,
            "Category": category,
            "Services": services,
            "Image": image_url,
            "Detail URL": detail_url
        })
    return data

Every lookup uses find_elements, so a card missing a field contributes an empty string instead of an exception.

Infinite Scrolling Implementation

We covered infinite scrolling in detail in another article, but here’s the basic idea:

  1. Identify the scrollable container.
  2. Scroll to the bottom of that element.
  3. Wait for a few seconds.
  4. Repeat until no new results appear or you hit a stopping point.

Scroll through the results to load more places:

def scroll_results(driver, max_scrolls: int = 10, scroll_pause: int = 2):
    # Scroll the results feed to load more places.
    scrollable = driver.find_element(By.CSS_SELECTOR, 'div[role="feed"]')
    for _ in range(max_scrolls):
        driver.execute_script('arguments[0].scrollTop = arguments[0].scrollHeight', scrollable)
        time.sleep(scroll_pause)

Each scroll of the feed loads another batch of places, and max_scrolls caps how deep the run goes.

Store results in CSV/JSON format

Save your scraped data to a CSV or JSON file and see how many records were saved:

def save_data(data, csv_filename="maps_data.csv", json_filename="maps_data.json"):
    # Save to CSV
    df = pd.DataFrame(data)
    df.to_csv(csv_filename, index=False)
    print(f"Saved {len(df)} records to {csv_filename}")

    # Save to JSON
    with open(json_filename, "w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=False, indent=4)
    print(f"Saved {len(data)} records to {json_filename}")

CSV suits spreadsheets and JSON keeps the nesting, so saving both costs nothing extra.

The Same Scraper in Playwright

Playwright drives the same flow as an alternative to Selenium, with auto-waiting locators. This condensed version returns the result cards with names and ratings, and it matches each card on div.Nv2PK alone, since the two extra classes in the table rotate with interface updates:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    page = browser.new_page(locale="en-US")
    page.goto("https://www.google.com/maps/search/restaurants+in+New+York?hl=en")
    page.wait_for_selector('div[role="feed"]')

    for card in page.locator('div[role="feed"] div.Nv2PK').all():
        name = card.locator(".qBF1Pd").inner_text()
        stars = card.locator('span[aria-label*="stars"]').first.get_attribute("aria-label")
        print(name, "|", stars)

    browser.close()

Navigating straight to /maps/search/{query} skips the search box, which renders unreliably in headless sessions. Everything else, the scrolling, the card parsing, and the saving, translates line for line, and our Playwright scraping guide covers the library itself.

Using API to Access Google Maps Data

HasData’s Google Maps Scraping API covers the same ground without a browser in your code. It renders the page, routes the request through its proxy pool, and returns the places as JSON.

The search endpoint below bills 5 credits per request, and one request returns a page of places rather than a single one, so 200,000 searches fit into the Basic plan’s 1,000,000 monthly credits at $119. A single-place lookup through the place endpoint costs the same 5 credits. Current numbers are on the pricing page.

The following script sends a request to HasData’s API using your API key and a search query. It reads the JSON response and saves the important information to CSV and JSON files.

import requests
import json
import pandas as pd

# To get an API key, sign up at https://app.hasdata.com/sign-up
api_key = 'YOUR-API-KEY'

# What we want to search for in Google Maps
query = 'Pizza'

# Documentation with all parameters: https://docs.hasdata.com/apis/google-maps/search
url = f"https://api.hasdata.com/scrape/google-maps/search?q={query}"

#  Headers for the API request
headers = {
    'Content-Type': 'application/json',
    'x-api-key': api_key
}

#  Send GET request to HasData API
response = requests.get(url, headers=headers)

#  Parse JSON response
data = response.json()

#  Save full response to a JSON file
with open('output.json', 'w', encoding='utf-8') as f:
    json.dump(data, f, ensure_ascii=False, indent=2)

#  Extract the main results list from the response
results = data.get("localResults", [])

#  Filter and normalize results
filtered = [
    {
        "title": r.get("title"),
        "address": r.get("address"),
        "phone": r.get("phone"),
        "website": r.get("website"),
        "rating": r.get("rating"),
        "reviews": r.get("reviews"),
        "type": r.get("type"),
        "price": r.get("price"),
        # Some fields may be nested, e.g., GPS coordinates
        "latitude": r.get("gpsCoordinates", {}).get("latitude"),
        "longitude": r.get("gpsCoordinates", {}).get("longitude")
    }
    for r in results
]

#  Convert to DataFrame for easy analysis
df = pd.DataFrame(filtered)

#  Save to CSV
df.to_csv('output.csv', index=False)

One request, one flat table, and no browser in the loop.

Conclusion

Scraping data from Google Maps can be difficult because of dynamic content and anti-scraping measures.

If you build your own Python scraper, you get full control over what and how to collect. But it takes more time, needs frequent updates, and can be hard to scale for higher data volumes.

Using a scraping API makes things easier. It handles browser automation, proxy rotation, and blocks for you. APIs are more reliable, especially for large-scale or regular data collection.

Valentina Skakun
Valentina Skakun
Valentina is a software engineer who builds data extraction tools before writing about them. With a strong background in Python, she also leverages her experience in JavaScript, PHP, R, and Ruby to reverse-engineer complex web architectures.If data renders in a browser, she will find a way to script its extraction.
Articles

Might Be Interesting