Repository navigation
Expand file tree
/
Copy pathscrape.py
More file actions
165 lines (140 loc) · 6.2 KB
/
Copy pathscrape.py
File metadata and controls
165 lines (140 loc) · 6.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
"""Google Hotels scraper in Python: hotel prices by booking site, to CSV.
Runs the Google Hotels Scraper (an Apify Actor) for one or more searches and
saves one row per hotel to a CSV file; with --offers, also one row per hotel
and booking site to a second CSV file (rate parity).
export APIFY_TOKEN="<your-apify-token>"
python scrape.py "hotels in Lisbon" --check-in 2026-11-20 --check-out 2026-11-23
python scrape.py "hotels in Rome" --offers --max-results 20
"""
import argparse
import csv
import json
import os
import sys
from decimal import Decimal
from apify_client import ApifyClient
ACTOR = "myth84/google-hotels-scraper"
# Columns written to the CSV. Every row of the dataset has more (see the README).
COLUMNS = [
"platformListingId",
"title",
"propertyType",
"city",
"checkIn",
"checkOut",
"nights",
"currency",
"pricePerNight",
"pricePerNightWithTaxes",
"priceTotal",
"taxesAndFees",
"rating",
"reviewCount",
"starRating",
"amenities",
"offerCount",
"cheapestOfferSite",
"cheapestOfferPriceTotal",
"officialSitePriceTotal",
"latitude",
"longitude",
"website",
"searchLocation",
"url",
]
# Keys of each booking site's rate in the offers column, written with --offers.
OFFER_COLUMNS = [
"site",
"isOfficialSite",
"currency",
"pricePerNight",
"priceTotal",
"priceTotalWithTaxes",
"bookingUrl",
]
def make_parser(description):
parser = argparse.ArgumentParser(description=description)
parser.add_argument("queries", nargs="+", help='searches as typed on Google, e.g. "hotels in Lisbon"')
parser.add_argument("--check-in", help="check-in date, YYYY-MM-DD (with --check-out)")
parser.add_argument("--check-out", help="check-out date, YYYY-MM-DD")
parser.add_argument("--check-in-days", type=int, default=30,
help="without fixed dates: check-in in N days, recomputed at every run (default 30)")
parser.add_argument("--nights", type=int, default=3, help="without fixed dates: nights of the stay (default 3)")
parser.add_argument("--adults", type=int, default=2)
parser.add_argument("--currency", default="USD", help="ISO code: USD, EUR, GBP, JPY...")
parser.add_argument("--max-results", type=int, default=50,
help="hotels per search (default 50); you pay per hotel saved")
parser.add_argument("--max-charge", type=Decimal, help="stop the run once it has cost this many USD")
return parser
def build_input(args):
if bool(args.check_in) != bool(args.check_out):
sys.exit("--check-in and --check-out go together.")
run_input = {
"searchQueries": args.queries,
"adults": args.adults,
"currency": args.currency,
"maxResults": args.max_results,
}
if args.check_in:
run_input["checkIn"] = args.check_in
run_input["checkOut"] = args.check_out
else:
run_input["checkInDaysFromNow"] = args.check_in_days
run_input["nights"] = args.nights
return run_input
def run_actor(run_input, max_charge=None):
"""Runs the Actor, waits for it to finish and returns the rows of its dataset."""
if not os.environ.get("APIFY_TOKEN"):
sys.exit("Set APIFY_TOKEN first: Apify Console > Settings > API & Integrations.")
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor(ACTOR).call(run_input=run_input, max_total_charge_usd=max_charge)
if run is None:
sys.exit("The run could not be started.")
status = getattr(run.status, "value", run.status) # SUCCEEDED, FAILED, TIMED-OUT...
if status != "SUCCEEDED":
sys.exit(f"The run ended as {status}: open its log in Apify Console.")
return list(client.dataset(run.default_dataset_id).iterate_items())
def cell(value):
"""Lists and objects as JSON text, so that every CSV row has the same columns."""
return json.dumps(value, ensure_ascii=False) if isinstance(value, (list, dict)) else value
def write_csv(rows, path, columns=COLUMNS, append=False):
new_file = not (append and os.path.exists(path))
with open(path, "a" if append else "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
if new_file:
writer.writerow(columns)
for row in rows:
writer.writerow([cell(row.get(column)) for column in columns])
def write_offers_csv(rows, path):
"""One line per hotel and booking site, cheapest site first."""
with open(path, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["title", "checkIn", "checkOut"] + OFFER_COLUMNS)
for row in rows:
for offer in row.get("offers") or []:
writer.writerow([row["title"], row["checkIn"], row["checkOut"]]
+ [offer.get(column) for column in OFFER_COLUMNS])
def main():
parser = make_parser("Scrape Google Hotels prices, ratings and booking-site rates to CSV.")
parser.add_argument("--offers", action="store_true",
help="add every booking site's rate for each hotel (no extra charge)")
parser.add_argument("--output", default="google_hotels.csv", help="CSV file (default google_hotels.csv)")
parser.add_argument("--offers-output", default="google_hotels_offers.csv",
help="CSV file of the booking-site rates, with --offers (default google_hotels_offers.csv)")
args = parser.parse_args()
run_input = build_input(args)
if args.offers:
run_input["includeOffers"] = True
rows = run_actor(run_input, args.max_charge)
write_csv(rows, args.output)
print(f"{len(rows)} hotels saved to {args.output}")
if args.offers:
write_offers_csv(rows, args.offers_output)
print(f"Booking-site rates saved to {args.offers_output}")
priced = sorted((row for row in rows if row.get("priceTotal") is not None), key=lambda row: row["priceTotal"])
for row in priced[:5]:
cheapest = f' cheapest on {row["cheapestOfferSite"]}' if row.get("cheapestOfferSite") else ""
print(f'{row["priceTotal"]:>9} {row["currency"]} for {row["nights"]} nights '
f'{row.get("rating") or "-":>3}/5 {row["title"]}{cheapest}')
if __name__ == "__main__":
main()