How to Scrape Google Maps Data Using Python

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:
Field | Description | CSS Selector | XPath |
---|---|---|---|
Name | Name of the place | .qBF1Pd | .//div[contains(@class, “qBF1Pd”)] |
Rating | Star rating (e.g., 4.7 stars) | span[aria-label*=“stars”] | .//span[contains(@aria-label, “stars”)] |
Reviews | Number of user reviews | .UY7F9 | .//span[contains(@class, “UY7F9”)] |
Category | Type of place (e.g., Restaurant, Coffee shop) | div.W4Efsd > span:first-child | .//div[contains(@class, “W4Efsd”)]/span[1] |
Services | Available services (e.g., Dine-in, Takeout) | div.ah5Ghc > span | .//div[contains(@class, “ah5Ghc”)]/span |
Image | Image preview of the place | img[src*=“googleusercontent”] | .//img[contains(@src, “googleusercontent”)] |
Detail URL | Link to detailed view of the place | a.hfpxzc | .//a[contains(@class, “hfpxzc”)] |
Feed Container | Container holding the list of result cards | div.m6QErb.DxyBCb.kA9KIf.dS8AEf.XiKgde.ecceSd[role=“feed”] | //div[@role=“feed” and contains(@class, “m6QErb”) and contains(@class, “XiKgde”)] |
Scrollable | Scrollable div that loads more results | div[role=“main”] div.m6QErb[tabindex=“-1”] | //div[@role=“main”]//div[@tabindex=“-1” and contains(@class, “m6QErb”)] |
Card | Single business listing card | div.Nv2PK.THOPZb.CpccDe | .//div[contains(@class, “Nv2PK”) and contains(@class, “CpccDe”)] |
Search Input | Input 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:
- Generate a search URL programmatically
- 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:
- Identify the scrollable container
- Scroll to the bottom of the element
- Wait a few seconds
- 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:
- The official Google Maps API
- 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:
API | What it does | Example usage covered by $200/month |
---|---|---|
Places API | Search places, get place details | ~11,000 requests |
Geocoding API | Convert address - coordinates | ~40,000 requests |
Maps JavaScript API | Display an interactive map on your site | ~28,000 map loads |
Directions API | Get routes between locations | ~10,000 directions |
Autocomplete API | Location 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 Type | Description | Bypass Methods |
---|---|---|
CAPTCHA challenges | Shows 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 limiting | Blocks requests after detecting high volume or suspicious IP behavior | - Use rotating proxies (residential or datacenter) |
User-Agent detection | Blocks access based on missing or spoofed browser headers | - Set realistic headers (User-Agent, Referer, etc.) |
Cookie/ Session tracking | Tracks session behavior to identify automation tools | - Maintain and reuse session cookies |
TLS/ HTTP fingerprinting | Detects 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.
