How to Scrape Hotel Listings: Property Data, Amenities, Photos, and Reviews

Denis Gramm, StayAPI
By Denis Gramm, StayAPI · · 10 min read

I hope you enjoy reading this blog post. If you want our team to help you with hotel data integration, click here.

Quick summary

Scraping hotel listings is a catalog problem: hundreds of properties, rich content per property, collected once and refreshed occasionally. This guide maps each listing field to its best source, walks through the catalog-scale mechanics (pagination, schemas, dedup), and covers the dataset and API shortcuts that often beat scraping entirely.

How to Scrape Hotel Listings: Property Data, Amenities, Photos, and Reviews — StayAPI

Scraping hotel prices is a monitoring problem: the same pages, polled every day, and it has its own hotel prices scraping tutorial.
This guide covers how to scrape hotel listings, which is a catalog problem.
The listing layer is the mostly-static content: names, locations, amenities, photos, review scores.
You collect it across hundreds of properties at once, then refresh it occasionally.
If you're seeding a travel product's property database, building an ML dataset, or enriching a comp set for an agency, this is your lane.
You'll get the field-by-field source map, a worked Python pattern for catalog-scale extraction, and the two shortcuts (open data and APIs) that often make scraping unnecessary for this job.

Key Takeaways

  • Listings are a catalog job, prices are a monitoring job. Catalogs get built once and refreshed quarterly; prices get polled daily. Different tools, different tutorials.
  • Each listing layer lives on a different source. Identity is easy everywhere, amenities are OTA-dependent, and photos come with licensing strings attached.
  • The scale mechanics are the hard part. Search-page enumeration, pagination, detail-page visits, and deduplication, not the parsing of any single page.
  • Cross-site identity is the hidden 40% of the work. The same hotel wears three names on three sites; name similarity plus geo distance is the standard fix.
  • The shortcuts are unusually strong for catalogs. Open datasets, Google Places, and hotel data APIs each beat scraping for a real slice of this job.

What's in a Hotel Listing (and Where It Lives)

What's in a Hotel Listing (and Where It Lives) — StayAPI

Every listing field type has a best source, and knowing the map before you scrape hotel data saves weeks.

Field type Examples Best sources Difficulty
Identity and location Name, address, coordinates, star class Google Places, Booking.com pages, OpenStreetMap Easy
Amenities Pool, parking, breakfast, pet policy Booking.com (most structured), TripAdvisor (prose-ish) Medium
Rooms and content Room types, descriptions, photos OTA detail pages Medium, licensing caveats
Review layer Aggregate score, review count, sub-scores Listing pages on every OTA Easy for summaries
Identifiers Booking hotel ID, TripAdvisor location ID Each platform's URLs The hidden 40%

Amenities deserve a warning: every OTA uses its own taxonomy.
Booking.com's facility list is the most structured (grouped categories like Bathroom, Services, Media & Technology); TripAdvisor's is closer to prose.
Mapping "parking" across 3 sites into one field is real work.
The review layer splits in two.
Aggregate score and review count sit right on listing pages; full review text is its own discipline with its own tooling, covered in our hotel reviews API guide.
Identifiers are the sleeper.
Every platform has its own ID scheme (Booking.com's numeric hotel ID, TripAdvisor's location ID, Expedia's property ID), and cross-referencing them is what makes a multi-source catalog usable.
And photos: they're licensed assets, the property's or the platform's.
Keep photo URLs as references for research, and source display imagery through licensed channels for anything user-facing.

The Tutorial: How to Scrape Hotel Listings at Catalog Scale

The Tutorial: How to Scrape Hotel Listings at Catalog Scale — StayAPI

The worked target: a 200-property catalog for one city, built from an OTA's search results.
Single-page scraping is easy; the catalog mechanics are what this section actually teaches.

Step 1: Enumerate via search pages

A city search gives you the property list: name, URL, coordinates, review score per result card.
Paginate through it politely, because this is a couple hundred requests, not five.

import time
import requests
from bs4 import BeautifulSoup

HEADERS = {"User-Agent": "Mozilla/5.0 ...", "Accept-Language": "en-US,en;q=0.9"}
SEARCH = "https://www.example-ota.com/search?city=lisbon&offset={}"

properties = []
for offset in range(0, 500, 25):        # 20 pages of 25 results
    html = requests.get(SEARCH.format(offset), headers=HEADERS, timeout=15).text
    cards = BeautifulSoup(html, "html.parser").select("[data-testid='property-card']")
    if not cards:
        break                           # last page, or a block page: check which
    for card in cards:
        properties.append({
            "name": card.select_one("h3").get_text(strip=True),
            "url": card.select_one("a")["href"],
        })
    time.sleep(2)                       # politeness, and fewer blocks

Selector names vary per site and change over time; find the current ones in DevTools rather than copying anyone's from a blog post.

Step 2: Visit detail pages for depth

Each property page adds the amenities block, room list, description, and photo URLs (references only, per the licensing note above).
Same escalation logic as the prices tutorial: requests first, Playwright when the content renders via JavaScript.
At 200 properties with a 2-second throttle, a full detail pass runs about 10 minutes.
Budget that, and don't parallelize your way into a ban.

Step 3: Normalize into a schema

Half of catalog pain is schema drift: freeze the shape before you collect property number two.

PROPERTY_SCHEMA = {
    "property_id": "uuid-you-mint",
    "name": "Hotel Aurora",
    "latitude": 38.7223,
    "longitude": -9.1393,
    "star_rating": 4,
    "amenities": ["pool", "parking", "breakfast"],
    "review_score": 8.6,
    "review_count": 1240,
    "source": "booking",
    "source_id": "1331780",
    "scraped_at": "2026-07-28",
}

Keep source and source_id on every record even if you only scrape one site today.
Multi-source is where every catalog ends up.

Step 4: Deduplicate across sources

The same hotel appears as "Hotel Aurora Lisboa", "Aurora Hotel", and "Hotel Aurora, Lisbon" on three sites.
The standard pattern: name similarity plus geo proximity.

from difflib import SequenceMatcher
from math import radians, sin, cos, asin, sqrt

def distance_m(a, b):
    lat1, lng1, lat2, lng2 = map(
        radians, [a["latitude"], a["longitude"], b["latitude"], b["longitude"]]
    )
    h = sin((lat2 - lat1) / 2) ** 2 + cos(lat1) * cos(lat2) * sin((lng2 - lng1) / 2) ** 2
    return 6371000 * 2 * asin(sqrt(h))

def same_property(a, b):
    name_score = SequenceMatcher(None, a["name"].lower(), b["name"].lower()).ratio()
    return name_score > 0.75 and distance_m(a, b) < 100

Merge matches into one property node and keep every per-source ID alongside.
The honest caveat: chains break this.
Two properties of the same brand 200 meters apart score high on name similarity, so review anything that matches on name but sits 100 to 500 meters apart.

Step 5: Refresh strategy

Content decays slowly, which is the whole reason catalogs are cheaper to run than price feeds.

Layer Refresh cadence Trigger
Identity, location Yearly Rebrands, closures
Amenities, photos Quarterly Renovations
Review score, count Monthly Continuous drift
Prices Daily (different discipline) Constant change

That last row belongs to the prices tutorial linked in the intro; keeping the two jobs separate is what keeps both cheap.

The Shortcuts: When You Shouldn't Scrape at All

The Shortcuts: When You Shouldn't Scrape at All — StayAPI

For catalog jobs specifically, the alternatives are unusually strong, and two of the three aren't even our product.
(If what you actually want is an off-the-shelf scraper rather than code or an API, we compared the hotel scraper tools separately.)
Open datasets.
Kaggle hotel datasets and OpenStreetMap cover identity and geo instantly and for free, which is often all a prototype or ML experiment needs.
Our free hotel API guide rounds these up.
The limits: staleness, thin amenities, no review layer.
Google Places.
Self-serve and metered per request, decent for name, geo, photos, and basic info, with a capped review teaser per property.
Fine as an enrichment layer; thin as a full catalog source.
Our Google Hotels API guide covers where Google's data helps and where it stops.
A hotel data API.
Providers in this category return the listing layer as JSON for any public property, extraction and normalization already done.
StayAPI's content endpoints cover identity, coordinates, star rating, review summaries, photos, rooms, and grouped amenities across major OTAs (Booking.com, TripAdvisor, and Airbnb among them).
Here's the listing layer for one property from the Booking.com hotel details endpoint:

curl -G "https://api.stayapi.com/v2/booking/hotel/details" \
  -H "x-api-key: YOUR_API_KEY" \
  -d "hotel_id=1331780"
{
  "success": true,
  "hotel_id": "1331780",
  "data": {
    "hotel_name": "Le Grand Hotel InterContinental",
    "address": "2 Rue Scribe, 75009 Paris, France",
    "latitude": 48.871586,
    "longitude": 2.330359,
    "star_rating": 5,
    "review_score": 8.9,
    "review_count": 2847,
    "main_photo_url": "https://cf.bstatic.com/..."
  }
}

The step 4 problem has an endpoint too: meta search takes a hotel name and returns that property's URL on each platform (Booking.com, Expedia, Agoda, TripAdvisor, the official site), so cross-site identity arrives pre-matched instead of fuzzy-matched.

Option Cost shape Depth Freshness Best for
Open datasets Free Identity and geo only Stale Prototypes, ML experiments
Google Places Metered per request Basic info, capped reviews Live Enrichment layer
Scrape it yourself Your time, ongoing Anything you can parse You decide One-time research datasets
Data API (StayAPI) Request-metered, 50 free Identity, amenities, rooms, review layer Live, on request Catalogs that must stay current

For a one-time research dataset, scraping or open data may genuinely be the right call.
The API earns its keep when the catalog has to stay current, carry amenity and review depth, or cross-reference OTAs cleanly.
Getting started, if you take the API route:
1. Sign up at stayapi.com: 50 free requests, no credit card.
2. Preview the data with the free Booking.com hotel details tool before writing code.
3. Resolve your property list with meta search, then pull details per property into the schema from step 3.
4. Prefer no-code? The same flow runs in N8N or Make.com as scheduled HTTP requests into a Google Sheet.

FAQ

How do I scrape hotel listings from Booking.com?

Search-page enumeration plus detail-page visits, exactly as in the tutorial above.
Expect the strongest anti-bot pressure in the vertical, though: reliability at catalog scale degrades fast, and our Booking.com API guide covers the alternatives.

Can I use scraped hotel photos in my product?

Treat photos differently from text facts: they're licensed creative work.
Photo URLs as internal references are common practice; for anything user-facing, source imagery through licensed channels or the platforms' partner programs.

How do I match the same hotel across different sites?

Name similarity plus geo proximity (about a 100-meter radius) resolves most of it, with per-source IDs stored to avoid re-matching later.
A meta search API that returns each platform's URL for a property skips the fuzzy matching entirely.

Where can I get a free hotel listings dataset?

Kaggle hotel datasets and OpenStreetMap are the two real options: identity and location for free, good for ML and prototypes.
They're stale and shallow for production catalogs, which is where live sources come in.

How often should a hotel catalog be refreshed?

Quarterly for amenities and photos, monthly for review scores, yearly for identity, and event-driven for closures and rebrands.
Prices are the exception: they change daily and belong in a separate monitoring pipeline, not in your catalog refresh.

Ship in 3 days

Ready to simplify your hotel data?

Join other developers using StayAPI to build the next generation of travel applications. Get started for free today.