HasData
Back to all posts

How to Scrape Google Maps Data Using Python

Valentina Skakun
Valentina Skakun
Last update: 4 Aug 2025

Scraping data from Google Maps is challenging because it involves dynamic content loading through JavaScript, complex and frequently changing DOM structures, anti-bot protection mechanisms like rate limiting and fingerprinting, and token-based requests that are hard to replicate.

Standard tools like the Python requests library or simple scraping libraries often fail, returning incomplete data or triggering rate limits.

This article will explore a reliable Python-based approach to extracting structured data from Google Maps, including places, ratings, and contact details.

Building a Google Maps Scraper with Python

This section explains how to build a Google Maps scraper using Python and browser automation tools. We’ll walk you through the process of loading map results, interacting with dynamic content, and extracting key details, such as name, rating, number of reviews, and detail page URL.

This method requires more initial effort, such as setting up a custom scraper using tools like Selenium, handling dynamic content, and managing anti-bot protection, compared to using ready-made solutions like the Google Maps API or precompiled datasets. 

However, it offers full flexibility: you can extract exactly the data you need and set the scraper’s behavior to your specific goals. Unlike third-party scraping services, which typically provide only limited predefined fields, a custom solution gives you complete control over the extraction process.

Code Overview

If you’re only looking for a ready-made script and don’t need the technical breakdown, here it is:

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


query = "restaurants in New York"
max_scrolls = 10
scroll_pause = 2


options = Options()
driver = webdriver.Chrome(options=options)


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)


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)


feed_container = driver.find_element(By.CSS_SELECTOR, 'div.m6QErb.DxyBCb.kA9KIf.dS8AEf.XiKgde.ecceSd[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 ""
    print(name)


    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
    })


df = pd.DataFrame(data)
df.to_csv("maps_data.csv", index=False)
print(f"Saved {len(df)} records to maps_data.csv")


driver.quit()

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

Tools and Setup

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

pip install selenium pandas

We’ll also use standard Python modules like requests, re, time, and json, which don’t require separate installation.

Now, let’s import all the necessary modules for 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

If you’re using an older version of Selenium, you may also need to install webdriver-manager:

pip install webdriver-manager

This is not necessary in newer Selenium versions.

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:

For more detailed information (like phone numbers or working hours), open each location or follow its dedicated details page. For now, we’ll focus on extracting core data from the main search list.

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.

We’ve written separate tutorials on how to work with CSS selectors and XPath, so here, we’ll only provide a table with the ready-to-use ones for this project:

FieldDescriptionCSS SelectorXPath
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”)]
ReviewsNumber of user reviews.UY7F9.//span[contains(@class, “UY7F9”)]
CategoryType of place (e.g., Restaurant, Coffee shop)div.W4Efsd > span:first-child.//div[contains(@class, “W4Efsd”)]/span[1]
ServicesAvailable services (e.g., Dine-in, Takeout)div.ah5Ghc > span.//div[contains(@class, “ah5Ghc”)]/span
ImageImage preview of the placeimg[src*=“googleusercontent”].//img[contains(@src, “googleusercontent”)]
Detail URLLink to detailed view of the placea.hfpxzc.//a[contains(@class, “hfpxzc”)]
Feed ContainerContainer holding the list of result cardsdiv.m6QErb.DxyBCb.kA9KIf.dS8AEf.XiKgde.ecceSd[role=“feed”]//div[@role=“feed” and contains(@class, “m6QErb”) and contains(@class, “XiKgde”)]
ScrollableScrollable div that loads more resultsdiv[role=“main”] div.m6QErb[tabindex=“-1”]//div[@role=“main”]//div[@tabindex=“-1” and contains(@class, “m6QErb”)]
CardSingle business listing carddiv.Nv2PK.THOPZb.CpccDe.//div[contains(@class, “Nv2PK”) and contains(@class, “CpccDe”)]
Search InputInput field for search queries#searchboxinput//*[@id=“searchboxinput”]

Google generates dynamic and often cryptic class names that can change even with minor interface updates. Relying on these class names for element selection is not a sustainable strategy, especially for tasks that require regular scraping, like daily or weekly data collection, where constant manual updates would make the process inefficient.

Instead, it’s better to anchor your scraper to more stable parts of the page structure, such as element hierarchy, attributes, or text content when possible. Alternatively, consider using the Google Maps API, which 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. As a result, it tends to be more reliable over time and scales more efficiently for high-volume or recurring data collection tasks.

Data Extraction

To begin, we need to find locations based on a keyword. There are two main approaches:

  1. Generate a search URL programmatically
  2. Use a headless browser to type the keyword into the search field

In this example, we’ll go with the second option, simulating user input in the browser.

Set the target keyword, load the Google Maps homepage, type the query, press Enter, and wait for the results to load:

query = "restaurants in New York"


options = Options()
driver = webdriver.Chrome(options=options)


driver.get("https://www.google.com/maps")


search.send_keys(query)
search.send_keys(Keys.ENTER)
time.sleep(5)

Once the search results appear, locate the container that holds the list of places:

feed_container = driver.find_element(By.CSS_SELECTOR, 'div.m6QErb.DxyBCb.kA9KIf.dS8AEf.XiKgde.ecceSd[role="feed"]')
cards = feed_container.find_elements(By.CSS_SELECTOR, "div.Nv2PK.THOPZb.CpccDe")
data = []

Then, loop through each item and extract the data using the CSS selectors or XPath patterns we identified earlier:

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


    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 ""

Store the results in a structured variable for further use or export:

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

Finally, don’t forget to close the browser session:

driver.quit()

By default, Google Maps only loads a few visible results. To access more, you’ll need to implement infinite scrolling, as described below.

Infinite Scrolling Implementation

Infinite scrolling in Selenium is straightforward. We covered this in detail in a separate article, but here’s the general logic:

  1. Identify the scrollable container
  2. Scroll to the bottom of the element
  3. Wait a few seconds
  4. Repeat until no new results are loaded, or a stopping condition is met

Let’s implement that step by step. Locate the scroll container and assign it to a variable:

scrollable = driver.find_element(By.CSS_SELECTOR, 'div[role="feed"]')

Define the number of scroll attempts and the pause duration between them:

max_scrolls = 10
scroll_pause = 2

Execute the scroll loop:

for _ in range(max_scrolls):
    driver.execute_script('arguments[0].scrollTop = arguments[0].scrollHeight', scrollable)
    time.sleep(scroll_pause)

Alternatively, you can monitor the number of loaded elements and stop scrolling when the count stops increasing. This is often more efficient.

Store results in CSV/JSON format

Since the data is already structured, it’s easy to export it to a CSV file using pandas:

df = pd.DataFrame(data)
df.to_csv("maps_data.csv", index=False)

Or save it as a JSON file:

df = pd.DataFrame(data)
df.to_json("maps_data.json", orient="records", indent=2, force_ascii=False)

Pandas also supports exporting to Excel, SQL databases, HTML, and many other formats.

Using API to Access Google Maps Data

If you need a faster and more reliable way to collect data, a scraping API can simplify the process by handling many common scraping challenges for you. These include managing browser behavior, rotating proxies to avoid IP bans, and bypassing anti-bot protections – tasks that are often complex, time-consuming, and require ongoing maintenance. Offloading this overhead to an API leads to more stable, consistent, and low-effort data scraping.

This approach is especially beneficial for large-scale projects, such as scraping thousands of locations daily or running frequent data updates, where manual handling would be impractical. It reduces setup time and minimizes risks from website changes or blocking mechanisms, making it ideal for high-volume or recurring data collection tasks.

HasData API vs. Google Maps Official API

If maintaining complex scrapers or constantly updating HTML selectors isn’t feasible for your workflow, using an API becomes the most efficient alternative. There are two main options:

  1. The official Google Maps API
  2. An alternative solution, like HasData’s Google Maps API

Google’s API uses pay-as-you-go billing. If your usage exceeds the quota, it returns errors, such as OVER_QUERY_LIMIT. You can configure usage limits in the Google Cloud Console → Quotas, and set up budget alerts.

As of now, Google offers $200 in free usage per month, which typically covers:

APIWhat it doesExample usage covered by $200/month
Places APISearch places, get place details~11,000 requests
Geocoding APIConvert address - coordinates~40,000 requests
Maps JavaScript APIDisplay an interactive map on your site~28,000 map loads
Directions APIGet routes between locations~10,000 directions
Autocomplete APILocation name suggestions while typing~11,000 requests

You can calculate your expected costs using Google’s Pricing Calculator. For large-scale data collection, costs escalate quickly. For example:

  • 200,000 detailed place lookups via Google’s official API cost around $850
  • The same volume of requests using HasData’s API costs only $99 (check our pricing page for details)

Code Overview

Here’s a quick example of how to get Google Maps Search results using HasData’s Google Maps API:

import requests
import json
import pandas as pd


api_key = 'YOUR-API-KEY'
query = 'Pizza'


url = f"https://api.hasdata.com/scrape/google-maps/search?q={query}"
headers = {
    'Content-Type': 'application/json',
    'x-api-key': api_key
}


response = requests.get(url, headers=headers)
data = response.json()


with open('output.json', 'w', encoding='utf-8') as f:
    json.dump(data, f, ensure_ascii=False, indent=2)


results = data.get("localResults", [])


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"),
        "latitude": r.get("gpsCoordinates", {}).get("latitude"),
        "longitude": r.get("gpsCoordinates", {}).get("longitude")
    }
    for r in results
]


df = pd.DataFrame(filtered)
df.to_csv('output.csv', index=False)

The API returns clean, ready-to-use JSON with no extra parsing required. 

API Request Setup

To use the script above, you’ll need a HasData API key issued upon free registration. Then simply define your API key and the keyword for the search:

api_key = 'YOUR-API-KEY'
query = 'Pizza'

You can find the complete list of available request parameters in the documentation.

Next, define the endpoint and request headers:

url = f"https://api.hasdata.com/scrape/google-maps/search?q={query}"
headers = {
    'Content-Type': 'application/json',
    'x-api-key': api_key
}

Now, execute the request:

response = requests.get(url, headers=headers)

At this point, you can either print the results, store them in a file, or continue processing them as needed.

Parse and Save JSON response

Since the API response is already in JSON, parsing it is easy:

data = response.json()

To save the 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)

This saves the entire response exactly as returned by the API.

Export Google Maps Data to CSV with Python

To export selected fields instead of saving the raw JSON, start by extracting the required fields:

results = data.get("localResults", [])


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"),
        "latitude": r.get("gpsCoordinates", {}).get("latitude"),
        "longitude": r.get("gpsCoordinates", {}).get("longitude")
    }
    for r in results
]

Then save it to CSV using pandas:

df = pd.DataFrame(filtered)
df.to_csv('output.csv', index=False)

You can easily extend the list of fields to include other values like service_options, working_hours, description, thumbnail, etc.

Alternatively, you can export the entire dataset in a flat structure:

df = pd.DataFrame(results)
df.to_csv('output.csv', index=False)

This approach preserves all the data fields, automatically using JSON keys as column names.

Advanced Techniques for DIY Scraping

If the Google Maps API doesn’t meet your needs and you’re building your scraper in Python, you’ll face a major challenge: anti-bot protection. These systems are specifically designed to detect and block automated access, making scraping at scale difficult, if you don’t apply the right techniques.

In this section, we’ll cover Google Maps’s most common anti-bot mechanisms and show how to work around them. We’ll also provide practical tips for keeping your scraper stable using try…except blocks.

If you’re using a scraping API like HasData’s, you can safely skip this part, as this tool manages anti-bot systems and error handling for you, so you can directly focus on collecting and processing the data. But if you’re taking the DIY route, here’s the breakdown of the most common anti-bot challenges you’re likely to encounter, along with the potential ways of addressing them:

Protection TypeDescriptionBypass Methods
CAPTCHA challengesShows CAPTCHA challenges to verify human activity- Use CAPTCHA-solving services
Dynamic content loading (JS)Loads page content asynchronously via JavaScript- Use browser automation (Selenium, Playwright, Puppeteer) 
IP blocking/ rate limitingBlocks requests after detecting high volume or suspicious IP behavior- Use rotating proxies (residential or datacenter) 
User-Agent detectionBlocks access based on missing or spoofed browser headers- Set realistic headers (User-Agent, Referer, etc.) 
Cookie/ Session trackingTracks session behavior to identify automation tools- Maintain and reuse session cookies 
TLS/ HTTP fingerprintingDetects non-browser clients via TLS or HTTP behavior- Use stealth plugins (e.g. Playwright Stealth, Selenium Base)

Handling anti-bot protection is just one part of building a functional scraper. Even after your scraper gets past these barriers, it still needs to run reliably, especially during long or repeated sessions.

That’s why error handling is just as important as bypassing detection. If your script is not properly managed, network failures, timeouts, or unexpected changes in the page structure can all cause it to crash.

To prevent this, we recommend you to wrap potentially unstable parts of the code in try...except blocks. This approach lets your scraper recover gracefully when something goes wrong, instead of stopping completely.

For example, to avoid script termination when a request to the API fails, you can place that section inside an error-handling block:

try:
    response = requests.get(url, headers=headers)
    response.raise_for_status()  # raises an exception for 4xx/5xx responses
    data = response.json()
    # further response processing goes here


except requests.exceptions.RequestException as e:
    print(f"Request failed: {e}")
    data = None

This way, the script will continue running, skip saving invalid data, and log the error for further review. This method significantly improves the scraper’s stability and resilience.

You can find more information on error types and handling strategies in our related article

Conclusion

Whether you’re building your scraper using Python from scratch or using a ready-made API, extracting data from Google Maps requires careful handling of dynamic content and protection mechanisms.

  • A self-made Python scraper gives you full control and flexibility: you can customize what data to collect and how. However, it often takes more time to develop, requires constant updates, and can be harder to scale for large or frequent workloads.
  • Scraping APIs, on the other hand, simplify the entire process by managing browser automation, proxy rotation, and blocking mechanisms for you. They offer higher reliability out of the box, especially for production use or high-volume data collection.

In short, choose Python when a lot of customization or control is required or your need to solve a very specific problem, such as extracting non-standard data fields, navigating through nested UI elements, or automating complex interactions that an API doesn’t support. If you need scale, speed, and reliability, though, you’ll be better off with an API.

Valentina Skakun
Valentina Skakun
I'm a technical writer who believes that data parsing can help in getting and analyzing data. I'll tell about what parsing is and how to use it.
Articles

Might Be Interesting