Search Engine Result Page (SERP) Scraper APIs focus on web page rankings and keyword-specific information from across the internet, while Google Maps scrapers specialize in extracting business and location-based data directly from Google Maps.
In this article, we’ll explore the top Google Maps scraper tools and provide a step-by-step Python tutorial showing how to build your own Google Maps scraper.
How to scrape Google Maps with Python
With this script, we can extract business names, addresses, phone numbers, ratings, review counts, opening hours, websites, and customer reviews directly from Google Maps search results.
Complete code structure
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
import time
import json
import re
class GoogleMapsScraper:
def __init__(self):
pass
Our scraper is built around 7 main functions. Each function handles a specific part of the web scraping process:
- create_driver(): Sets up the Chrome browser
- search_places(): Main function that coordinates everything
- extract_businesses(): Finds businesses and manages scrolling
- extract_name_from_url(): Gets business names from URLs
- scroll_results(): Scrolls down to load more results
- get_business_info(): Extracts detailed business information
- save_results(): Saves the collected data into a JSON file
Step 1: Set up your browser environment
To interact with Google Maps, we first need to launch a Chrome browser session with Selenium. This is handled by the create_driver() function.
def create_driver(self):
from selenium.webdriver.chrome.options import Options
import tempfile
import os
options = Options()
temp_dir = tempfile.mkdtemp()
profile_dir = os.path.join(temp_dir, "chrome_profile")
options.add_argument(f"--user-data-dir={profile_dir}")
options.add_argument("--no-first-run")
options.add_argument("--no-default-browser-check")
options.add_argument("--disable-default-apps")
driver = webdriver.Chrome(options=options)
return driver
Why we use temporary profiles
When Chrome detects automation, Windows Defender may show warnings about resetting browser settings. By creating a temporary profile, we avoid interfering with your main Chrome installation. The temporary directory is automatically cleaned up when the program ends.
Why we use minimal configuration
We tested many advanced “anti-detection” methods, but most either break easily or cause extra problems. This simple approach, using a temporary profile with minimal flags, works reliably for our purposes.
Step 2: Search businesses on Google Maps with Selenium
The search_places() function serves as the entry point for our scraper. It opens Google Maps with a specific search query and location, waits for the page to load, and then triggers the business extraction process.
def search_places(self, query, location="Berlin", max_results=20):
driver = self.create_driver()
try:
url = f"https://www.google.com/maps/search/{query.replace(' ', '+')}+{location.replace(' ', '+')}"
driver.get(url)
time.sleep(15) # Page loading + cookie popup
businesses = self.extract_businesses(driver, max_results)
return businesses
except Exception as e:
print(f"Error: {e}")
return []
finally:
driver.quit()
Key details
- URL construction: Instead of manually navigating Google Maps, we build the URL directly. Spaces are replaced with + for proper URL encoding.
- 15-second wait: This delay ensures the page fully loads and gives you time to dismiss any cookie pop-ups.
- Error handling: The try-except-finally block guarantees the browser closes even if an error occurs.
Customizable parameters
- query → The search term (e.g., “coffee shop”, “restaurant”, “hotel”)
- location → The city or area to search (e.g., “Berlin”, “Munich”, “Hamburg”)
- max_results → The number of businesses to scrape (default = 20; you can increase to 50, 100, etc.)
Step 3: Extract business listings from Google Maps search results
The extract_businesses() function is responsible for collecting business links and details from Google Maps search results. It scrolls through the page, extracts businesses, avoids duplicates, and stops once the desired number of results is reached.
def extract_businesses(self, driver, max_results):
businesses = []
scroll_count = 0
no_change_count = 0
last_count = 0
while len(businesses) < max_results and scroll_count < 8:
# Find place links
links = driver.find_elements(By.XPATH, "//a[contains(@href, '/place/')]")
for link in links:
if len(businesses) >= max_results:
break
try:
href = link.get_attribute('href')
name = self.extract_name_from_url(href)
if name and href:
if not any(b['name'] == name for b in businesses):
business = {
'name': name,
'link': href,
'rating': 'N/A',
'reviews_count': 'N/A',
'address': 'N/A',
'phone': 'N/A',
'website': 'N/A',
'hours': 'N/A',
'review': 'N/A',
}
# Get detailed info
details = self.get_business_info(driver, link, name)
if details:
business.update(details)
businesses.append(business)
except Exception as e:
continue
# Check for new results
if len(businesses) == last_count:
no_change_count += 1
if no_change_count >= 3:
break
else:
no_change_count = 0
last_count = len(businesses)
if len(businesses) >= max_results:
break
# Scroll
if self.scroll_results(driver):
pass
time.sleep(4)
scroll_count += 1
return businesses
Key details
- Why XPath instead of CSS selectors
Google frequently changes its CSS class names, but the /place/ URL pattern is more stable. Using this XPath helps reliably capture business links. - Business dictionary structure
Each business is stored as a dictionary with default ‘N/A’ values. This ensures a consistent format even if some fields are missing. - Duplicate prevention
The check if not any(b[‘name’] == name for b in businesses) ensures that the same business isn’t added twice. - Scroll limit of 8
The function will scroll up to 8 times to find results. You can increase this limit, but higher numbers result in longer execution times. - New results detection
If no new businesses are found after three consecutive scrolls, the function stops early to avoid infinite loops.
Step 4: Extract business names from Google Maps URLs
The extract_name_from_url() function extracts business names directly from the Google Maps URL. This method is the most reliable because names are always embedded in the /place/ part of the URL.
def extract_name_from_url(self, url):
"""Extract business name from URL - most reliable method"""
try:
if '/place/' in url:
parts = url.split('/place/')
if len(parts) > 1:
name_part = parts[1].split('/')[0]
import urllib.parse
decoded_name = urllib.parse.unquote(name_part)
clean_name = decoded_name.replace('+', ' ')
return clean_name
return None
except Exception as e:
return None
Why we extract names from URLs
We tested multiple methods for extracting business names:
- Reading text directly from page elements → Often unreliable, sometimes empty.
- Parsing complex DOM structures → Breaks easily due to dynamic loading.
- Extracting from URLs → Works consistently because Google always includes the business name in the /place/ URL.
This is why URL extraction is the most robust approach.
URL decoding process
Google Maps encodes business names in URLs. For example:
- Raw URL: KPM+Caf%C3%A9
- urllib. parse.unquote() converts %C3%A9 → é
- .replace(‘+’, ‘ ‘) converts + → space
- Final result: “KPM Café”
Step 5: Scroll through Google Maps results
The scroll_results() function handles scrolling through the Google Maps results panel. Since not all businesses load at once, scrolling is required to reveal additional results.
def scroll_results(self, driver):
"""Scroll using working selector - .m6QErb"""
try:
elements = driver.find_elements(By.CSS_SELECTOR, '.m6QErb')
for element in elements:
scroll_height = driver.execute_script("return arguments[0].scrollHeight", element)
client_height = driver.execute_script("return arguments[0].clientHeight", element)
current_scroll = driver.execute_script("return arguments[0].scrollTop", element)
if scroll_height > client_height:
driver.execute_script("arguments[0].scrollTop += 300", element)
time.sleep(1)
new_scroll = driver.execute_script("return arguments[0].scrollTop", element)
if new_scroll > current_scroll:
driver.execute_script("arguments[0].scrollTop = arguments[0].scrollHeight", element)
return True
return False
except Exception as e:
return False
Why .m6QErb class?
Through testing, we found that this CSS class reliably represents the scrollable container for Google Maps search results. Other selectors were either missing or didn’t scroll correctly.
Two-step scroll process
- Partial scroll → Moves down by 300 pixels to trigger new results loading.
- Full scroll → Moves to the very bottom to reveal all available results.
- Validation → Confirms that the scroll position actually changed. If it didn’t, the function returns False.
Scroll validation
This check prevents wasted time if scrolling fails. If the scroll doesn’t move, the scraper knows no additional results can be loaded.
Step 6: Extract business details (ratings, reviews, address, website, hours)
The get_business_info() function collects detailed information about each business. It clicks into the business detail panel and extracts data such as ratings, reviews, address, phone number, website, opening hours, and a short review snippet.
def get_business_info(self, driver, link_element, business_name):
"""Get business details in one go"""
details = {}
try:
# Click on business link to open detail panel
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", link_element)
time.sleep(1)
driver.execute_script("arguments[0].click();", link_element)
time.sleep(4)
# Get page text and filter by business name
body_text = driver.find_element(By.TAG_NAME, "body").text
business_section = ""
if business_name in body_text:
start_idx = body_text.find(business_name)
business_section = body_text[start_idx:start_idx + 1500]
else:
business_section = body_text[:2000]
# Rating
rating_pattern = rf"{re.escape(business_name)}.*?(\d+[.,]\d+)"
rating_match = re.search(rating_pattern, business_section, re.DOTALL)
if rating_match:
details['rating'] = rating_match.group(1).replace(',', '.')
else:
# Fallback
business_idx = business_section.find(business_name)
if business_idx != -1:
after_name = business_section[business_idx + len(business_name):]
rating_match = re.search(r"(\d+[.,]\d+)", after_name[:200])
if rating_match:
rating_val = float(rating_match.group(1).replace(',', '.'))
if 1.0 <= rating_val <= 5.0:
details['rating'] = str(rating_val)
# Reviews count
reviews_pattern = rf"{re.escape(business_name)}.*?\((\d+[.,]*)\)"
reviews_match = re.search(reviews_pattern, business_section, re.DOTALL)
if reviews_match:
details['reviews_count'] = reviews_match.group(1).replace(',', '').replace('.', '')
else:
# Fallback
business_idx = business_section.find(business_name)
if business_idx != -1:
after_name = business_section[business_idx + len(business_name):]
reviews_match = re.search(r"\((\d+[.,]*)\)", after_name[:300])
if reviews_match:
details['reviews_count'] = reviews_match.group(1).replace(',', '').replace('.', '')
# Address (German formats)
address_patterns = [
r'([A-ZÄÖÜ][a-zäöüß]{2,}(?:straße|str\.?|platz|weg|damm|allee)\s*\d+[a-zA-Z]?\s*,?\s*\d{5}\s*[A-ZÄÖÜ][a-zäöüß]+)',
r'([A-ZÄÖÜ][a-zäöüß]{2,}(?:straße|str\.?|platz|weg|damm|allee)\s*\d+[a-zA-Z]?)'
]
for pattern in address_patterns:
matches = re.findall(pattern, business_section)
if matches:
best_address = max(matches, key=len)
if len(best_address) > 8:
details['address'] = best_address.strip()
break
# Phone (Berlin formats)
phone_patterns = [
r'(030\s*\d{8})',
r'(\+49\s*30\s*\d{8})',
r'(\+49\s*\d{2,4}\s*\d{6,8})',
r'(\d{2,4}\s*\d{6,8})'
]
for pattern in phone_patterns:
match = re.search(pattern, business_section)
if match:
phone = match.group(1).strip()
if len(phone) >= 8:
details['phone'] = phone
break
# Website (exclude Google links)
try:
website_links = driver.find_elements(
By.XPATH,
"//a[contains(@href, 'http') and not(contains(@href, 'google'))]"
)
for link in website_links:
try:
href = link.get_attribute('href')
if href and href.startswith('http'):
if link.is_displayed():
details['website'] = href
break
except:
continue
except:
pass
# Hours
hours_patterns = [
r'(Geöffnet|Geschlossen|Öffnet|Schließt).*?(\d{1,2}:\d{2})',
r'(Open|Closed).*?(\d{1,2}:\d{2})',
r'(Opens?|Closes?).*?(\d{1,2}:\d{2})'
]
for pattern in hours_patterns:
match = re.search(pattern, business_section, re.IGNORECASE)
if match:
details['hours'] = match.group(0).strip()
break
# Short review
desc_match = re.search(r'"([^"]{10,100})"', business_section)
if desc_match:
details['review'] = desc_match.group(1).strip()
return details
except Exception as e:
return {}
Key details
- Why click on each business?
Basic info (name, category) is visible in the list, but details (address, phone, website, hours) only appear when you open the business panel. - Scroll into view
Ensures the business link is visible before attempting to click it. - 4-second wait
Allows time for the detail panel to load completely. - Text section isolation
By extracting 1500 characters around the business name, we avoid mixing in details from other businesses on the page.
Data extraction strategies
- Business name anchoring → Ratings and reviews are searched after the business name appears, ensuring accuracy.
- Fallback system → If the anchored search fails, a secondary check is performed just after the business name.
- Address extraction → Since addresses vary by country, regex patterns are used (with examples for German formats: straße, platz, weg, allee + postal codes).
- Phone extraction → Multiple regex patterns handle Berlin numbers (030) and general German formats.
- Website filtering → Captures only real business websites, ignoring Google and social media links.
- Visibility check → link.is_displayed() ensures we only capture links from the active panel.
- Hours patterns → Works with both German (“Geöffnet”, “Schließt”) and English (“Open”, “Closed”).
- Review extraction → Finds short customer reviews in quotes (10–100 characters long).
Country customization
This function is tailored to German formats (addresses, phone numbers). To adapt it for other countries, you’ll need to adjust the regex patterns for local conventions.
Step 7: Export data to JSON
The save_results() function saves the scraped data to a JSON file for later use.
def save_results(self, data, filename="results.json"):
try:
with open(filename, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
print(f"Saved: {filename}")
except Exception as e:
print(f"Save error: {e}")
Key details
- Error handling → Wrapped in a try-except to catch file-writing issues (e.g., disk space, permissions).
- UTF-8 encoding → ensure_ascii=False allows proper saving of special characters (e.g., ä, ö, ü).
- Pretty printing → indent=2 makes the JSON file easy to read.
Step 8: Run the Google Maps scraper and print results
The main() function ties everything together: it launches the scraper, runs a query, prints results, and saves them.
def main():
scraper = GoogleMapsScraper()
print("Google Maps Business Scraper")
print("=" * 40)
# Change these parameters for different searches
results = scraper.search_places("coffee shop", "Berlin", 10)
if results:
print(f"\n{len(results)} results found:")
print("=" * 40)
for i, business in enumerate(results, 1):
print(f"\n{i:2d}. {business['name']}")
print(f" Rating: {business['rating']} ({business['reviews_count']} reviews)")
if business['address'] != 'N/A':
print(f" Address: {business['address']}")
if business['phone'] != 'N/A':
print(f" Phone: {business['phone']}")
if business['website'] != 'N/A':
print(f" Website: {business['website']}")
if business['hours'] != 'N/A':
print(f" Hours: {business['hours']}")
if business['review'] != 'N/A':
print(f" Review: {business['review']}")
scraper.save_results(results, "google_maps_results.json")
else:
print("No results found")
if __name__ == "__main__":
main()
Key details
- Output formatting → Only prints fields that contain actual values (not ‘N/A’), keeping results clean.
- Results enumeration → enumerate(results, 1) numbers businesses starting from 1.
- Conditional display → Fields like phone, website, or hours only appear if found.
Step 9: Customize the Google Maps scraper (queries, scrolling, wait times)
You can easily customize the scraper by changing parameters.
Change search parameters:
# Different business types
results = scraper.search_places("restaurant", "Berlin", 30)
results = scraper.search_places("hotel", "Munich", 50)
results = scraper.search_places("gym", "Hamburg", 25)
# Different result counts
results = scraper.search_places("coffee shop", "Berlin", 100) # More results
results = scraper.search_places("pharmacy", "Berlin", 5) # Fewer results
Adjust scroll behavior:
# In extract_businesses(), change this line:
while len(businesses) < max_results and scroll_count < 8:
# To scroll more times:
while len(businesses) < max_results and scroll_count < 15:
Modify wait times:
# In search_places(), adjust initial wait:
time.sleep(15) # Increase if you have slow internet
# In get_business_info(), adjust detail loading wait:
time.sleep(4) # Increase if details load slowly
When to use 3rd-party tools vs. build your own
Depending on your needs, you can:
- Use a third-party Google Maps extractor
- Providers handle proxy rotation, CAPTCHA solving, and data exports.
- Suitable for non-technical users or businesses that require rapid data access.
- Build a DIY Google Maps scraper if you need flexibility (as shown in the tutorial)
- Offers complete control over what you scrape and how you structure the data.
- Leverage the Google Places API service for compliance
- Has quotas, requires billing, and doesn’t expose all data available on Maps.
Final notes
This Google Maps extractor extracts business data from Google Maps locations, including names, addresses, ratings, reviews, phone numbers, and websites.
You can adapt it for different countries by modifying:
- Address regex patterns (to fit local formats)
- Phone number regex patterns (to match local dialing rules)
Disclaimer: This tutorial is for educational purposes only. Scraping Google Maps data may violate Google’s Terms of Service.
FAQs about Google Maps scrapers

Be the first to comment
Your email address will not be published. All fields are required.