HasData
Back to all posts

How to Scrape Google Maps Reviews

This article is about scraping Google Maps reviews, and it covers three ways to do it:

  1. Writing your own Python script. A step-by-step Selenium scraper, with ready-to-run code you can adapt.
  2. Using a Google Maps Reviews API. One request per page of reviews, structured JSON back, and the proxy pool and rendering on our side.
  3. Using a no-code Google Maps Reviews scraper. The same data through a form, for anyone who doesn’t write code.

Before the walkthroughs, here’s how the three routes compare:

Python + SeleniumReviews APINo-code scraper
You maintainChrome, selectors, IPs, consent screensthe code that calls one endpointnothing
When Google changes markupyour script breaksthe schema staysthe schema stays
Paginationscrolling you writenextPageTokena reviews-count field
Cost per 1,000 reviewsyour time and proxiesabout $0.15 on the entry plan$0.30 on the entry plan

The full cost table, with competitors, closes the article. Now the walkthroughs.

Method 1. Scrape Google Maps Reviews with Python

Reviews on Google Maps render client-side, so Requests with BeautifulSoup or Scrapy never see them. The scraper needs a real browser, and Selenium drives one from Python.

Extract Google Maps Reviews for a Specific Place

The full script comes first, and each step is broken down below it. It builds the page URL from a place_id instead of a hardcoded link, handles the consent screen, and waits for review cards rather than sleeping blind:

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
import time

PLACE_ID = "ChIJifIePKtZwokRVZ-UdRGkZzs"  # Joe's Pizza Broadway
url = f"https://www.google.com/maps/place/?q=place_id:{PLACE_ID}&hl=en"

chrome_options = Options()
chrome_options.add_argument("--headless=new")
chrome_options.add_argument("--lang=en-US")
driver = webdriver.Chrome(options=chrome_options)

driver.get(url)
time.sleep(8)

# EU and some VPN exits land on a consent screen first
if "consent" in driver.current_url:
    buttons = driver.find_elements(By.XPATH, "//button[contains(., 'Accept all')]")
    if buttons:
        buttons[0].click()

# open the reviews pane
tabs = driver.find_elements(By.XPATH, "//button[contains(@aria-label, 'Reviews')]")
if tabs:
    driver.execute_script("arguments[0].click();", tabs[-1])

# wait until review cards exist instead of sleeping blind
review_elements = []
for _ in range(15):
    time.sleep(2)
    review_elements = driver.find_elements(By.CSS_SELECTOR, "div.jftiEf")
    if len(review_elements) >= 5:
        break

reviews = []
for review_elem in review_elements:
    def first(selector):
        found = review_elem.find_elements(By.CSS_SELECTOR, selector)
        return found[0] if found else None

    name_el = first(".d4r55")
    text_el = first(".wiI7pd")
    date_el = first(".rsqaWe")
    stars_el = first("span.kvMYJc")

    rating = 0
    if stars_el:
        label = stars_el.get_attribute("aria-label") or ""   # e.g. "4 stars"
        digits = [int(s) for s in label.split() if s.isdigit()]
        rating = digits[0] if digits else 0

    photo_links = []
    for photo_elem in review_elem.find_elements(By.CSS_SELECTOR, ".Tya61d"):
        style = photo_elem.get_attribute("style") or ""
        if 'url("' in style:
            photo_links.append(style.split('url("')[1].split('")')[0])

    reviews.append({
        "reviewer_name": name_el.text.strip() if name_el else "No name",
        "review_text": text_el.text.strip() if text_el else "No review text",
        "rating": rating,
        "review_date": date_el.text.strip() if date_el else "No date",
        "photo_links": photo_links,
    })

for review in reviews:
    print(f"{review['reviewer_name']} | {review['rating']} stars | {review['review_date']}")
    print(review["review_text"][:100])
    print("-" * 50)

driver.quit()

Any place opens as https://www.google.com/maps/place/?q=place_id:{PLACE_ID}, and the place_id comes from a share link, from Google’s place-id finder, or from the Maps search API covered below (it returns placeId and dataId for every result). For a link that opens with the reviews pane already active, there’s a second buildable form, .../@{lat},{lng},17z/data=!4m8!3m7!1s{dataId}!8m2!3d{lat}!4d{lng}!9m1!1b1!16s?hl=en, which takes the dataId and coordinates instead. Google still renames its CSS classes now and then, so check the selector table below against the live page before a long run.

Let’s break this script down step by step. First, you’ll need to import the required libraries and modules:

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
import time

Next, we build the URL from the place id and initialize a headless Chrome:

PLACE_ID = "ChIJifIePKtZwokRVZ-UdRGkZzs"
url = f"https://www.google.com/maps/place/?q=place_id:{PLACE_ID}&hl=en"

chrome_options = Options()
chrome_options.add_argument("--headless=new")
chrome_options.add_argument("--lang=en-US")
driver = webdriver.Chrome(options=chrome_options)

Navigate to the page, give it a moment, and handle the consent screen that Google shows to EU and many VPN exits before any Maps content loads:

driver.get(url)
time.sleep(8)

if "consent" in driver.current_url:
    buttons = driver.find_elements(By.XPATH, "//button[contains(., 'Accept all')]")
    if buttons:
        buttons[0].click()

Without the consent click the script sees a cookie wall instead of the place, and every selector search returns nothing.

Next comes the page analysis, working out the correct selectors for the elements you want to scrape:

DataCSS Selector
Reviewer Namediv.jftiEf .d4r55
Review Textdiv.jftiEf .wiI7pd
Ratingdiv.jftiEf span.kvMYJc (its aria-label)
Review Datediv.jftiEf .rsqaWe
Photo Linksdiv.jftiEf .Tya61d

The page’s class names are generated and change from time to time, so check them against the live page before a long run.

Once you have the selectors, open the reviews pane, wait for cards to exist, and collect the data. Every lookup goes through find_elements, which returns an empty list on a miss, so an absent field becomes a default value instead of a NoSuchElementException. The rating comes from the star element’s aria-label (“4 stars”), which survives redesigns better than counting star icons:

tabs = driver.find_elements(By.XPATH, "//button[contains(@aria-label, 'Reviews')]")
if tabs:
    driver.execute_script("arguments[0].click();", tabs[-1])

review_elements = []
for _ in range(15):
    time.sleep(2)
    review_elements = driver.find_elements(By.CSS_SELECTOR, "div.jftiEf")
    if len(review_elements) >= 5:
        break

reviews = []
for review_elem in review_elements:
    def first(selector):
        found = review_elem.find_elements(By.CSS_SELECTOR, selector)
        return found[0] if found else None

    name_el, text_el = first(".d4r55"), first(".wiI7pd")
    date_el, stars_el = first(".rsqaWe"), first("span.kvMYJc")

    rating = 0
    if stars_el:
        label = stars_el.get_attribute("aria-label") or ""
        digits = [int(s) for s in label.split() if s.isdigit()]
        rating = digits[0] if digits else 0

    photo_links = []
    for photo_elem in review_elem.find_elements(By.CSS_SELECTOR, ".Tya61d"):
        style = photo_elem.get_attribute("style") or ""
        if 'url("' in style:
            photo_links.append(style.split('url("')[1].split('")')[0])

    reviews.append({
        "reviewer_name": name_el.text.strip() if name_el else "No name",
        "review_text": text_el.text.strip() if text_el else "No review text",
        "rating": rating,
        "review_date": date_el.text.strip() if date_el else "No date",
        "photo_links": photo_links,
    })

Finally, we print the data and close the webdriver:

for review in reviews:
    print(f"{review['reviewer_name']} | {review['rating']} stars | {review['review_date']}")
    print(review["review_text"][:100])
    print("-" * 50)

driver.quit()

The script prints:

Aleksandra | 3 stars | a month ago
Super full, big queue, what is understandable because place is very known...
--------------------------------------------------
Maha | 4 stars | 2 months ago
We had a nice experience at Joe's Pizza on Broadway. Everything...
--------------------------------------------------
CK Yu (KeN) | 5 stars | a month ago
If you're looking for an authentic New York pizza experience...

The pane renders five reviews before any scrolling. To load more, you’ll need to implement infinite scrolling to load additional reviews from Google Maps.

Scrape Google Reviews for Multiple Places

We need to modify the script we discussed earlier to find places based on a specific query and gather customer feedback from all relevant locations. If you want only the code, here’s the final version:

import json
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
import time


keyword = "pizza in new york usa places"  # Change keyword here


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


def collect_place_links(keyword):
    driver.get(f"https://www.google.com/maps/search/{keyword.replace(' ', '+')}/")
    time.sleep(3)
    return [elem.get_attribute('href') for elem in driver.find_elements(By.CSS_SELECTOR, 'a.hfpxzc')]


def scrape_reviews():
    reviews = []
    for review_elem in driver.find_elements(By.CSS_SELECTOR, 'div.jftiEf'):
        reviewer_name = review_elem.find_element(By.CSS_SELECTOR, '.d4r55').text.strip() if review_elem.find_elements(By.CSS_SELECTOR, '.d4r55') else 'No name'
        review_text = review_elem.find_element(By.CSS_SELECTOR, '.wiI7pd').text.strip() if review_elem.find_elements(By.CSS_SELECTOR, '.wiI7pd') else 'No review text'
        stars = review_elem.find_elements(By.CSS_SELECTOR, 'span.kvMYJc')
        label = stars[0].get_attribute('aria-label') if stars else ''
        rating = next((int(s) for s in label.split() if s.isdigit()), 0)
        review_date = review_elem.find_element(By.CSS_SELECTOR, '.rsqaWe').text.strip() if review_elem.find_elements(By.CSS_SELECTOR, '.rsqaWe') else 'No date'
        photo_links = [photo.get_attribute('style').split('url("')[1].split('")')[0] for photo in review_elem.find_elements(By.CSS_SELECTOR, '.Tya61d')]

        reviews.append({
            'reviewer_name': reviewer_name,
            'review_text': review_text,
            'rating': rating,
            'review_date': review_date,
            'photo_links': photo_links
        })
    return reviews


def navigate_to_reviews(links):
    all_reviews = {}
    for href in links:
        driver.get(href)
        time.sleep(3)
        try:
            driver.find_element(By.XPATH, "//button[contains(@aria-label, 'Reviews')]").click()
            time.sleep(3)
            all_reviews[href] = scrape_reviews()
            print(f"Scraped reviews for {href}")
        except Exception as e:
            print(f"Failed to extract reviews for {href}: {str(e)}")
    return all_reviews


def save_to_file(data, filename="reviews.json"):
    with open(filename, 'w', encoding='utf-8') as f:
        json.dump(data, f, ensure_ascii=False, indent=4)
    print(f"Data saved to {filename}")


links = collect_place_links(keyword)
all_reviews = navigate_to_reviews(links)
save_to_file(all_reviews)


driver.quit()

This version adds functionality to search for places using a keyword, collect links to multiple locations, and extract reviews for each location. We save the data in a JSON file because printing everything to the console would be overwhelming. After running the script, we end up with 204 reviews from different places.

The review-scraping part from the previous example has been refactored into a separate function, scrape_reviews(), so we won’t repeat that here. To collect links to different locations on Google Maps, we generate a search URL using a keyword from a variable and then extract all the links matching a specific selector:

def collect_place_links(keyword):
    driver.get(f"https://www.google.com/maps/search/{keyword.replace(' ', '+')}/")
    time.sleep(3)
    return [elem.get_attribute('href') for elem in driver.find_elements(By.CSS_SELECTOR, 'a.hfpxzc')]

Next, we loop through the collected links, navigate to the “Reviews” section on each page, and scrape the reviews:

def navigate_to_reviews(links):
    all_reviews = {}
    for href in links:
        driver.get(href)
        time.sleep(3)
        try:
            driver.find_element(By.XPATH, "//button[contains(@aria-label, 'Reviews')]").click()
            time.sleep(3)
            all_reviews[href] = scrape_reviews()
            print(f"Scraped reviews for {href}")
        except Exception as e:
            print(f"Failed to scrape Google reviews for {href}: {str(e)}")
    return all_reviews

Finally, we save all the data to a file:

def save_to_file(data, filename="reviews.json"):
    with open(filename, 'w', encoding='utf-8') as f:
        json.dump(data, f, ensure_ascii=False, indent=4)
    print(f"Data saved to {filename}")

Here’s an example of what the JSON output looks like:

{
    "https://www.google.com/maps/place/Joe%27s+Pizza+Broadway/data=!4m7!3m6!1s0x89c259ab3c1ef289:0x3b67a41175949f55!8m2!3d40.7546795!4d-73.9870291!16s%2Fg%2F11bw4ws2mt!19sChIJifIePKtZwokRVZ-UdRGkZzs?authuser=0&hl=en&rclk=1": [
        {
            "reviewer_name": "deinz abella",
            "review_text": "Visited twice in this location, there was a line both times but the wait wasn’t bad at all. Moves fast. I got the cheese pizza, I guess it’s the classic must try, that was good. I came back for the got their version of supreme pizza with …",
            "rating": 5,
            "review_date": "a week ago",
            "photo_links": [
                "https://lh5.googleusercontent.com/p/AF1QipP7ZIsdTKiFrN1hORnKIR6q0A9wJNPdn6ID72Xp=w375-h281-p-k-no",
                "https://lh5.googleusercontent.com/p/AF1QipPTdVki6E1lkviWA9LeaUjGeKmi6S_-paRkaklc=w375-h281-p-k-no",
                "https://lh5.googleusercontent.com/p/AF1QipO_C3OqQqspqiucL7sVNZJPaoa65GgjZzPzoI9_=w375-h281-p-k-no",
                "https://lh5.googleusercontent.com/p/AF1QipP6nbHmt3lcezjCAgPxR2C8KLZvYVtATd67m7aX=w375-h281-p-k-no"
            ]
        },
…
}

The API route below skips the browser, the selectors and the consent screen entirely.

Method 2. Scrape Google Maps Reviews Using an API

The second method is simpler because it uses a ready-made API to fetch all the required data based on specified filters. All we have to do is send a request to the Google Maps Reviews API, retrieve the data, and save it in the format we want.

To run the code examples from this section, you’ll need to provide your personal HasData API key. You can get one for free by signing up on our website.

Scrape Reviews by PlaceID&DataID

If you want to extract reviews for a specific place, this example is exactly what you need. Let me walk you through it step by step. First, we’ll start with the full code and then break it down below:

import requests
import csv


API_KEY = 'YOUR-API-KEY'


identifier = "dataId"  # Change to "placeId" if needed
identifier_value = "0x80cc0654bd27e08d%3A0xb1c2554442d42e8d"


url = f"https://api.hasdata.com/scrape/google-maps/reviews?{identifier}={identifier_value}"


headers = {
    'Content-Type': 'application/json',
    'x-api-key': API_KEY
}


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


if response.status_code == 200:
    data = response.json()


    with open('reviews.csv', mode='w', newline='', encoding='utf-8') as file:
        writer = csv.writer(file)
        writer.writerow(['User Name', 'User Link', 'Rating', 'Date', 'Snippet', 'Review Link', 'Likes'])


        for review in data.get('reviews', []):
            writer.writerow([
                review['user'].get('name', 'N/A'),
                review['user'].get('link', 'N/A'),
                review.get('rating', 'N/A'),
                review.get('date', 'N/A'),
                review.get('snippet', 'N/A'),
                review.get('link', 'N/A'),
                review.get('likes', 'N/A')
            ])
else:
    print(f"Error. Status Code: {response.status_code}")

Now, let’s break it down so you can see how everything fits together. We’ll start by importing the necessary libraries to handle API requests and save the data into a file:

import requests
import csv

Next, we’ll create variables to store HasData’s API key and the type of identifier we’re working with. The identifier could be either a placeId or a dataId, depending on how the location is defined:

API_KEY = 'YOUR-API-KEY'


identifier = "dataId"  # Change to "placeId" if needed
identifier_value = "0x80cc0654bd27e08d%3A0xb1c2554442d42e8d"

Then, assemble the URL for the API request. We’ll also set up headers for authentication and make the request:

url = f"https://api.hasdata.com/scrape/google-maps/reviews?{identifier}={identifier_value}"


headers = {
    'Content-Type': 'application/json',
    'x-api-key': API_KEY
}


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

Finally, we process the response. If the request is successful, we save the data to a CSV file. If it fails, we’ll print an error message so you can figure out what went wrong:

if response.status_code == 200:
    data = response.json()


    with open('reviews.csv', mode='w', newline='', encoding='utf-8') as file:
        writer = csv.writer(file)
        writer.writerow(['User Name', 'User Link', 'Rating', 'Date', 'Snippet', 'Review Link', 'Likes'])


        for review in data.get('reviews', []):
            writer.writerow([
                review['user'].get('name', 'N/A'),
                review['user'].get('link', 'N/A'),
                review.get('rating', 'N/A'),
                review.get('date', 'N/A'),
                review.get('snippet', 'N/A'),
                review.get('link', 'N/A'),
                review.get('likes', 'N/A')
            ])
else:
    print(f"Error. Status Code: {response.status_code}")

You can also change this step to customize the data you’re saving or add some preprocessing before writing it to the file. Here’s what the final dataset looks like:

CSV file with Google Maps reviews collected through the Reviews API, one row per review with user, rating, date and snippet columns

In this example, we’re only fetching one page of reviews. If you want more, you’ll need to include the next page token in your API requests. This token is returned in the API response, so you can use it to get the next batch of reviews. The first page carries 8 reviews and the pages after it 10 each.

A review record carries these fields, and the no-code scraper returns the same set plus the owner’s response and the place URL:

FieldWhat it holds
ratingthe star rating as a number
snippetthe review text
date / isoDatethe relative date Google shows, and a machine-readable timestamp
link / reviewIda direct URL and a stable id for the single review
likeshow many people marked the review helpful
imagesphoto URLs attached to the review
sourcewhere the review was posted
user.name / user.linkthe reviewer and their profile
user.reviews / user.photoshow prolific the reviewer is
user.localGuidewhether Google marks them a Local Guide

A DIY scraper reads the visible subset of this (name, rating, date, text, photos). The likes, stable ids, timestamps, and reviewer stats are the part the rendered page gives up less readily, and here they arrive in the JSON without extra work.

Scrape Reviews from Search Results

You can use the following example to scrape reviews from all the Google Maps places found on the map based on a specific query. If you’re looking for a ready-to-go solution, here’s the final Python code:

import requests
import csv


API_KEY = 'YOUR-API-KEY'
keyword = "Pizza"
output_file = "places_reviews.csv"


def get_places(keyword):
    url = f"https://api.hasdata.com/scrape/google-maps/search?q={keyword}"
    headers = {'Content-Type': 'application/json', 'x-api-key': API_KEY}
    response = requests.get(url, headers=headers)
    if response.status_code == 200:
        return response.json().get('localResults', [])
    else:
        print(f"Error fetching places: {response.status_code}")
        return []


def get_reviews(data_id):
    url = f"https://api.hasdata.com/scrape/google-maps/reviews?dataId={data_id}"
    headers = {'Content-Type': 'application/json', 'x-api-key': API_KEY}
    response = requests.get(url, headers=headers)
    if response.status_code == 200:
        return response.json().get('reviews', [])
    else:
        print(f"Error fetching all the reviews for {data_id}: {response.status_code}")
        return []


def collect_data(keyword, output_file):
    places = get_places(keyword)


    with open(output_file, mode='w', newline='', encoding='utf-8') as file:
        writer = csv.writer(file)
        writer.writerow([
            'Place Name', 'Place ID', 'Total Reviews', 'Rating', 'User Name', 'User Link',
            'User Rating', 'Review Date', 'Review Snippet', 'Review Likes'
        ])


        for place in places:
            if place['reviews'] > 0:
                place_name = place.get('title', 'N/A')
                place_id = place.get('placeId', 'N/A')
                data_id = place.get('dataId', 'N/A')
                total_reviews = place.get('reviews', 0)
                rating = place.get('rating', 'N/A')


                reviews = get_reviews(data_id)


                for review in reviews:
                    writer.writerow([
                        place_name,
                        place_id,
                        total_reviews,
                        rating,
                        review['user'].get('name', 'N/A'),
                        review['user'].get('link', 'N/A'),
                        review.get('rating', 'N/A'),
                        review.get('date', 'N/A'),
                        review.get('snippet', 'N/A'),
                        review.get('likes', 'N/A')
                    ])


if __name__ == "__main__":
    collect_data(keyword, output_file)
    print(f"Data collection completed. Results saved to {output_file}")

This version extracts the code for calling the Google Maps Reviews API from the previous example into a separate function, get_reviews(), and moved the logic for saving the data into collect_data(). The main change is that we added a call to the Google Maps API to get a list of places, along with their IDs and the number of reviews:

def get_places(keyword):
    url = f"https://api.hasdata.com/scrape/google-maps/search?q={keyword}"
    headers = {'Content-Type': 'application/json', 'x-api-key': API_KEY}
    response = requests.get(url, headers=headers)
    if response.status_code == 200:
        return response.json().get('localResults', [])
    else:
        print(f"Error fetching places: {response.status_code}")
        return []

A filter in the collect_data() function skips places with zero reviews, so we only scrape reviews for places with one or more reviews.

The result is a file that looks like this:

CSV file combining place details and reviews for every place found by the search query

As you can see, this script can still be improved further to scrape even more reviews for each place.

Method 3. Scrape Google Maps Reviews Without Code

This method doesn’t require any programming skills. All you need to do is enter the data you’re interested in to collect reviews. To get started, head over to your account on our website, go to the “No-Code Scrapers” section, and find the Google Maps Reviews Scraper:

The Google Maps Reviews scraper in the no-code scrapers section of the HasData dashboard

On the scraper page, specify the number of reviews you want to collect, provide the link to the Google Maps location, and, if you’d like, choose a sorting option:

The scraper form with the reviews limit, the Google Maps place link, and the sorting option

Run the scraper and wait for it to finish. The download button then offers CSV, XLSX and JSON, and the reviews come out with all their detail:

[
    {
        "date": "2 weeks ago",
        "isoDate": "2024-12-09T03:02:46.087Z",
        "rating": 5,
        "snippet": "Wow, what an incredible experience! The views are absolutely stunning – pictures don’t do it justice. There are plenty of spots to stop for photos while driving along the rim, the views are gorgeous from everywhere. We stayed in the village for one night and it was very convenient to be close to the most popular viewpoints. The visitor center has helpful info, and the staff is super friendly. We didn’t get a chance to hike but I would highly recommend it if you have more time. Definitely a must-see if you are driving through.",
        "source": "Google",
        "responseDate": "",
        "responseIsoDate": "",
        "responseSnippet": "",
        "likes": 0,
        "url": "https://www.google.com/maps/reviews/data=!4m8!14m7!1m6!2m5!1sChdDSUhNMG9nS0VJQ0FnSUN2dlBlWDNBRRAB!2m1!1s0x0:0xb1c2554442d42e8d!3m1!1s2@1:CIHM0ogKEICAgICvvPeX3AE%7CCgsI1rvZugYQwM7QKQ%7C?hl=en-US",
        "userName": "Daria Kurovskaya",
        "userProfileLink": "https://www.google.com/maps/contrib/111542442643050280292?hl=en-US",
        "userPhotos": 81,
        "userReviews": 62,
        "userThumbnail": "https://lh3.googleusercontent.com/a-/ALV-UjVkjExshA-NJpU3ywgYK3vh7jqxGyqIpRKmADeQGnh5M9lxwdbj=s120-c-rp-mo-ba4-br100",
        "isUserLocalGuide": true,
        "placeUrl": "https://www.google.com/maps/place/Grand+Canyon/@36.099796,-112.1299942,14z/data=!3m1!4b1!4m5!3m4!1s0x80cc0654bd27e08d:0xb1c2554442d42e8d!8m2!3d36.0997631!4d-112.1124846",
        "dataId": "0x80cc0654bd27e08d:0xb1c2554442d42e8d",
        "images": "https://lh5.googleusercontent.com/p/AF1QipM2bJSxbQJyU-XLVXJWfSWanMSJXcM-V2UwPwOY=w150-h150-k-no-p, https://lh5.googleusercontent.com/p/AF1QipMFc1tlexXIM00JTqwbTrb_De2W70Zt78aBYBOb=w150-h150-k-no-p, https://lh5.googleusercontent.com/p/AF1QipN5EAw2tQs72IOSK3lAVV7pB9gMRStwhiQJoOpB=w150-h150-k-no-p, https://lh5.googleusercontent.com/p/AF1QipMR-oYTe4qj0nuYN8_9QawxqldIthbyIJCRgaq7=w150-h150-k-no-p, https://lh5.googleusercontent.com/p/AF1QipPR9QnJQsi_wCbTmL-DzDZfYGfqtY-XpJlYn3gS=w150-h150-k-no-p"
    },
…
]

Of the three methods, this one asks the least of you. The form takes the place link and a review count, and the download button does the rest.

What 1,000 Reviews Cost

Vendors count different things, so each price here comes with its billing unit:

RoutePer 1,000 reviewsBilling unit
HasData no-code scraper$0.30 on the entry plan, down to $0.083 on the largest1 credit per review
HasData Reviews APIabout $0.15 entry, $0.04 largest5 credits per request, with 8 reviews on the first page and 10 per page after
Outscraperfirst 500 free, then $3.00, dropping to $1.00 past 100,000per review
Apify (Compass scraper)from $0.30per scraped review
SerpApi$1.25 to $2.50 on its $25 per 1,000 searches planper search, and num defaults to 10 reviews with 20 the maximum
Your own Selenium scriptno vendor feeyour proxies, retries, and maintenance time

Read the per-review and per-search rows separately, since one search returns a page of reviews rather than a single one.

Conclusion

The three routes differ in what they ask of you rather than in what they return. Your own Selenium script costs no vendor fee and costs you the proxies, the retries and every selector change Google ships. The Reviews API takes the same place_id and returns parsed JSON, so the code is a request and a loop over pages. The no-code scraper takes a link and a review count in a form and hands back a file.

Pick by where you want the maintenance to sit. If it belongs in your codebase, write the script. If it belongs with a vendor, the API and the no-code scraper differ only in whether you want the data in a program or in a spreadsheet.

Valentina Skakun
Valentina Skakun
Valentina is a software engineer who builds data extraction tools before writing about them. With a strong background in Python, she also leverages her experience in JavaScript, PHP, R, and Ruby to reverse-engineer complex web architectures.If data renders in a browser, she will find a way to script its extraction.
Articles

Might Be Interesting