Back to Blog
Guides
Andrei OgiolanLast updated on May 7, 202615 min read

How to Scrape Google Maps for Reviews: A Practical Python Guide

How to Scrape Google Maps for Reviews: A Practical Python Guide
TL;DR: Figuring out how to scrape Google Maps for reviews comes down to three method tracks: a DIY Selenium scraper behind a rotating proxy, a scraping API with render instructions, or a structured Maps Reviews API that returns parsed JSON. This guide walks through all three in Python with copy-pasteable code, pagination patterns, anti-block tactics, and a final cleaning step that turns raw reviews into something a business can actually use.

Introduction

If you have ever tried to figure out how to scrape Google Maps for reviews at any real volume, you already know the pain points: the official Places API returns at most a handful of reviews per place, JavaScript renders most of the listing, and the CSS class hooks rotate often enough to break a scraper between Friday and Monday. The good news is that the data is reachable, you just have to pick the right tool for the job.

This guide is written for Python developers, data analysts, and growth teams who want business reviews from Google Maps for sentiment analysis, competitor research, location intelligence, or lead generation. We will cover three method tracks, side by side: Selenium plus a proxy for full control, a scraping API with render instructions for a lower-code path, and a per-review JSON API when you need clean, structured fields like review_id, iso_date_of_last_edit, and owner responses.

You will walk away with working code, a comparison table to defend the approach you pick, and a short pipeline for cleaning and analyzing what you collect. Where the underlying selectors or quotas are known to drift, the article calls that out so you can plan for it instead of debug it at 2 a.m.

How to scrape Google Maps for reviews: business value and what fields you can extract

Reviews are one of the highest-signal datasets on the public web. A clean Maps review feed lets you benchmark a competitor's customer experience, monitor reputation across locations, build location-aware lead lists, score a regional sentiment index, or train a recommendation model on real human commentary. None of that works if you only have star averages, which is exactly what most generic listing scrapers stop at.

Realistically, the fields you can pull from Google Maps fall into two layers. At the listing layer you get place name, address, overall rating, review count, category, hours, phone, and service options like dine-in or delivery. At the per-review layer you get review text (the snippet), reviewer name, profile link, star rating, review date or last edit date, photo URLs, an owner response when present, and topic tags such as Service or Atmosphere. Knowing this split up front lets you decide whether listing-level scraping is enough, or whether you need to drill into individual reviews. The market-research and review-monitoring use cases almost always need the second layer.

Three ways to pull Maps review data: official API vs DIY scraper vs scraping API

When you research how to scrape Google Maps for reviews, three paths show up. Picking the wrong one is the most common reason teams give up halfway, so spend a few minutes here before you write code.

The official Google Places API is the cleanest legally, but it is built for app integrations, not analytics. Per Google's docs, place details requests return only a small, capped subset of the reviews shown in the Maps UI, which makes it a poor fit for sentiment scoring at any scale. A DIY Selenium scraper plus a residential proxy gives you full control and the same view a logged-out user would see, at the cost of running a browser and chasing CSS changes. A scraping API in the middle absorbs proxy rotation, CAPTCHA handling, and headless rendering, and a per-review JSON API on top of that absorbs parsing as well.

Use the table below as a defensible decision matrix.

Approach

Setup time

Scale ceiling

Anti-bot handling

Maintenance

When to use it

Official Google Places API

Low

Low (per-place review caps)

Built-in, but quota-bound

Low

Embedding a few reviews into your own product UI

DIY Selenium plus proxy

Medium

Medium

Manual: proxies, waits, retries

High (CSS drift)

Full control, custom flows, modest volume

SERP API

Low

High

Handled by provider

Low

Repeatable jobs, scheduled crawls

Per-review JSON API

Lowest

High

Handled by provider

Lowest

Clean fields, no parsing, fastest path to data

Anti-bot challenges unique to Google Maps

Maps is harder to scrape than a typical e-commerce site, and not for the reasons people expect. The first thing you hit is the cookie consent wall, which blocks the search results panel from rendering until it is dismissed. The second is that Maps is a JavaScript-only experience, so a plain requests.get returns a near-empty shell. The third, and the one that breaks scrapers most often, is that the listing panel uses scroll-driven loading instead of paginated URLs. There is no ?page=2 you can hit.

On top of that, Google obfuscates and rotates CSS class names. Hooks like hfpxzc, MW4etd, UY7F9, and Nv2PK were valid at the time of writing, but they should be re-verified before publish or run because they change often. Aggressive request patterns from a single IP also draw rate limits and the occasional reCAPTCHA, which is why a residential proxy pool and randomized waits are not optional once you go past a handful of queries.

Prerequisites: Python, Chrome, libraries, and an API key

You will need Python 3.10 or newer and a recent Chrome install. The Selenium track also needs a few packages. Install them with pip:

pip install selenium selenium-wire webdriver-manager beautifulsoup4 lxml requests

Each one earns its keep. Selenium drives the browser. Selenium Wire is a thin extension that exposes the underlying HTTP requests so you can attach an authenticated proxy without juggling Chrome flags. Webdriver Manager handles the ChromeDriver binary so you do not have to pin a specific version on every laptop. BeautifulSoup with the lxml backend parses the rendered HTML, and requests is enough for the API tracks.

For Methods 2 and 3, sign up for a SERP API account before you start. You can usually grab a free credit allowance to test against, then plug the key into the code blocks below.

Method 1 - Selenium plus proxy walkthrough

The textbook answer to how to scrape Google Maps for reviews is a real browser plus a rotating residential proxy. This is the path with the most surface area and the most control. You drive a Chrome instance, route its traffic through the proxy, scroll the panel until enough listings load, and parse the page source with BeautifulSoup. It is the right choice when you need flexibility (custom click flows, screenshot capture, login-gated business profiles) or your scale is small enough to babysit selectors. The five subsections below walk through every step end to end, with code you can paste straight into a script.

Configure Selenium Wire to route Chrome through a proxy

The trick to scraping Google Maps reviews with Selenium is sending every request through a rotating residential proxy. Plain Selenium cannot pass HTTP basic auth credentials cleanly, which is where Selenium Wire comes in. It exposes the proxy block in seleniumwire_options and handles the auth handshake under the hood. The proxy URL pattern most providers expect looks like http://<username>:<password>@<host>:<port>, with extra params such as render or country_code appended to the username. Confirm the exact format against your provider's current docs before you run this in production, because URL conventions vary.

from seleniumwire import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager

API_KEY = "YOUR_API_KEY"
PROXY = (
    f"http://scrape.render=true.country_code=us:{API_KEY}"
    "@proxy-server.example.com:8001"
)

options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
options.add_argument("--window-size=1920,1080")

sw_options = {"proxy": {"http": PROXY, "https": PROXY, "no_proxy": "localhost,127.0.0.1"}}

driver = webdriver.Chrome(
    service=Service(ChromeDriverManager().install()),
    seleniumwire_options=sw_options,
    options=options,
)
driver.implicitly_wait(30)

Headless mode keeps memory usage lower on servers, and the 30-second implicit wait gives Maps time to render before any selector lookups fire. If you want a deeper Selenium primer, the language-bindings docs walk through every option in detail.

Once the driver is wired up, point it at a search URL such as https://www.google.com/maps/search/pizza+in+new+york. Maps usually surfaces a cookie consent dialog the first time a session loads, and the layout will not let you scroll the results until you dismiss it. Wrap the click in a try/except, because proxy-rendered traffic in some regions skips the dialog entirely.

from selenium.webdriver.common.by import By

driver.get("https://www.google.com/maps/search/pizza+in+new+york")

try:
    accept_btn = driver.find_element(
        By.XPATH, "//button[.//span[contains(text(), 'Accept all')]]"
    )
    accept_btn.click()
except Exception:
    print("Cookie consent not shown for this session.")

Always log the miss instead of letting the exception bubble. You want the run to continue when the dialog is absent, not abort. If your locale renders a different button label, swap the XPath text or fall back to an aria-label match on the consent banner.

Scroll the results panel to trigger lazy-loaded listings

Maps does not paginate. As you scroll the left-hand panel, it streams more listings into the DOM and eventually hits an end marker. To extract reviews from Google Maps at any reasonable scale, you have to drive that scroll yourself. Two patterns work well. The first uses keystrokes through ActionChains, which feels like a real user. The second uses a JavaScript executor, which is faster but more fingerprintable.

import time
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys

def scroll_panel_down(driver, panel_xpath, presses=5, pause_time=1):
    panel = driver.find_element(By.XPATH, panel_xpath)
    actions = ActionChains(driver)
    actions.move_to_element(panel).click().perform()
    for _ in range(presses):
        actions.send_keys(Keys.PAGE_DOWN).perform()
        time.sleep(pause_time)

# Cleaner JS-executor alternative
def scroll_panel_js(driver, panel_xpath, rounds=8, pause=1.2):
    panel = driver.find_element(By.XPATH, panel_xpath)
    for _ in range(rounds):
        driver.execute_script("arguments[0].scrollTop = arguments[0].scrollHeight", panel)
        time.sleep(pause)

Tune presses and pause_time against your target query. Five page-downs with a one-second pause is a reasonable starting point, but a dense city query may need 15 or more rounds before the panel reports 'You've reached the end of the list'. Watch the rendered HTML and stop when new listings stop appearing.

Parse business names, ratings, and review counts with BeautifulSoup

Once the panel has loaded enough listings, hand the page source to BeautifulSoup and pull the fields out. The class hooks below were valid at the time of writing, but Maps rotates them on its own schedule, so always run a fresh DOM check before a production run.

from bs4 import BeautifulSoup

soup = BeautifulSoup(driver.page_source, "lxml")

# These class names rotate. Re-verify before each run.
titles = soup.find_all("a", class_="hfpxzc")        # listing anchors
ratings = soup.find_all("span", class_="MW4etd")      # numeric rating
counts = soup.find_all("span", class_="UY7F9")        # review count

places = []
for i, t in enumerate(titles):
    place = {
        "name": t.get("aria-label") or t.get_text(strip=True),
        "url": t.get("href"),
        "rating": ratings[i].get_text(strip=True) if i < len(ratings) else "N/A",
        "review_count": counts[i].get_text(strip=True) if i < len(counts) else "N/A",
    }
    places.append(place)

Two resilience habits matter here. First, prefer the aria-label attribute over the visible text, because the label is more stable across UI tweaks. Second, use index-safe lookups so a missing rating does not crash the loop. If you want a deeper BeautifulSoup primer, an internal walkthrough on parsing patterns is a useful companion read for more advanced selector strategies.

Persist results to CSV with N/A fallbacks

The last step in Method 1 is writing your parsed listings to disk. CSV is fine for one-off jobs, and switching to Parquet or a database is a one-line change later.

import csv

with open("maps_pizza_places.csv", "w", encoding="utf-8", newline="") as f:
    writer = csv.writer(f)
    writer.writerow(["name", "rating", "review_count", "url"])
    for p in places:
        writer.writerow([
            p.get("name") or "N/A",
            f"{p['rating']}/5" if p["rating"] != "N/A" else "N/A",
            p.get("review_count") or "N/A",
            p.get("url") or "N/A",
        ])

driver.quit()

Always default missing fields to a sentinel like N/A instead of an empty cell. Empty cells are ambiguous in downstream pipelines (was the field missing, or was the scrape broken?), and they make data-quality checks harder. Calling driver.quit() at the end releases the Chrome process and any proxy connections it was holding.

Method 2 - SERP API (low-code path)

Method 1 is fine for prototypes, but it is heavy. The cleaner production answer to how to scrape Google Maps for reviews at scale is to delegate rendering and proxy rotation to a scraping API and send a JSON instruction set that tells a hosted headless browser what to do on the page. You get the same final HTML, with none of the local browser overhead, and the provider absorbs the retries and proxy rotation you would otherwise own yourself.

Most providers, including our WebScrapingAPI SERP API, accept a JSON instruction set in the request headers describing scrolls, clicks, and waits. The example below dismisses the cookie banner, scrolls the results panel a few times, and returns the final HTML in a single request. Confirm the exact header names and instruction schema against the provider's current docs, since these details vary.

import json, requests
from bs4 import BeautifulSoup

API_KEY = "YOUR_API_KEY"
TARGET_URL = "https://www.google.com/maps/search/pizza+in+new+york"

instructions = [
    {"type": "click", "selector": "button[aria-label='Accept all']", "optional": True},
    {"type": "scroll", "selector": "div[role='feed']", "repeat": 8, "delay": 1500},
    {"type": "wait", "value": 2000},
]

headers = {
    "x-api-key": API_KEY,
    "x-render-js": "true",
    "x-instruction-set": json.dumps(instructions),
}

resp = requests.get("https://api.example-scraper.com/v1",
                    params={"url": TARGET_URL}, headers=headers, timeout=120)

soup = BeautifulSoup(resp.text, "lxml")
listings = soup.find_all("div", class_="Nv2PK")  # re-verify class before each run
print(f"Got {len(listings)} listings.")

Three things make this pattern worth the swap. First, you are no longer running and updating Chrome on every worker. Second, the provider rotates proxies and retries failed requests for you, which removes most of the bad-day operational toil. Third, the entire pipeline can sit in a cron job or serverless function with no browser dependency.

Method 3 - Pulling individual review fields (text, date, sort, topic filters)

Listing-level scraping tells you which places matter, but the business value lives one layer deeper, in the actual review text. The shortest answer to how to scrape Google Maps for reviews at the per-review level is to skip parsing entirely and use a structured Maps Reviews API. You look up a place's data_id once, then call the reviews endpoint with the params you need.

The shape is intuitive. engine selects the Maps Reviews engine. data_id identifies the place. sort_by controls ordering: qualityScore (the default, most relevant), newestFirst, ratingHigh, or ratingLow. topic_id filters to a specific theme, for example an Amenities topic id pulled from the structured response (the brief flagged a sample id of /m/0f3yyn for verification). Optional fields like Snippet, Response, and Response.iso_date are zero-indexed, with the first review at index 0.

import requests

params = {
    "engine": "google_maps_reviews",
    "data_id": "0x80dcd1f0c5cb6f53:0xd2c4f5c30e2b7c5a",
    "sort_by": "newestFirst",
    "api_key": "YOUR_API_KEY",
}

r = requests.get("https://api.example-reviews.com/search", params=params, timeout=60).json()

for rev in r.get("reviews", []):
    print({
        "user": rev.get("user", {}).get("name"),
        "rating": rev.get("rating"),
        "date": rev.get("iso_date_of_last_edit"),
        "text": rev.get("snippet", "No review text, rating only"),
        "owner_response": rev.get("response", {}).get("snippet", "No response from owner"),
    })

Always default missing fields. Reviewers often leave a star rating with no text, and most owners never respond, so the JSON will have holes.

Pagination and getting more than the first batch of reviews

Both API and DIY tracks cap the first batch at a small number. According to the providers' docs, a typical Maps Reviews API returns roughly 10 reviews per request and exposes a next_page_token (or equivalent cursor) in the response. The pagination loop is the same shape you would use for any token-based API: keep calling until the token field is missing.

all_reviews, token = [], None
while True:
    p = dict(params, **({"next_page_token": token} if token else {}))
    r = requests.get(URL, params=p, timeout=60).json()
    all_reviews.extend(r.get("reviews", []))
    token = r.get("serpapi_pagination", {}).get("next_page_token")
    if not token:
        break

On the Selenium side, pagination is just continued scrolling inside the place card's reviews tab. Watch for the 'You've reached the end of the list' marker and stop when the rendered review count stops growing across two consecutive scrolls.

Avoiding IP bans, CAPTCHAs, and selector breakage

Once you understand how to scrape Google Maps for reviews mechanically, the longer game is staying unblocked. Volume and stealth are a tradeoff, not a fixed setting. The single biggest lever is your proxy pool. Datacenter IPs get flagged on Maps queries far faster than residential ones, so once you go past a few dozen requests an hour, plan to rotate through residential IPs with country-level targeting. Mix in randomized waits between actions (300 to 1500 ms is a reasonable band), back off exponentially on 429s, and always cap retries so a bad target does not eat your whole budget.

Selector drift is the second failure mode. Treat hooks like hfpxzc, MW4etd, and Nv2PK as volatile. Build a thin DOM-monitoring step that fails loudly when a selector stops matching anything, prefer aria-label and structural selectors over class names where you can, and keep at least one fallback per critical field. A short tip sheet on common blocking signals and how to dodge them is worth bookmarking, and our team's longer guide on avoiding IP bans goes deeper on header rotation and fingerprinting (a useful companion read when a once-stable script suddenly starts returning 403s).

Cleaning, storing, and analyzing the scraped review data

The point of all this is decision-ready data, not a giant CSV. A short post-processing pipeline turns raw reviews into something a product or marketing team can act on.

import pandas as pd
from textblob import TextBlob

df = pd.DataFrame(all_reviews)
df = df.drop_duplicates(subset=["review_id"])  # dedupe
df["rating"] = df["rating"].astype(float).clip(0, 5)  # normalize
df["text"] = df["snippet"].fillna("").str.replace(r"[\u200b-\u200f]", "", regex=True)
df["sentiment"] = df["text"].apply(lambda t: TextBlob(t).sentiment.polarity if t else 0.0)

Four moves do most of the work: dedupe by review_id, clip ratings to a 0-5 range, strip zero-width and emoji noise from the text, and tag a sentiment polarity per row with a lightweight NLP library. From there it is a straight Pandas-to-SQL or Pandas-to-warehouse step. That output is what powers a review-monitoring dashboard or a competitor sentiment benchmark, which closes the loop back to the use cases we opened with.

Key Takeaways

  • The official Google Places API is convenient for embedding a few reviews into a product, but its per-place caps make it the wrong tool for analytics or competitor research.
  • A Selenium plus residential proxy stack gives you the most control. Plan for cookie banners, scroll-driven loading, and rotating CSS class hooks like hfpxzc and Nv2PK.
  • A scraping API with a JSON render instruction set removes the local browser entirely and is the cleanest path for scheduled, repeatable jobs.
  • A structured Maps Reviews API is the fastest route to clean per-review fields. Use sort_by, topic_id, and next_page_token to control depth and slice.
  • Treat the pipeline as a pipeline. Dedupe by review_id, normalize ratings, strip noise, and tag sentiment so the output is decision-ready, not just stored.

Frequently asked questions

Public review data is generally accepted as scrapeable in many jurisdictions when used for analysis or research, but Google's Terms of Service restrict automated access to its products, and U.S. case law on public-data scraping continues to evolve. Always check Maps' terms, respect robots.txt, avoid scraping personal data beyond what is publicly visible, and consult counsel before commercial use.

Why not just use the official Google Places API instead of a scraper?

The Places API is rate-limited, requires a billable Google Cloud project, and returns only a small subset of the reviews shown in the Maps UI. That makes it fine for embedding a few reviews into a product, but unworkable for sentiment analysis, competitor benchmarking, or any use case that needs a representative sample.

Can I scrape Google Maps reviews without Selenium or a headless browser?

Not reliably with raw requests, because Maps is a JavaScript-heavy SPA. The realistic alternatives are a scraping API that runs a hosted headless browser for you, or a structured Reviews API that returns parsed JSON without rendering anything client side.

How often do Google Maps CSS classes change, and how do I keep my scraper alive?

Often enough that you should not assume any class hook is permanent. Defend against drift with aria-label selectors, structural XPath fallbacks, a small DOM-monitoring job that fails loudly when a selector returns zero matches, and an alert that pings you before the next scheduled run.

How many reviews can I pull per business in one run, and how does pagination work?

A typical Maps Reviews API returns about 10 reviews per request and a next_page_token cursor for the next batch. There is no fixed total cap, but extremely old reviews get harder to retrieve. Loop through tokens until none is returned, and on the Selenium side keep scrolling the reviews tab until the count stops growing.

Wrapping up

There is no single right answer to how to scrape Google Maps for reviews. The right answer depends on how much data you need, how often you need it, and how much operational toil you can absorb. For a one-off audit of a competitor's locations, Selenium plus a residential proxy is plenty. For a recurring sentiment pipeline that has to keep running while Google rotates class names and tightens its anti-bot stack, a managed scraping API or a structured Maps Reviews API is the path that does not wake you up at night.

Whichever method you pick, treat the output like data and not like HTML. Dedupe by review id, normalize ratings, strip noise, score sentiment, and load it somewhere queryable so the work pays off in a dashboard or model rather than a stale CSV.

If you would rather not run and maintain a Chrome fleet just to keep up with Maps' UI changes, our Scraper SERP API at WebScrapingAPI handles the proxy rotation, headless rendering, and CAPTCHA solving behind a single endpoint, so you can keep the Python code in this guide and just swap out the fetch layer. Spin up an API key, point it at a Maps URL, and start collecting clean review data within an afternoon.

About the Author
Andrei Ogiolan, Full Stack Developer @ WebScrapingAPI
Andrei OgiolanFull Stack Developer

Andrei Ogiolan is a Full Stack Developer at WebScrapingAPI, contributing across the product and helping build reliable tools and features for the platform.

Start Building

Ready to Scale Your Data Collection?

Join 2,000+ companies using WebScrapingAPI to extract web data at enterprise scale with zero infrastructure overhead.