DEV Community

Cover image for TripAdvisor hotel reviews in 30 site languages, without a browser (Python)
Tedj MEABIOU
Tedj MEABIOU

Posted on

TripAdvisor hotel reviews in 30 site languages, without a browser (Python)

TripAdvisor has no public API for review text and its pages sit behind DataDome, so most tools that read them drive a headless browser. They do not have to: the full record is already in the HTML the server sends, ten reviews to a page, paged by a plain offset. This is how to read TripAdvisor reviews as rows in thirty site languages, with no browser and no key.

1. One request, one row per review

The TripAdvisor Reviews Scraper on Apify is a hotel reviews scraper: give it hotel URLs, TripAdvisor location ids (the d<number> in a URL) or hotel names, get reviews as rows.

curl -X POST "https://api.apify.com/v2/acts/kestrel~tripadvisor-reviews-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"locationIds": ["4509998"], "maxReviewsPerHotel": 50, "languages": ["en"]}'
Enter fullscreen mode Exit fullscreen mode

A review row:

{ "type": "review", "review_id": "1075128384", "hotel_name": "Memmo Alfama Hotel",
  "language": "en", "original_language": "en", "translated": false, "title": "A Gem in the Heart of Alfama",
  "text": "This place is a gem — a gorgeous haven in the heart of Alfama, just off tram 28.",
  "rating": 5, "sub_ratings": {"value": 4, "rooms": 4, "location": 5, "cleanliness": 5, "service": 5, "sleep_quality": 5},
  "review_date": "2026-08-27", "stay_date": "2026-08-31", "trip_type": "COUPLES", "helpful_votes": 0,
  "reviewer_name": "Frank S", "reviewer_contributions": 391, "reviewer_hometown": "Brooklyn, New York",
  "photos": [], "response": "Dear Frank, thank you so much for your incredible review",
  "url": "https://www.tripadvisor.com/ShowUserReviews-g189158-d4509998-r1075128384-Memmo_Alfama_Hotel-Lisbon.html" }
Enter fullscreen mode Exit fullscreen mode

rating is 1–5 bubbles; sub_ratings carries the six aspects a reviewer can score on their own — value, rooms, location, cleanliness, service, sleep quality — and only the ones they filled in. stay_date is the month of the stay, trip_type one of FAMILY, COUPLES, SOLO, BUSINESS, FRIENDS or null.

2. Why no browser is needed

A review page is /Hotel_Review-g<geo>-d<id>-Reviews-or10-…html: or10 is the review offset and page three or20. It is server-rendered, with the whole record — sub-ratings, reviewer profile, management response — inside its hydration payload, a JSON.parse("…") literal in the bootstrap script.

DataDome reads the TLS handshake, not the traffic pattern: datacenter ranges are refused outright and Chrome-shaped clients challenged, while a residential IP with a Firefox fingerprint is served at a page per second. A refused page is rotated onto a fresh session; a hotel that never comes through is error, never no_reviews.

3. Language is the domain

TripAdvisor is not one site. tripadvisor.com serves English, tripadvisor.de German, tripadvisor.fr French, and each domain carries the reviews written in its own language plus machine translations of the rest. languages is therefore a list of hosts — thirty, from de and ja to en-sg — and one hotel × one language is one job. Each row names the site it was read on (language), what the guest wrote in (original_language) and whether this is a translation (translated); includeTranslated: false keeps each review once, in its author's words.

{ "locationIds": ["4509998", "228423"], "hotelNames": ["The Lumiares Lisbon"], "languages": ["en", "de", "fr"],
  "includeTranslated": false, "maxRating": 3, "requireText": true, "tripTypes": ["business", "solo"],
  "sinceDate": "6 months", "maxReviewsPerHotel": 0 }
Enter fullscreen mode Exit fullscreen mode

maxRating, tripTypes, sinceDate and requireText run before billing, so what they drop is never charged, and sinceDate stops the paging once a whole page predates it. A URL, its id and its name are one hotel, read once.

4. Python: which aspect each hotel loses on

import os, statistics
from collections import Counter, defaultdict
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])
def rows(inp): return [r for r in client.dataset(client.actor("kestrel/tripadvisor-reviews-scraper").call(run_input=inp)["defaultDatasetId"]).iterate_items() if r["type"] == "review"]

comp = rows({"locationIds": ["4509998", "228423", "190364"], "languages": ["en", "de", "fr"],
             "includeTranslated": False, "sinceDate": "12 months", "requireText": True, "maxReviewsPerHotel": 0})
aspects = defaultdict(list)
for r in comp:
    for aspect, bubbles in (r["sub_ratings"] or {}).items(): aspects[(r["hotel_name"], aspect)].append(bubbles)
for (hotel, aspect), v in sorted(aspects.items(), key=lambda kv: statistics.mean(kv[1]))[:10]: print(f"{statistics.mean(v):.2f}  {len(v):4}  {hotel:30} {aspect}")
print(Counter(r["language"] for r in comp), sum(r["response"] is None for r in comp), "of", len(comp), "unanswered")
Enter fullscreen mode Exit fullscreen mode

Three hotels, a year of guest feedback in three languages, and the first ten lines are each property's weakest aspect and how many guests scored it — counted, no text model anywhere. The last line is the reputation one: how many reviews nobody answered. The Booking.com Reviews Scraper is the second corpus beside it: no aspect scores, but it splits what guests liked from what they did not.

5. Review monitoring on a schedule (n8n)

The Agoda template in the n8n/ folder of kestrel-actors-examples is the workflow to copy: an 08:00 Schedule trigger, an HTTP Request to the run-sync endpoint, a Code node dropping ids seen before, an IF, a Sheets append and a Slack message. For a tripadvisor scraper feed, send the section 3 body with sinceDate: "2 days" and dedupe on review_id plus language — a German review read from two sites is one review, twice.

6. Cost and limits

  • $0.005 per delivered review row; hotel and status rows, filtered reviews, unknown ids and duplicates are free. A hotel's whole English corpus of 1,456 reviews is $7.28; ten hotels at their 500 newest, $25 once — the daily feed after it costs cents a month.
  • Pages come newest first and there is no rating sort, so maxRating still reads the pages it filters — pair it with sinceDate or a cap.
  • The free hotel row carries reviews_by_language; review_count is the all-language total; one site lists far fewer.
  • Hotels only; restaurants and attractions have other page shapes.
  • The actor reads public review pages that any visitor sees without logging in. Reviews are user-generated content published under a public name or handle: if you build a product on top, you are the data controller for whatever you keep, so retain what you need and respect deletion. Read TripAdvisor's terms before scraping at scale — they restrict automated access, and the responsibility for how the data is used is yours. Do not republish reviews wholesale; do use them to understand what guests say about a property.

That is the feed: the record already in the page, the right IP and TLS to reach it, one domain per language. Full reference on the actor page: apify.com/kestrel/tripadvisor-reviews-scraper.

Every actor's inputs, output fields, a sample row and its honest limits are documented at mtedj.github.io/kestrel-actors-examples, including a side-by-side comparison of every review scraper — what each source really carries and where its ceiling is.

Top comments (0)