Search results tell you which hotels exist and roughly what they cost. Once you are down to eight candidates for a precise stay, the questions change: which room is the cheap price for, can it be cancelled for free and until when, is breakfast included, is parking free or paid? Answering that means opening eight hotel pages and copying facts into a spreadsheet. This tutorial exports Booking.com hotel rooms and rates to CSV with Python instead: the Booking.com Hotel Details Scraper takes hotel URLs and a stay and returns one flat row per hotel with the room types, the facilities by group and the cheapest available offer for your dates; a few lines of pandas turn the rows into a comparison table.
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
- Hotel page URLs. Any Booking.com hotel URL copied from the browser works, with or without a language suffix or query parameters; the short form
pt/masa-campo-grandeworks too. Search result URLs are not accepted.
Step 1: the input
The example is a family stay: two adults and two children aged 6 and 10, three nights in Lisbon, a shortlist of eight properties mixing hotels, apartment hotels and suites. It is the input of the public task "Price a Lisbon family stay in US dollars":
run_input = {
"hotelUrls": [
"https://www.booking.com/hotel/pt/villa-baixa-lisbon-luxury-apartments.html",
"https://www.booking.com/hotel/pt/wilde-lisbon-liberdade.html",
"https://www.booking.com/hotel/pt/masa-campo-grande.html",
"https://www.booking.com/hotel/pt/family-refuge-lisbon.html",
"https://www.booking.com/hotel/pt/cloud-one-lisbon.html",
"https://www.booking.com/hotel/pt/purple-break-apartaments.html",
"https://www.booking.com/hotel/pt/vip-executive-suites-do-marques.html",
"https://www.booking.com/hotel/pt/bacalhoeiros-125.html",
],
"adults": 2,
"children": 2,
"childrenAges": [6, 10],
"currency": "USD",
"maxPhotos": 5,
"maxResults": 8,
}
The fields not given keep the Actor's defaults: one room, texts in en-us, the cheapest offer included. Without checkIn and checkOut, the stay starts 30 days after the run date for three nights, which suits a scheduled script. For a real trip, add both dates in YYYY-MM-DD; dates inside a pasted URL are not used, the stay always comes from the input. The children's ages matter, because Booking.com's prices depend on them; a child without an age counts as 8.
Step 2: run it and read the rows
import os
import pandas as pd
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("myth84/booking-hotel-details-scraper").call(run_input=run_input)
hotels = list(client.dataset(run.default_dataset_id).iterate_items())
for item in hotels:
print(item["title"], item.get("priceTotal"), item.get("taxesAndFees"), item["currency"], item.get("cancellationPolicy"))
What the main columns mean, on a real row from the Actor's documentation (a Lisbon hotel for 20 to 23 November 2026, two adults, in EUR):
-
priceTotal(289.81) is the cheapest available offer for the whole stay, andtaxesAndFees(41.39) the charges Booking.com announces as not included. The all-in price is the sum, 331.20 EUR, or 110.40 EUR per night, not the 96.60 ofpricePerNight. -
priceBeforeDiscount(362.26) is the crossed-out total: the offer is shown as a discount. -
cancellationPolicyisfree_cancellation_until:2026-11-15, five days before arrival; it isnullwhen the cheapest offer cannot be cancelled for free. -
meal_plan_includedinbadgesmeans the offer includes breakfast or another meal plan. -
highlightscontains a line "Cheapest offer for your dates: Deluxe Double or Twin Room", the room the offer belongs to;sleepingArrangementslists every room type of the hotel with its amenities. -
amenityGroupsholds the facilities under Booking.com's own groups (Bathroom, Internet, Parking…), with the payment notes it gives: "(free)", "(paid)", "(paid: 2 EUR per hour)", "(off-site)". -
isAvailableisfalsewhen Booking.com had no offer for this occupancy and these dates.
Step 3: the comparison table with pandas
OFFER_PREFIX = "Cheapest offer for your dates: "
def offer_room(highlights):
return next((h[len(OFFER_PREFIX):] for h in highlights or [] if h.startswith(OFFER_PREFIX)), None)
df = pd.DataFrame(hotels)
df["allInTotal"] = df["priceTotal"] + df["taxesAndFees"].fillna(0)
df["allInPerNight"] = (df["allInTotal"] / df["nights"]).round(2)
df["freeCancellationUntil"] = df["cancellationPolicy"].map(lambda policy: policy.split(":", 1)[1] if policy else None)
df["breakfast"] = df["badges"].map(lambda badges: "meal_plan_included" in (badges or []))
df["offerRoom"] = df["highlights"].map(offer_room)
df["parking"] = df["amenityGroups"].map(lambda groups: "; ".join((groups or {}).get("Parking", [])))
df["roomTypes"] = df["sleepingArrangements"].map(lambda rooms: len(rooms or []))
columns = [
"title", "isAvailable", "allInTotal", "allInPerNight", "freeCancellationUntil",
"breakfast", "offerRoom", "parking", "roomTypes", "rating", "reviewCount", "starRating", "url",
]
table = df[columns].sort_values("allInTotal", na_position="last")
print(table.to_string(index=False))
table.to_csv("lisbon-family-stay.csv", index=False)
Two habits help. Sort on the all-in price, never on pricePerNight, because some hotels exclude more charges than others. And read isAvailable before the price: false means sold out, a minimum stay, or too many guests for one room. For a family of four, run again with "rooms": 2 before crossing a hotel off.
Optional: start from a search instead of a list
If you start from a destination, run the Booking.com Hotels Scraper first and feed its url column to the details Actor. Here with the input of its public task "Hotels in Lisbon for a weekend, prices in EUR":
search = client.actor("myth84/booking-hotels-scraper").call(run_input={
"location": "Lisbon, Portugal",
"destId": -2167973,
"filters": "ht_id=204",
"checkInDaysFromNow": 30,
"nights": 2,
"adults": 2,
"currency": "EUR",
"maxResults": 60,
})
urls = [item["url"] for item in client.dataset(search.default_dataset_id).iterate_items()][:8]
run_input["hotelUrls"] = urls
Insert it in the script of step 2, just before the call to the details Actor, and the details Actor prices the first eight hotels of the search, for the stay of its own input. The two Actors share their first columns and join on platformListingId, so the search rank and the detail columns can end up on the same line.
Without code
In Apify Console, open the public task "Price a Lisbon family stay in US dollars", replace the URLs and the stay, and run it. The Output tab has four views of the same rows (Overview, Prices, Rooms and facilities, Photos and descriptions), and the export to CSV, JSON or Excel keeps every column in the same order on every run. Add a Schedule to re-price the shortlist daily: prices change, cancellation deadlines follow the arrival date, rooms sell out.
Honest limits
- One offer per hotel. The row carries the cheapest available offer for your occupancy, not the rate table of every room. A more expensive room with free cancellation or breakfast does not appear; the breakfast flag and the cancellation date belong to the cheapest offer only.
- No house rules or check-in times in this version, and no category scores: the overall score on 10 and the number of reviews only.
-
Paid facilities keep the hotel's currency.
currencyconverts the offer, but "paid: 2 EUR per hour" stays in EUR in a USD run. -
Texts are Booking.com's, in the requested
language; the flags (propertyType,badges) stay in English. - Unknown pages are skipped. A removed hotel yields no row and is not charged; the run log names it.
- Property facts only. Nothing about guests, reviewers or hosts is collected, and no review texts.
Pricing is pay-per-event, a fixed price per hotel saved with the offer included; the Pricing tab of the Store page has the current rates. Use the data for analysis, respect the platform's terms and local law, and do not republish hotel pages, descriptions or photos.
Disclosure: I build these Actors and may earn a commission if you sign up to Apify through these links.
Top comments (0)