How to Scrape Hotel Reviews with Python Across Booking Sites (and When to Stop)
A real tutorial to scrape hotel reviews across booking sites: finding where reviews load, the headless fallback, one schema for every platform's score scale and fields, the incremental pattern, and the honest point where an API wins.
Table of contents

You need to scrape hotel reviews because the analysis you're planning (topic modeling, a sentiment pipeline, a competitor benchmark) needs thousands of them, from several booking sites, and none of those sites offers a public reviews API.
Our hotel reviews API explainer covers why; this post covers how.
This is a real tutorial.
You'll find where reviews load, pull them with requests, fall back to a headless browser when that fails, and, the part every other tutorial skips, fold five platforms' different scales and fields into one schema you can analyze.
We sell the alternative (a data API), and this tutorial still teaches the real thing.
The build-vs-buy decision only makes sense once you know what the build costs.
Key Takeaways
- Reviews load after the page does. They arrive through internal calls and "load more" requests, so the first skill is finding that request in DevTools.
- The hardest sites block the cheap path. A plain script request this week got a challenge page from Booking.com and a 403 from TripAdvisor; a headless browser is the working fallback, at 10 to 50x the cost.
- Every platform scores differently. Booking.com uses 1 to 10 with positive and negative text split, TripAdvisor 1 to 5 with machine-translated text, Expedia 10-point labels. One schema or your analysis lies.
- Newest-first plus a stop rule makes collection incremental. Sort by date, page until you hit a review you've stored, and never re-pull a hotel's full history.
- Scraping is a fine teacher; production wants an API. If the reviews feed a decision someone is paid to make, buy the pipe and keep the normalization code.
Before You Scrape: Ground Rules and Targets

A few practical rules keep the scraper clean.
Throttle hard (one request every few seconds), don't scrape anything behind a login, and don't collect reviewer profiles: the review text, score, date, and trip type are the data, and the person who wrote it isn't.
This post stays in one lane: review text and ratings for specific hotels.
The method for prices is in our hotel price scraping tutorial, and the two share DevTools inspection and the headless fallback, so read that one if you've never opened the Network tab.
Know your targets, because difficulty varies more for reviews than for prices.
| Target | Difficulty | What Happened This Week | What You're Up Against |
|---|---|---|---|
| Google (Travel and Maps) | Moderate | Reviews render after load, no block on a first request | JavaScript rendering, obfuscated markup, "more reviews" pagination |
| Expedia | Hard | Rendered application, challenged at volume | Bot detection, per-page review loading |
| TripAdvisor | Hard | A plain GET returned HTTP 403 | Blocking on the first request; language filters change which reviews you see |
| Booking.com | Hardest | A plain GET returned HTTP 202 with a 4 KB challenge page and no data | The strongest anti-automation stack in the vertical; our Booking scraper guide covers the details |
The tutorial below uses Google as the worked target, because it's the least hostile major source of hotel reviews and the one people most often want to scrape with Python.
The method transfers; only the pain level changes.
The Tutorial: Scrape Hotel Reviews with Python

Step 1: Find where the reviews actually load
Open a hotel's page on Google, scroll to the reviews, and open DevTools → Network → filter by Fetch/XHR.
Click "more reviews" and watch the request that fires.
That request is your target.
Its response carries the review text, and its parameters carry the pagination token that fetches the next batch, which is the mechanism every review surface uses regardless of markup.
Learn the inspection method rather than any specific endpoint.
Endpoints churn constantly; the method survives every redesign.
Step 2: The requests-first attempt
If the request you found works outside a browser session, requests is the cheap path.
import requests
headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36",
"Accept-Language": "en-US,en;q=0.9",
}
# Paste the reviews request you found in DevTools. It usually carries
# the hotel identifier and a pagination token that expires.
url = "https://www.google.com/...reviews-request-from-devtools..."
resp = requests.get(url, headers=headers, timeout=15)
print(resp.status_code, len(resp.text))
When it works, you get a batch of reviews for one HTTP call.
The failure modes: a 403 (TripAdvisor gave one this week on the first request), a challenge page with a 2xx status (Booking.com's 202), or a token that only works inside the session that minted it.
Any of those means the lightweight path is closed, and you escalate.
Step 3: The headless fallback
A headless browser renders the page and clicks "more" like a person would.
import time
from playwright.sync_api import sync_playwright
def collect_reviews(hotel_url, max_pages=5):
reviews = []
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto(hotel_url, wait_until="networkidle")
for _ in range(max_pages):
# Class names are obfuscated and churn weekly.
# Accessibility roles churn far less, so anchor on those.
cards = page.get_by_role("article")
for i in range(cards.count()):
reviews.append(cards.nth(i).inner_text())
more = page.get_by_role("button", name="More reviews")
if more.count() == 0:
break
more.first.click()
time.sleep(3) # throttle between batches, always
browser.close()
return reviews
About 25 lines, and it will genuinely return review text.
The cost: a full browser per hotel is 10 to 50x slower than requests, and it eats RAM accordingly.
At 5 hotels that's irrelevant; at 500 it's an infrastructure project.
Step 4: One schema for every platform
This step decides whether your data can be analyzed, and it's where multi-platform review scrapers quietly lie.
| Platform | Score Scale | Text Fields | Date Format | Trap |
|---|---|---|---|---|
| Booking.com | 1 to 10 | positive and negative, separately |
2026-09-09 10:34:57 |
Two fields per review; a "6.0" is mixed, not bad |
| TripAdvisor | 1 to 5 | One text, machine-translated to English when you request all languages |
2024-01-10 |
Original-language flag travels with the review |
| Expedia | 10-point label ("8/10 Good") | One text |
Sep 3, 2026 |
The score is a string until you parse it |
| 1 to 5 stars | One text |
Relative ("a week ago") | Relative dates need the scrape date to resolve |
Write the normalizer before you collect a single review:
import re
from datetime import date
def normalize(platform, raw):
"""Map one platform's review dict onto a shared schema."""
if platform == "booking":
score10 = float(raw["score"])
text = " ".join(t for t in (raw.get("positive"), raw.get("negative")) if t)
elif platform == "tripadvisor":
score10 = float(raw["rating"]) * 2
text = raw["text"]
elif platform == "expedia":
score10 = float(re.match(r"(\d+)/10", raw["rating"]).group(1))
text = raw["text"]
else:
raise ValueError(platform)
return {
"platform": platform,
"review_id": str(raw["id"]),
"score_10": score10,
"text": text,
"positive": raw.get("positive"), # Booking.com only
"negative": raw.get("negative"), # Booking.com only
"date": raw["date"],
"language": raw.get("language"),
"scraped_on": date.today().isoformat(),
}
The judgment calls around it are the real work: whether a TripAdvisor 4 equals a Booking.com 8 (it doesn't, and the analysis should stay per platform where it matters), whether to keep translated text or originals, and how to resolve "a week ago" against the day you scraped.
Decide once, write it down, and every platform lands in the same table.
Step 5: Dedupe and go incremental
Never re-pull a hotel's whole review history.
Sort newest first, page until the first review you've already stored, and stop.
def newest_unseen(reviews_newest_first, seen_ids):
fresh = []
for r in reviews_newest_first:
if r["review_id"] in seen_ids:
break # everything after this is already stored
fresh.append(r)
return fresh
Key the store on (platform, review_id), because IDs collide across platforms and never within one.
A hotel with 3,600 reviews then costs one page a day rather than 150.
The Wall: What Breaks and What It Costs

Everything above works on the day you build it.
The question that decides build-vs-buy is what happens over the following 6 months.
| What Breaks | How Often | What It Looks Like |
|---|---|---|
| Challenge pages and blocks | Continuous, worst on Booking.com and TripAdvisor | 202s with no data, 403s, bans spreading through your proxy pool |
| Markup and role changes | Weekly to monthly, per platform | Cards return empty, "more" button never found |
| Pagination loops | Quietly, after a token format changes | The same batch collected forever, or page 2 skipped |
| Translated text mixed with originals | Whenever a language filter defaults differently | Sentiment scores drift with no code change |
| Score-scale mix-ups | Once, at the join | A 4.5-star hotel "outscored" by a 6.0 Booking.com average |
The last two are silent corruption, the worst failure mode on the list.
A crashed scraper gets fixed; a scraper that stores translated text as original, or joins a 5-point scale to a 10-point one, feeds wrong conclusions into real decisions for weeks.
Now the math, stated as the estimate it is.
A four-platform hotel review scraper at portfolio scale realistically consumes 5 to 10 engineer hours a week once challenge pages, markup churn, and proxy babysitting are counted, which lands at $60,000 to $100,000+ a year in developer time and infrastructure; our hotel data API guide breaks that figure down line by line.
The decision rule is simple.
If the reviews feed a decision someone is paid to make (reputation, pricing, a product your customers see), buy the pipe.
If it's a thesis, a one-off study, or a weekend project, scraping is a fine teacher, and you just learned it.
The Graduation Path: Same Reviews, No Scraper

Hotel data APIs sell the part of this tutorial you'd rather not own: the rendering, the challenge pages, the pagination, and the per-platform quirks, maintained by someone whose product depends on it.
StayAPI is one of them, with reviews endpoints for Booking.com, TripAdvisor, Expedia, Google, Agoda, Airbnb, Vrbo, Trip.com, and several chain sites, each keyed by that platform's hotel ID.
The incremental pattern from step 5 is built in.
Here's StayAPI's Booking.com reviews endpoint, newest first, for the Lisbon hotel from our other guides:
curl -X GET "https://api.stayapi.com/v1/booking/hotel/reviews?hotel_id=4045490&sort=recent_desc&per_page=25&page=1" \
-H "x-api-key: YOUR_API_KEY"
The response (truncated to one review):
{
"success": true,
"hotel_id": "4045490",
"data": {
"reviews_returned": 25,
"reviews": [
{
"id": "5339751279",
"score": 6.0,
"reviewed_date": "2026-09-09 10:34:57",
"guest": { "country_code": "gb", "traveler_type": "Solo traveler" },
"stay_details": { "room_type": "Deluxe Twin Room", "nights": 1 },
"review": {
"title": "Pleasant",
"positive": "Excellent location. Breakfast was exceptionally poor. Staff were all extremely friendly and helpful.",
"negative": "Coffee and juice at breakfast were terrible.",
"language": "en-gb"
},
"travel_purpose": "business",
"partner_reply": null
},
...
]
}
}
| Field | What It Replaces in the Scraper |
|---|---|
sort=recent_desc |
Your newest-first ordering and the stop rule |
review.positive / review.negative |
Parsing Booking.com's split text out of the markup |
review.language |
Guessing whether text was translated |
guest.traveler_type, travel_purpose |
Regexing trip-type labels off the card |
reviewed_date |
Resolving relative dates against the scrape date |
The TripAdvisor endpoint returns 1-to-5 ratings with the original language code beside machine-translated text and up to 50 reviews per page, and the Expedia endpoint returns its 10-point labels sortable newest first, so the normalizer from step 4 still runs.
That's the honest split: the API owns the fetch, and you still own the schema.
Our Booking.com reviews API guide and Google reviews API guide cover those two sources in depth, and our hotel review sentiment analysis guide covers what to do with the table once it exists.
Getting Started
- Run the tutorial once on a single Google hotel page, and keep the normalizer and the incremental function; they survive the move to an API unchanged.
- Preview the review payload for any Booking.com property with the free Booking.com reviews tool, no code needed.
- When the reviews feed a real decision, sign up at stayapi.com. Free tier, no credit card.
- Swap the fetch step for StayAPI's reviews endpoints, keep
(platform, review_id)as your key, and schedule the newest-first pull daily. - Pipe the normalized table into your analysis stack, Google Sheets, Power BI, or no-code tools like N8N and Make.com. For review monitoring across a portfolio and every major booking site, book a demo call.
FAQ
How can I scrape hotel reviews from multiple platforms?
Find each platform's reviews request in DevTools, use a headless browser where the direct request is blocked, and normalize every platform into one schema (score converted to a shared scale, text fields, date, language) before storing.
The normalization is the hard part; the fetching is the part a data API can replace.
What are the best tools for scraping hotel reviews?
Three categories: per-run scraper tools for one-off datasets, DIY Python for learning and custom fields, and data APIs for anything recurring.
Our hotel data scraper comparison covers the tools by job.
Can I scrape Booking.com reviews with Python?
Yes, with a headless browser, because a plain request returns a challenge page rather than reviews.
Expect the most maintenance of any platform; the reviews endpoint is the recurring-use route.
How do I scrape Google hotel reviews?
Google renders reviews after page load and paginates them with a "more reviews" request, which makes it the most workable target for the headless method above.
For business reviews at scale, our Google reviews API guide covers the official caps and the alternatives.
Is there an API for hotel reviews instead of scraping?
Not from the booking sites themselves, whose review data sits inside partner programs.
Third-party hotel data APIs return reviews per property, newest first, across Booking.com, TripAdvisor, Expedia, Google, and others, which is the graduation path from this tutorial.
Ready to simplify your hotel data?
Join other developers using StayAPI to build the next generation of travel applications. Get started for free today.