This article is about scraping Google Maps reviews, and it covers three ways to do it:
- Writing your own Python script. A step-by-step Selenium scraper, with ready-to-run code you can adapt.
- Using a Google Maps Reviews API. One request per page of reviews, structured JSON back, and the proxy pool and rendering on our side.
- 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 + Selenium | Reviews API | No-code scraper | |
|---|---|---|---|
| You maintain | Chrome, selectors, IPs, consent screens | the code that calls one endpoint | nothing |
| When Google changes markup | your script breaks | the schema stays | the schema stays |
| Pagination | scrolling you write | nextPageToken | a reviews-count field |
| Cost per 1,000 reviews | your time and proxies | about $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 timeNext, 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:
| Data | CSS Selector |
|---|---|
| Reviewer Name | div.jftiEf .d4r55 |
| Review Text | div.jftiEf .wiI7pd |
| Rating | div.jftiEf span.kvMYJc (its aria-label) |
| Review Date | div.jftiEf .rsqaWe |
| Photo Links | div.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_reviewsFinally, 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 csvNext, 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:

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:
| Field | What it holds |
|---|---|
rating | the star rating as a number |
snippet | the review text |
date / isoDate | the relative date Google shows, and a machine-readable timestamp |
link / reviewId | a direct URL and a stable id for the single review |
likes | how many people marked the review helpful |
images | photo URLs attached to the review |
source | where the review was posted |
user.name / user.link | the reviewer and their profile |
user.reviews / user.photos | how prolific the reviewer is |
user.localGuide | whether 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:

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:

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:

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:
| Route | Per 1,000 reviews | Billing unit |
|---|---|---|
| HasData no-code scraper | $0.30 on the entry plan, down to $0.083 on the largest | 1 credit per review |
| HasData Reviews API | about $0.15 entry, $0.04 largest | 5 credits per request, with 8 reviews on the first page and 10 per page after |
| Outscraper | first 500 free, then $3.00, dropping to $1.00 past 100,000 | per review |
| Apify (Compass scraper) | from $0.30 | per scraped review |
| SerpApi | $1.25 to $2.50 on its $25 per 1,000 searches plan | per search, and num defaults to 10 reviews with 20 the maximum |
| Your own Selenium script | no vendor fee | your 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.


