DEV Community

elliott maqy
elliott maqy

Posted on

Compare hotel prices on Booking.com, Expedia and Agoda with Python

You want to compare hotel prices on Booking.com, Expedia and Agoda for the same stay, and you want it in a DataFrame rather than in three browser tabs. The catch is not getting the data, it is that each site means something different by "price": Booking.com shows a total before the charges it lists as excluded, Expedia a total with taxes and fees included, Agoda both. This tutorial runs three Apify Actors that share one output schema, the Booking.com Hotels Scraper, the Expedia Hotels Scraper and the Agoda Hotels Scraper, then builds an all-in price per night and the cheapest site for each hotel in about forty lines of Python.

What you need

  • An Apify account and its API token, from Console → Settings → API & Integrations, exported as APIFY_TOKEN.
  • Python 3 and two packages:
pip install "apify-client>=3,<4" pandas
Enter fullscreen mode Exit fullscreen mode

The three Actors return the same column names in the same order (title, pricePerNight, priceTotal, priceBeforeDiscount, taxesAndFees, rating, reviewCount, latitude, longitude, url…), with a platform column that says where each row comes from. That is what makes a single DataFrame possible.

Step 1: one stay, three inputs

Keep the stay identical everywhere: Lisbon, three nights starting in 30 days, two adults, one room, prices in US dollars. The inputs below come from three public tasks: "Hotels in Lisbon for a weekend, prices in EUR" on Booking.com (hotels only, through Booking.com's ht_id=204 filter), "Hotels in Lisbon for a long weekend, prices in USD" on Expedia and "Cheapest hotels in Lisbon next month, prices in USD" on Agoda. Two values are aligned on purpose: the Booking.com task uses two nights and euros, so the script sets three nights and USD on all three.

Relative dates (checkInDaysFromNow, nights) are convenient because a scheduled script never expires; for a one-off quote, replace them with checkIn and checkOut in YYYY-MM-DD on the three inputs.

Step 2: run the three Actors

import os
import re
import unicodedata

import pandas as pd
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])

stay = {"checkInDaysFromNow": 30, "nights": 3, "adults": 2, "currency": "USD", "maxResults": 60}


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"),
    {"location": "Lisbon, Portugal", "destId": -2167973, "filters": "ht_id=204", **stay},
)
expedia = fetch(client.actor("myth84/expedia-hotels-scraper"), {"location": "Lisbon, Portugal", **stay})
agoda = fetch(client.actor("myth84/agoda-hotels-scraper"), {"location": "Lisbon", "sortBy": "PRICE_ASC", **stay})

for item in booking[:3] + expedia[:3] + agoda[:3]:
    print(item["platform"], item["title"], item.get("priceTotal"), item["currency"])
Enter fullscreen mode Exit fullscreen mode

Each call waits for its run to finish, then reads the run's dataset. The fields not given keep each Actor's defaults. Each row is one property with the stay dates, the prices, the rating on 10, the number of reviews, coordinates and a link that opens the property with those dates.

Step 3: what each site puts in the price columns

This is the part to read twice. Real rows from the three Actors for Lisbon, 20 to 23 November 2026, two adults (three different properties: the point is the conventions, not the amounts):

Column Booking.com Expedia Agoda (default priceView)
priceTotal 637.43 EUR, before excluded charges 582 USD, taxes and fees included 84.06 USD, taxes and fees included
taxesAndFees 62.25 EUR, the excluded charges null (not itemised) 17.43 USD, the difference for the stay
pricePerNight 212.48 EUR, average per night 174 USD, the card's nightly price 28.02 USD, cheapest offer per night
priceBeforeDiscount strike-through total null unless a deal strike-through price per night

So the comparable number is an all-in total: priceTotal + taxesAndFees on Booking.com, priceTotal as is on Expedia, and priceTotal as is on Agoda with its default priceView (inclusive). Then divide by nights:

df = pd.DataFrame(booking + expedia + agoda)
df = df[df["priceTotal"].notna()].copy()

df["allInTotal"] = df["priceTotal"]
on_booking = df["platform"] == "booking"
df.loc[on_booking, "allInTotal"] += df.loc[on_booking, "taxesAndFees"].fillna(0)
df["allInPerNight"] = (df["allInTotal"] / df["nights"]).round(2)

print(df.groupby("platform")["allInPerNight"].describe()[["count", "50%", "min", "max"]])
Enter fullscreen mode Exit fullscreen mode

Agoda returns properties sold out for the dates with isAvailable false and empty prices rather than dropping them; the filter on the second line sets them aside.

Step 4: match the same hotel across sites

The three sites use their own identifiers, so platformListingId does not join across platforms. Exact names after normalisation catch chains and most independent hotels:

def name_key(title):
    text = unicodedata.normalize("NFKD", title).encode("ascii", "ignore").decode().lower()
    text = re.sub(r"\b(hotel|hostel|lisbon|lisboa|the|by|and)\b", " ", text)
    return " ".join(re.findall(r"[a-z0-9]+", text))


df["nameKey"] = df["title"].map(name_key)
prices = df.pivot_table(index="nameKey", columns="platform", values="allInPerNight", aggfunc="min")
prices = prices.dropna(thresh=2)
table = prices.join(pd.DataFrame({
    "cheapest": prices.idxmin(axis=1),
    "spread": prices.max(axis=1) - prices.min(axis=1),
}))

print(table.sort_values("spread", ascending=False).head(15))
table.to_csv("lisbon-hotels-3-sites.csv")
Enter fullscreen mode Exit fullscreen mode

For the rest, latitude and longitude are present on all three: two rows within about 50 metres with a similar name are almost always the same property. starRating is a useful sanity check where the site shows it (Booking.com and Agoda do; Expedia carries it on expedia.fr in EUR, not on expedia.com in USD).

The result has one line per hotel found on at least two sites, its all-in price per night on each, the cheapest site and the gap. For a traveller it is a shortlist; for a hotelier it is a rate-parity check of their own property against the three sites.

Without code

Each input is also a task in Apify Console: open the public task of each Actor, adjust the stay, save it, and add a Schedule that runs the three at the same hour. Each run's Output tab exports CSV, JSON or Excel with the same columns, so the matching above also works in a spreadsheet.

Honest limits

  • Several cities, one stay from the form. On each Actor, locations takes up to 50 destinations in one run, all with the same dates, guests and currency, and maxResults applies to each destination. Search URLs copied from the site (startUrls on Booking.com and Expedia, searchUrls on Agoda) are one search each and keep their own dates.
  • Different depths and filters. About 1,000 properties per search on Booking.com, about 500 on Expedia, thousands on Agoda. The Booking.com input keeps hotels only; Expedia and Agoda also list apartments and guest houses, which simply do not match.
  • Sort orders differ. Booking.com's price and Expedia's PRICE_LOW_TO_HIGH are exact; Agoda's PRICE_ASC sorts on the price before taxes.
  • Search cards are a subset. Amenities, room details and full cancellation terms are not in search results.
  • Currencies follow the site. Expedia is USD on expedia.com and EUR on expedia.fr. Never mix currencies in one column; filter or convert on currency first.
  • Rankings move between sessions on all three sites; use the sorted orders when position matters.
  • Property facts only. The rows hold the average score and the number of reviews, nothing about guests or reviewers.

Pricing is pay-per-event on each Actor, a fixed price per property saved; the Pricing tab of each Store page has the current rates. Use the data for analysis, respect the platform's 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)