Hotel prices are spread over many sites, and two of them give a good picture of a city: Booking.com, with its own rates and guest scores, and Google Hotels, which shows the lowest price among dozens of booking sites for each hotel. Reading one without the other gives half of it. This tutorial scrapes hotel prices from both with Python, for the same city and the same dates, and puts them in a single pandas table: all-in prices per night, guest scores on the same scale, a summary by star class and a hotel-by-hotel comparison. The data comes from two Apify Actors, the Booking.com Hotels Scraper and the Google Hotels Scraper, called through the official Python client.
What you need
- An Apify account (the free plan is enough to try this) and its API token, from Apify Console → Settings → API & Integrations, exported as an environment variable:
export APIFY_TOKEN=... - Python 3 and two packages:
pip install "apify-client>=3,<4" pandas
The two Actors return the same column names: platform, title, propertyType, starRating, rating, ratingMax, reviewCount, checkIn, checkOut, nights, currency, pricePerNight, priceTotal, taxesAndFees, latitude, longitude, url… That is what makes one DataFrame possible. Two columns need care, and the script handles both:
-
Taxes. On Booking.com,
priceTotalis the price of the stay before the taxes and charges it lists as excluded, given intaxesAndFees. On Google Hotels,priceTotalalready includes taxes and fees. -
Scores. Booking.com scores are on 10, Google ratings on 5;
ratingMaxsays which.
Step 1: one stay, two inputs
Both Actors take the same stay fields, so one dictionary serves both:
stay = {"checkInDaysFromNow": 30, "nights": 3, "adults": 2, "currency": "EUR", "maxResults": 60}
booking_input = {"location": "Lisbon, Portugal", "destId": -2167973, "filters": "ht_id=204", **stay}
google_input = {"searchQueries": ["hotels in Lisbon"], **stay}
-
checkInDaysFromNowandnightsgive a stay relative to today (check-in in 30 days, 3 nights), so the script can run again next week. Each Actor computes the dates from today in UTC; the script checks that both priced the same nights. For fixed dates, use"checkIn"and"checkOut"(YYYY-MM-DD) instayinstead. - On Booking.com,
destIdis thedest_idof a Booking.com search URL, the reliable way to name a destination, andfiltersis Booking.com's own filter string:ht_id=204keeps hotels only. Rows come in Booking.com's top-picks order. - On Google Hotels, the search is written as on Google ("hotels in Lisbon"). Hotels, hostels and guest houses come back; vacation rentals are left out by default.
-
maxResultsis the number of hotels saved and charged on each side.
Step 2: run both and build one table
Save this as hotel_prices.py:
import os
import pandas as pd
from apify_client import ApifyClient
stay = {"checkInDaysFromNow": 30, "nights": 3, "adults": 2, "currency": "EUR", "maxResults": 60}
booking_input = {"location": "Lisbon, Portugal", "destId": -2167973, "filters": "ht_id=204", **stay}
google_input = {"searchQueries": ["hotels in Lisbon"], **stay}
client = ApifyClient(os.environ["APIFY_TOKEN"])
def fetch(actor, run_input):
run = actor.call(run_input=run_input)
return list(client.dataset(run.default_dataset_id).iterate_items())
booking = fetch(client.actor("myth84/booking-hotels-scraper"), booking_input)
google = fetch(client.actor("myth84/google-hotels-scraper"), google_input)
df = pd.DataFrame(booking + google)
print(df.groupby("platform")["checkIn"].unique())
df = df[df["priceTotal"].notna()].copy()
on_booking = df["platform"] == "booking"
df["allInTotal"] = df["priceTotal"] + df["taxesAndFees"].where(on_booking, 0).fillna(0)
df["allInPerNight"] = (df["allInTotal"] / df["nights"]).round(2)
df["score"] = (10 * df["rating"] / df["ratingMax"]).round(1)
columns = ["platform", "title", "starRating", "score", "reviewCount", "allInPerNight", "allInTotal", "currency", "url"]
df[columns].sort_values("allInPerNight").to_csv("lisbon-hotel-prices.csv", index=False)
print(f"{len(df)} priced hotels saved to lisbon-hotel-prices.csv")
platform is booking or google_hotels. allInTotal is what a guest pays for the stay on each side: the Booking.com price plus its excluded charges, the Google price as it is. score puts both ratings on 10. The CSV is the single table: one row per hotel and source, sorted by all-in price per night.
The print of the check-in dates shows one date per platform. If they differ (a run started just before midnight UTC and the other just after), run again, or use fixed dates.
Step 3: the market at a glance
Add this to the script:
overview = df.groupby("platform").agg(
hotels=("title", "count"),
cheapestPerNight=("allInPerNight", "min"),
medianPerNight=("allInPerNight", "median"),
medianScore=("score", "median"),
)
print(overview)
by_class = df.pivot_table(index="starRating", columns="platform", values="allInPerNight", aggfunc="median")
print(by_class.round(0))
The first table compares the two sources as a whole. The second gives the median all-in price per night of each star class on each side, the fairest way to compare two lists that do not contain exactly the same hotels: Booking.com ranks by its own popularity, Google by its own relevance.
Step 4: the same hotel on both sites
Many hotels appear on both sides. Match them by name, and confirm with the coordinates so that two "Hotel Lisboa" in different streets do not pair up:
import re
import unicodedata
def name_key(title):
text = unicodedata.normalize("NFKD", title).encode("ascii", "ignore").decode().lower()
text = re.sub(r"\b(hotel|lisbon|lisboa|the|by|and)\b", " ", text)
return " ".join(re.findall(r"[a-z0-9]+", text))
df["nameKey"] = df["title"].map(name_key)
keep = ["nameKey", "title", "score", "allInPerNight", "latitude", "longitude", "url"]
pairs = (
df[df["platform"] == "booking"][keep]
.merge(df[df["platform"] == "google_hotels"][keep], on="nameKey", suffixes=("Booking", "Google"))
.query("abs(latitudeBooking - latitudeGoogle) < 0.003 and abs(longitudeBooking - longitudeGoogle) < 0.004")
.eval("gapPerNight = allInPerNightGoogle - allInPerNightBooking")
)
shown = ["titleBooking", "allInPerNightBooking", "allInPerNightGoogle", "gapPerNight", "scoreBooking", "scoreGoogle"]
print(pairs.sort_values("gapPerNight")[shown].to_string(index=False))
pairs.to_csv("lisbon-same-hotels.csv", index=False)
pairs holds one row per hotel found on both sites, with both prices and both scores. A negative gapPerNight means Google Hotels found a lower price than the Booking.com search; since Google's price is the lowest among all the booking sites it lists, often Booking.com itself, gaps are mostly small. A large gap usually means a different room or rate on each side (Booking.com prices the unit it matches to your guests, Google the cheapest rate of the stay): open both url columns of the CSV before drawing conclusions. The scores tell you how guests rate the hotel on Booking.com and on Google, two different audiences.
Schedule it
With cron, one line runs the script every morning (with APIFY_TOKEN set in the cron environment):
0 7 * * * cd /path/to/hotels && python3 hotel_prices.py >> hotels.log 2>&1
Without code: open the public tasks of the two Actors in Apify Console ("Hotels in Lisbon for a weekend, prices in EUR" on Booking.com, "Compare booking sites for Lisbon hotels next month" on Google Hotels), set the same stay on both, save them as your own tasks and add a daily schedule. Both Actors also have a price tracker mode (monitorPrices) that flags every price change since your previous run.
Good to know
- Rankings move on both sites between requests, so the hotels of each list change a little from run to run; the medians by star class move much less than the individual rows.
-
One price per hotel and source: the search card's price for your guests on Booking.com, the lowest booking-site price on Google Hotels. Google Hotels can also return every site's rate with
includeOffers. -
Same currency on both sides: both Actors return every price in the
currencyof the input; USD and EUR are validated. - A run that finds nothing fails with an explicit message, on either side.
- No personal data: hotel facts, average scores and review counts, nothing about guests or reviewers.
Pricing is pay-per-event on both Actors, a fixed price per hotel saved: $2.50 per 1,000 on Apify's Free plan for each, down to $1.69 on the higher plans, so this run of 2 × 60 hotels costs about $0.30. Use the data for analysis, respect the platforms' terms and local law, and do not republish listings.
Disclosure: I build these Actors and may earn a commission if you sign up to Apify through these links.
Top comments (0)