← Back to blog

Published

Scrape Google Maps, Yelp, and Tripadvisor Reviews with Python

Reviews for the same business look different depending on where you read them. Each platform draws a different crowd: locals checking a map, diners choosing where to eat, travelers planning a trip. If you want to know what customers actually say, whether about your own locations, a competitor, or a list of acquisition targets, you need all three in one place.

Scraping each site yourself means three sets of selectors, three pagination schemes, and three sets of anti-bot defenses to keep working. This guide takes a different path: one Python script, built on the official PrismCrawl Python SDK, that finds a business on each platform, pulls its reviews concurrently, and writes them into a single CSV you can analyze with pandas.

What you'll build

The script does four things:

  1. Finds the listing on Google Maps, Yelp, and Tripadvisor from a business name and city.
  2. Pages through the reviews on each platform, with a hard cap on how many pages it fetches.
  3. Normalizes every review into the same columns: source, rating, rating scale, date, author, and text.
  4. Writes one CSV, which the last section analyzes with pandas.

It runs the three platforms concurrently, so the whole job takes about as long as the slowest source.

Setup

Install the SDK and pandas. The SDK requires Python 3.9 or later.

pip install prismcrawl pandas
export PRISMCRAWL_API_KEY="your-api-key"

Create a free PrismCrawl account to get an API key. New accounts get 100 free credits, which is enough to run this script several times. The client reads PRISMCRAWL_API_KEY from the environment, so the key never appears in your code.

Start small: one place, one platform

Before combining sources, here is the core pattern on its own. Search Google Maps for the business, take its place_id, and page through its reviews:

from prismcrawl import PrismCrawl

client = PrismCrawl()

places = client.google.maps.search(
    query="Halcyon Austin, TX",
    coordinates={"latitude": 30.2672, "longitude": -97.7431},
)
place = places["data"]["content"]["results"][0]
print(place["title"], place["rating"], place["reviews"])

for page in client.google.maps.reviews.pages(id=place["place_id"], limit=20, max_pages=3):
    for review in page["data"]["content"]["results"]:
        print(review.get("rating"), (review.get("text") or "")[:80])

.pages() handles pagination for you. It sends the first request, reads next_page_token from the response, and keeps going until has_next_page is false or it reaches max_pages. Each page is a separate request that uses one credit, so max_pages doubles as a spending limit.

The same pattern works for Yelp and Tripadvisor. The only difference is which field from the search result you pass as id:

PlatformSearch endpointField to pass as idReviews endpoint
Google Mapsclient.google.maps.searchplace_idclient.google.maps.reviews
Yelpclient.yelp.searchid (the business alias, such as halcyon-austin-2)client.yelp.reviews
Tripadvisorclient.tripadvisor.searchid (the canonical place URL)client.tripadvisor.reviews

Each platform name links to that reviews API's page, which lists every parameter and response field. Finding the place in the first step uses the Google Maps API, Yelp Search, or Tripadvisor Search.

Yelp pages by page number and the others use continuation tokens, but .pages() handles both, so your code doesn't have to care.

The full script

Here is the complete version. It uses AsyncPrismCrawl, which has the same interface as the sync client with await in front of each call, so the three platforms run at the same time.

import asyncio
from typing import Any, Dict, List, Optional

import pandas as pd
from prismcrawl import AsyncPrismCrawl
from prismcrawl.types import Coordinates

BUSINESS = "Halcyon"
CITY = "Austin, TX"
CITY_CENTER: Coordinates = {"latitude": 30.2672, "longitude": -97.7431}
MAX_PAGES = 5  # per source; every page is one credit


def first_result(response: Dict[str, Any]) -> Optional[Dict[str, Any]]:
    data = response["data"]
    if data["format"] != "json" or not data["content"]["results"]:
        return None
    return data["content"]["results"][0]


def normalize(source: str, review: Dict[str, Any]) -> Dict[str, Any]:
    author = review.get("author")
    if isinstance(author, dict):
        author = author.get("name")
    return {
        "source": source,
        "rating": review.get("rating"),
        "rating_max": review.get("rating_max") or 5,
        "date": review.get("published_at") or review.get("date") or review.get("date_text"),
        "author": author,
        "text": (review.get("text") or "").strip(),
        "is_excerpt": review.get("is_excerpt", False),
        "review_id": review.get("id"),
    }


async def find_listings(client: AsyncPrismCrawl) -> Dict[str, Optional[str]]:
    google, yelp, tripadvisor = await asyncio.gather(
        client.google.maps.search(query=f"{BUSINESS} {CITY}", coordinates=CITY_CENTER),
        client.yelp.search(query=BUSINESS, location=CITY),
        client.tripadvisor.search(query=BUSINESS, location=CITY),
    )
    listings: Dict[str, Optional[str]] = {}
    for source, response, id_field in [
        ("google", google, "place_id"),
        ("yelp", yelp, "id"),
        ("tripadvisor", tripadvisor, "id"),
    ]:
        match = first_result(response)
        listings[source] = match.get(id_field) if match else None
        name = (match.get("title") or match.get("name")) if match else None
        print(f"{source:>12}: {name or 'not found'}")
    return listings


async def collect(endpoint: Any, source: str, **params: Any) -> List[Dict[str, Any]]:
    rows: List[Dict[str, Any]] = []
    async for page in endpoint.pages(**params, max_pages=MAX_PAGES):
        if page["data"]["format"] != "json":
            break
        rows.extend(normalize(source, review) for review in page["data"]["content"]["results"])
    return rows


async def main() -> None:
    async with AsyncPrismCrawl() as client:
        listings = await find_listings(client)

        jobs = {}
        if listings["google"]:
            jobs["google"] = collect(client.google.maps.reviews, "google", id=listings["google"], limit=20)
        if listings["yelp"]:
            jobs["yelp"] = collect(client.yelp.reviews, "yelp", id=listings["yelp"], sort_by="newest")
        if listings["tripadvisor"]:
            jobs["tripadvisor"] = collect(
                client.tripadvisor.reviews, "tripadvisor", id=listings["tripadvisor"], sort_by="newest"
            )

        outcomes = await asyncio.gather(*jobs.values(), return_exceptions=True)

    reviews: List[Dict[str, Any]] = []
    for source, outcome in zip(jobs, outcomes):
        if isinstance(outcome, BaseException):
            print(f"{source}: failed with {outcome!r}")
        else:
            print(f"{source}: {len(outcome)} reviews")
            reviews.extend(outcome)

    pd.DataFrame(reviews).to_csv("reviews.csv", index=False)


asyncio.run(main())

A few decisions in this script are worth explaining.

The listing lookup prints what it matched. first_result trusts the top search result. That's usually right for a specific name plus a city, but a common name like "Joe's Pizza" can match the wrong location. The script prints each match so you can check it before relying on the data. For a production pipeline, compare the address or coordinates across platforms, or store the confirmed IDs once and skip the search step on later runs.

One platform failing doesn't lose the others. asyncio.gather(..., return_exceptions=True) returns an exception in place of a result instead of cancelling everything, so a Tripadvisor error still leaves you with the Google and Yelp reviews. You don't need your own retry loop: the SDK already retries rate limits, server errors, and connection failures with exponential backoff.

normalize flattens the differences between platforms. Author is sometimes a string and sometimes an object, and the date can arrive as published_at, date, or a display string like "2 weeks ago" in date_text. The function picks the best available value so every row has the same shape.

is_excerpt is kept, not dropped. Some platforms only expose part of a review's text in certain cases. PrismCrawl marks those rows with is_excerpt, which matters when you analyze wording: a truncated review can hide the complaint that comes after the first sentence.

Analyze the reviews with pandas

With everything in one CSV, comparing platforms takes a few lines:

import pandas as pd

df = pd.read_csv("reviews.csv")
df["stars"] = df["rating"] / df["rating_max"] * 5  # put every source on a 5-point scale

print(df.groupby("source")["stars"].agg(["count", "mean"]).round(2))
print(df.pivot_table(index="stars", columns="source", values="text", aggfunc="count", fill_value=0))

STOPWORDS = {"that", "this", "with", "have", "were", "they", "their", "there", "just", "from",
             "what", "when", "very", "been", "would", "about", "which", "only", "also", "here"}
low = df[(df["stars"] <= 2) & ~df["is_excerpt"]]
words = low["text"].str.lower().str.findall(r"[a-z']{4,}").explode()
print(words[~words.isin(STOPWORDS)].value_counts().head(15))

The first table shows the review count and average rating per platform. Averages can differ noticeably between sites, which is one reason looking at a single platform can mislead you. The pivot table shows the rating distribution, where a platform with many 1-star and 5-star reviews tells a different story than one with mostly 3s and 4s. The last block lists the most common words in full-text reviews of two stars or fewer: a crude but fast way to see whether complaints are about service, price, wait times, or something else.

From here, the same DataFrame can feed a sentiment model, an LLM summary, or a weekly report. The structured data extraction guide covers turning the review text itself into structured fields.

What it costs

Every successful request uses one credit, failed requests are free, and each page of reviews counts as its own request. One run of the script above uses at most:

  • 3 credits for the three listing lookups
  • 15 credits for five pages of reviews from each platform

That is 18 credits per business. To track 100 locations weekly, you'd use about 7,800 credits a month. Credit packages start at $5 with no subscription; see pricing for current rates. Lower MAX_PAGES if you only need recent reviews: with sort_by="newest" on Yelp and Tripadvisor, the first pages hold the most recent reviews.

Things to watch for

  • Keep parameters fixed while paginating. Continuation tokens are tied to the original request's parameters and expire after an hour. .pages() resends the same parameters for you, but if you paginate by hand, don't change limit, sort_by, or language between pages.
  • Google may limit access to some places' reviews. When that happens, the response marks it with access_limited and pagination_limited in search_parameters rather than returning a partial list without saying so. Check those fields if a place returns fewer reviews than its listed count.
  • Store IDs, not names. A business's place_id, Yelp alias, and Tripadvisor URL are stable. Once you've confirmed the right listing, save the IDs and skip the lookup step on future runs. That saves three credits per business and avoids matching the wrong location.
  • Know how you'll use the data. Collecting public reviews for analysis is common, but republishing review text has different considerations. Our web scraping legality guide covers the general landscape.

Frequently asked questions

How do I get all Google Maps reviews for a place in Python?

Search Google Maps to get the place's place_id, then call the reviews endpoint and follow next_page_token until has_next_page is false. With the SDK, that loop is client.google.maps.reviews.pages(id=place_id). Set max_pages to cap the number of requests.

How much does it cost to collect reviews with PrismCrawl?

Each successful request uses one credit, and each page of reviews is a separate request. Failed requests are free. Collecting five pages from each of three platforms, plus three listing lookups, uses 18 credits.

Why do ratings from different review sites need normalizing?

Platforms can use different rating scales, author formats, and date fields. PrismCrawl reports the scale in rating_max when the source provides one, so dividing rating by rating_max puts every source on the same scale before you compare averages.

Can I use the same approach for app store reviews?

Yes. The SDK has the same .pages() pattern for Google Play and Apple App Store reviews: client.google.play.reviews and client.apple.app_store.reviews. See the SDK README for every endpoint, or the API reference for their parameters.