Main Website
Scraping
IN THIS ARTICLE
Don’t miss your 10 free proxies
No credit card required.
Get 10 Proxies For Free
Table of Contents
Updated on
December 8, 2025

How to Scrape Google Maps

TL;DR

  • Learn how to scrape Google Maps business listings using Python.
  • Use Webshare’s rotating residential proxies to get accurate results and avoid Google’s anti-bot detection.
  • Extract structured business data including title, address, phone number, website, rating, reviews count, and coordinates, and export everything into a JSON/CSV spreadsheet for lead-generation and local SEO research.
  • Enable an optional scraping toggle to collect text reviews.

Google Maps is one of the most powerful sources of real-world business data. Whether you're generating leads, analyzing competitors for local SEO, or building location-based datasets, Google Maps results offer rich, high-intent information. Automating this data collection transforms hundreds of business listings into structured spreadsheets you can easily filter, sort, and analyze. In this guide, you’ll learn how to build a Google Maps scraper in Python and turn raw listings into clean data ready for SEO research, lead generation, and more.

Prerequisites

Before building and running the Google Maps scraper, ensure your environment is properly set up with the required Python version, libraries, and proxy configuration.

  • Python Installation: Make sure you have Python 3.9+ installed. Verify your version with:
python --version
  • Required Python Packages: This scraper uses Playwright for browser automation. Install Playwright and its Python bindings:
pip install playwright
  • Browser Setup: After installing Playwright, install the Chromium browser once:
playwright install chromium
  • Built-in Python Modules: These modules come preinstalled with Python but must be available in your environment:
    • asyncio - Manages asynchronous events.
    • json, csv - Handles data export.
    • time, random - Adds human-like delays.
    • re - Uses regex for parsing coordinates.
    • urllib.parse - Encodes URLs safely.
  • Webshare Proxy Access: Google Maps uses aggressive anti-bot protection – IP throttling, fingerprinting, and behavior-based detection. If scraping multiple queries, enable Webshare rotating residential proxies to minimize CAPTCHAs and reduce blocking. These proxies rotate automatically and mimic real residential traffic, making your scraper significantly harder for Google to fingerprint.

Scraping Google Maps

Once your prerequisites are ready and your environment is set up, you can begin scraping business listings from Google Maps. Below are the steps you’ll follow to run the scraper and customize the data you want to extract.

Step 1: Prepare your search queries

Start by creating a list of Google Maps search terms you want to scrape. These can be location-based keywords such as:

search_queries = [
    "Restaurants in New York",
    "Plumbers in London",
    "Gyms near me"
]

Step 2: Choose how many results you want per query

Decide how many business listings you want to extract for each search keyword:

max_results_per_query = 10

Increase this number if you want deeper coverage for each search results page.

Step 3: Enable or disable review scraping

You can include an optional review extraction toggle in the scraper and enable it if you want to collect review text:

SCRAPE_REVIEWS = True

If you only want business details and no reviews:

SCRAPE_REVIEWS = False

This toggle is passed into the scraper when you initialize it:

scraper = GoogleMapsScraper(scrape_reviews=SCRAPE_REVIEWS)

Step 4: Start the Google Maps scraper

Once your configuration is ready, start the scraper by calling:

businesses = await scraper.scrape_queries(
    queries=search_queries,
    use_proxy=USE_PROXY,
    proxy_country=PROXY_COUNTRY,
    max_results_per_query=max_results_per_query
)

During this process, the scraper will:

  • Open Google Maps for each search query
  • Scroll through the results
  • Extract business details like 
    • Title
    • Address
    • Rating
    • Reviews count
    • Phone number
    • Website URL
    • Coordinates (if available in Maps URL)
  • Click into each listing if review scraping is enabled

Step 5: Save the data to json & csv

After scraping is complete, save the results in structured formats:

scraper.save_to_json(businesses, json_filename)
scraper.save_to_csv(businesses, csv_filename)

Here’s the complete code:

import asyncio
import json
import csv
import time
import random
import re
from datetime import datetime
from playwright.async_api import async_playwright
from urllib.parse import quote

class GoogleMapsScraper:
    def __init__(self, scrape_reviews=False):
        self.browser = None
        self.page = None
        self.playwright = None
        self.base_url = "https://www.google.com/maps"
        self.scrape_reviews = scrape_reviews

        self.selectors = {
            'business_card': 'div[role="article"]',
            'title': '.qBF1Pd.fontHeadlineSmall',
            'rating': '.MW4etd',
            'reviews_count': '.UY7F9',
            'address': '.W4Efsd span',
            'business_link': 'a.hfpxzc',
            'phone_number': 'button[data-item-id*="phone"] .Io6YTe',
            'website_url': 'a[data-item-id="authority"]',
            'review_text': '.wiI7pd',
            'review_container': '.MyEned',
            'review_expand_button': 'button.w8nwRe.kyuRq',
        }

    async def setup_browser(self, use_proxy=False, proxy_country='US'):
        """Setup browser with optional proxy"""
        await self.close()

        self.playwright = await async_playwright().start()

        launch_options = {
            'headless': True,
            'args': [
                '--no-sandbox',
                '--disable-setuid-sandbox',
                '--disable-blink-features=AutomationControlled',
            ]
        }

        # Proxy configuration - easily toggleable
        if use_proxy:
          username = f"username-{proxy_country}-rotate" # Enter username
          password = "password" # Enter password
         
          launch_options['proxy'] = {
                'server': 'http://p.webshare.io:80',
                'username': username,
                'password': password
            }

        self.browser = await self.playwright.chromium.launch(**launch_options)

        context = await self.browser.new_context(
            viewport={'width': 1920, 'height': 1080},
            user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
        )

        await context.add_init_script("""
            Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
        """)

        self.page = await context.new_page()

    async def random_delay(self, min_seconds=1, max_seconds=3):
        """Random delay between actions"""
        delay = random.uniform(min_seconds, max_seconds)
        await asyncio.sleep(delay)

    async def extract_coordinates_from_url(self, url):
        """Extract coordinates from Google Maps URL"""
        try:
            if '!3d' in url and '!4d' in url:
                lat_match = re.search(r'!3d(-?\d+\.\d+)', url)
                lng_match = re.search(r'!4d(-?\d+\.\d+)', url)
                if lat_match and lng_match:
                    return {
                        'latitude': float(lat_match.group(1)),
                        'longitude': float(lng_match.group(1))
                    }
        except:
            pass
        return None

    async def extract_address_from_card(self, business_card):
        """Extract address from business card"""
        try:
            address_elements = await business_card.query_selector_all('.W4Efsd span')
            for element in address_elements:
                text = await element.text_content()
                if text and text.strip():
                    # Look for address patterns
                    if (any(char.isdigit() for char in text) and
                        any(indicator in text for indicator in ['St', 'Ave', 'Rd', 'Street', 'Avenue', 'Road', 'Broadway'])):
                        # Clean up the address
                        cleaned_address = re.sub(r'^[·\s]+', '', text.strip())
                        return cleaned_address
        except:
            pass
        return ""

    async def expand_review_if_needed(self, review_container):
        """Click 'More' button to expand truncated reviews"""
        try:
            # Check if there's a "More" button and click it
            expand_button = await review_container.query_selector(self.selectors['review_expand_button'])
            if expand_button:
                await expand_button.click()
                await self.random_delay(0.5, 1)
                return True
        except Exception as e:
            # Sometimes the click might fail, that's okay
            pass
        return False

    async def extract_reviews(self):
        """Extract review texts from business page"""
        reviews = []

        if not self.scrape_reviews:
            return reviews

        try:
            # Wait for reviews to load
            try:
                await self.page.wait_for_selector(self.selectors['review_container'], timeout=15000)
            except:
                print("No review containers found")
                return reviews

            # Scroll to load more reviews
            await self.page.evaluate("""
                const reviewSection = document.querySelector('[data-section-id="reviews"]');
                if (reviewSection) {
                    reviewSection.scrollIntoView();
                }
            """)
            await self.random_delay(2, 3)

            # Extract review containers
            review_containers = await self.page.query_selector_all(self.selectors['review_container'])

            for container in review_containers[:5]:  # Limit to 5 reviews
                try:
                    # Expand review if it has a "More" button
                    await self.expand_review_if_needed(container)

                    # Extract review text from the main review text element
                    review_text_element = await container.query_selector(self.selectors['review_text'])
                    if review_text_element:
                        review_text = await review_text_element.text_content()
                        if review_text and review_text.strip():
                            reviews.append(review_text.strip())

                except Exception as e:
                    print(f"Error extracting individual review: {e}")
                    continue

        except Exception as e:
            print(f"Error extracting reviews: {e}")

        return reviews

    async def extract_phone_website_reviews(self, business_url):
        """Extract phone number, website, and reviews from business URL"""
        phone = ""
        website = ""
        reviews = []

        if not business_url:
            return phone, website, reviews

        try:
            # Navigate directly to the business page
            await self.page.goto(business_url, wait_until='domcontentloaded', timeout=15000)
            await self.random_delay(2, 3)

            # Wait for business details to load
            try:
                await self.page.wait_for_selector('[data-item-id]', timeout=10000)
            except:
                print("Business details didn't load properly")

            # Extract phone number
            try:
                phone_element = await self.page.query_selector(self.selectors['phone_number'])
                if phone_element:
                    phone = await phone_element.text_content()
            except Exception as e:
                print(f"Error extracting phone: {e}")

            # Extract website
            try:
                website_element = await self.page.query_selector(self.selectors['website_url'])
                if website_element:
                    website_href = await website_element.get_attribute('href')
                    if website_href:
                        website = website_href
            except Exception as e:
                print(f"Error extracting website: {e}")

            # Extract reviews if enabled
            if self.scrape_reviews:
                reviews = await self.extract_reviews()

        except Exception as e:
            print(f"Error extracting business details: {e}")

        return phone, website, reviews

    async def extract_business_basic_data(self, business_card):
        """Extract basic business data without clicking into details"""
        business_data = {}

        try:
            # Extract title
            title_element = await business_card.query_selector(self.selectors['title'])
            business_data['title'] = await title_element.text_content() if title_element else ""
            if not business_data['title']:
                return None

            # Extract rating
            rating_element = await business_card.query_selector(self.selectors['rating'])
            if rating_element:
                rating_text = await rating_element.text_content()
                try:
                    business_data['rating'] = float(rating_text) if rating_text else None
                except:
                    business_data['rating'] = None

            # Extract reviews count
            reviews_element = await business_card.query_selector(self.selectors['reviews_count'])
            if reviews_element:
                reviews_text = await reviews_element.text_content()
                reviews_match = re.search(r'\(([\d,]+)\)', reviews_text)
                if reviews_match:
                    try:
                        reviews_count = reviews_match.group(1).replace(',', '')
                        business_data['reviews_count'] = int(reviews_count)
                    except:
                        business_data['reviews_count'] = None

            # Extract address
            business_data['address'] = await self.extract_address_from_card(business_card)

            # Extract business URL for coordinates
            business_link = await business_card.query_selector(self.selectors['business_link'])
            if business_link:
                business_url = await business_link.get_attribute('href')
                if business_url:
                    full_url = f"https://www.google.com{business_url}" if business_url.startswith('/') else business_url
                    business_data['business_url'] = full_url
                    business_data['coordinates'] = await self.extract_coordinates_from_url(full_url)

            # Initialize empty fields
            business_data['phone_number'] = ""
            business_data['website_url'] = ""
            business_data['reviews'] = []

        except Exception as e:
            print(f"Error extracting basic business data: {e}")
            return None

        return business_data

    async def scrape_search_results(self, query, max_results=10):
        """Scrape business listings for a search query"""
        businesses = []

        try:
            # Use Google Maps search URL
            encoded_query = quote(query)
            search_url = f"https://www.google.com/maps/search/{encoded_query}"
            print(f"Navigating to: {search_url}")

            await self.page.goto(search_url, wait_until='domcontentloaded', timeout=30000)
            await self.random_delay(3, 5)

            # Handle cookie consent
            try:
                cookie_button = await self.page.query_selector('button:has-text("Accept all"), button:has-text("I agree")')
                if cookie_button:
                    await cookie_button.click()
                    await self.random_delay(1, 2)
            except:
                pass

            # Wait for business cards
            try:
                await self.page.wait_for_selector(self.selectors['business_card'], timeout=15000)
            except:
                print("No business cards found")
                return businesses

            # Scroll to load more results
            await self.page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
            await self.random_delay(2, 3)

            # Extract all business cards and get basic data first
            business_cards = await self.page.query_selector_all(self.selectors['business_card'])

            basic_business_data = []
            for i, card in enumerate(business_cards[:max_results]):
                business_data = await self.extract_business_basic_data(card)
                if business_data:
                    basic_business_data.append(business_data)
                await self.random_delay(1, 2)

            # Now extract phone, website, and reviews for each business
            for i, business_data in enumerate(basic_business_data):
                if business_data.get('business_url'):
                    phone, website, reviews = await self.extract_phone_website_reviews(business_data['business_url'])
                    business_data['phone_number'] = phone
                    business_data['website_url'] = website
                    business_data['reviews'] = reviews
                    business_data['search_query'] = query
                    business_data['scraped_at'] = datetime.now().isoformat()
                    businesses.append(business_data)

                await self.random_delay(2, 4)  # Longer delay between detail extractions

        except Exception as e:
            print(f"Error scraping search results for '{query}': {e}")

        return businesses

    async def scrape_queries(self, queries, use_proxy=False, proxy_country='US', max_results_per_query=10):
        """Scrape business listings for multiple search queries"""
        all_businesses = []

        await self.setup_browser(use_proxy=use_proxy, proxy_country=proxy_country)

        try:
            for i, query in enumerate(queries):
                print(f"\n{'='*50}")
                print(f"Processing query {i+1}/{len(queries)}: '{query}'")
                print(f"{'='*50}")

                businesses = await self.scrape_search_results(query, max_results_per_query)
                all_businesses.extend(businesses)

                print(f"Completed '{query}': {len(businesses)} businesses found")

                # Random delay between queries
                if i < len(queries) - 1:
                    delay = random.uniform(3, 6)
                    print(f"Waiting {delay:.1f} seconds before next query...")
                    await asyncio.sleep(delay)

        finally:
            await self.close()

        return all_businesses

    async def close(self):
        """Close browser and playwright"""
        try:
            if self.page:
                await self.page.close()
                self.page = None
        except:
            pass

        try:
            if self.browser:
                await self.browser.close()
                self.browser = None
        except:
            pass

        try:
            if self.playwright:
                await self.playwright.stop()
                self.playwright = None
        except:
            pass

    def save_to_json(self, businesses, filename):
        """Save businesses to JSON file"""
        with open(filename, 'w', encoding='utf-8') as f:
            json.dump(businesses, f, indent=2, ensure_ascii=False)
        print(f"Saved {len(businesses)} businesses to {filename}")

    def save_to_csv(self, businesses, filename):
        """Save businesses to CSV file"""
        if not businesses:
            print("No businesses to save")
            return

        fieldnames = ['search_query', 'title', 'address', 'phone_number', 'website_url',
                     'rating', 'reviews_count', 'business_url', 'latitude', 'longitude', 'scraped_at']

        # Add reviews field only if reviews scraping is enabled
        if any('reviews' in business for business in businesses):
            fieldnames.append('reviews')

        with open(filename, 'w', newline='', encoding='utf-8') as f:
            writer = csv.DictWriter(f, fieldnames=fieldnames)
            writer.writeheader()

            for business in businesses:
                row = business.copy()
                # Flatten coordinates for CSV
                if row.get('coordinates'):
                    row['latitude'] = row['coordinates'].get('latitude')
                    row['longitude'] = row['coordinates'].get('longitude')
                else:
                    row['latitude'] = None
                    row['longitude'] = None

                # Remove the coordinates object for CSV
                if 'coordinates' in row:
                    del row['coordinates']

                writer.writerow(row)

        print(f"Saved {len(businesses)} businesses to {filename}")

async def main():
    # === USER CONFIGURATION ===

    # 1. User inputs list of search queries
    search_queries = ["Restaurants in New York", "Plumbers in London", "Gyms near me"]

    # 2. Configure scraping parameters
    max_results_per_query = 10

    # REVIEWS TOGGLE - Set to True to scrape reviews, False to skip reviews
    SCRAPE_REVIEWS = True  # Change this to False if you don't want reviews

    # PROXY TOGGLE - Set to True to use proxy
    USE_PROXY = True
    PROXY_COUNTRY = 'US' # Change this to your desired country (US, UK, CA, DE, etc.)

    # Initialize scraper with reviews toggle
    scraper = GoogleMapsScraper(scrape_reviews=SCRAPE_REVIEWS)

    print("Starting Google Maps Scraper")
    print(f"Queries: {search_queries}")
    print(f"Max results per query: {max_results_per_query}")
    print("=" * 50)

    # Scrape businesses
    start_time = time.time()
    businesses = await scraper.scrape_queries(
        queries=search_queries,
        use_proxy=USE_PROXY,
        proxy_country=PROXY_COUNTRY,
        max_results_per_query=max_results_per_query
    )
    end_time = time.time()

    # Save results
    if businesses:
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        json_filename = f"google_maps_{timestamp}.json"
        csv_filename = f"google_maps_{timestamp}.csv"

        scraper.save_to_json(businesses, json_filename)
        scraper.save_to_csv(businesses, csv_filename)

        # Print summary
        print(f"\n{'='*50}")
        print("SCRAPING COMPLETED!")
        print(f"{'='*50}")
        print(f"Total businesses found: {len(businesses)}")

        for query in search_queries:
            count = len([b for b in businesses if b['search_query'] == query])
            print(f"   - '{query}': {count} businesses")

    else:
        print("No businesses found!")

# Run the scraper
await main()

Note: This code uses await main() which works in Google Colab and Jupyter notebooks. For regular Python scripts, use this instead:

if __name__ == "__main__":
    asyncio.run(main())

The console output is as:

Wrapping up: Scrape Google Maps

In this guide, we built a Google Maps scraper using Python and Playwright, enhanced with Webshare rotating residential proxies to avoid Google’s anti-bot mechanisms. The scraper collects structured business data – including names, addresses, phone numbers, websites, ratings, review counts, and coordinates – and can optionally extract text reviews for deeper insights. By automating Google Maps data extraction, you can quickly generate organized JSON or CSV datasets for lead generation or local SEO research, all while minimizing CAPTCHAs, IP blocks, and other anti-bot challenges.