Agoda 在亚洲及全球列出了数百万家酒店和短租房源,每条搜索结果都携带着驱动价格追踪、旅行研究和竞争分析所需的精确结构化数据:酒店名称、每晚价格、宾客评分、位置,以及完整房源的链接。对于关注市场或构建旅行比价工具的人来说,这些公开房源数据就是原始素材,而手动跨结果页采集这些数据既慢又很快过时。
本指南将向您展示如何用 Python 以可靠的方式从 Agoda 抓取酒店数据。您将构建一个小型可运行的爬虫,通过 Crawling API 获取渲染后的 Agoda 搜索页面,处理 Agoda 基于滚动的加载方式使完整结果集出现,用 BeautifulSoup 解析所需字段,并将整洁的记录存储为 JSON。整个演示仅限于公开房源数据的范围,文末的合法性部分并非样板文字,请在将此方法用于任何实际规模前先行阅读。
您将构建什么
一个 Python 脚本,接收某城市的公开 Agoda 搜索 URL,通过 Crawling API 获取渲染后的结果页面,滚动页面使所有房源卡片加载完毕,并为每家酒店提取结构化记录。示例以吉隆坡的酒店为例,提取以下字段:
- 酒店名称:卡片上显示的房源名称。
- 价格:所选日期的每晚价格。
- 评分:体现质量和受欢迎程度的宾客评分。
- 位置:搜索所在的城市或地区。
- 链接:酒店房源页面的完整 URL。
为什么普通请求在 Agoda 上会失败
如果您使用基础 HTTP 客户端请求 Agoda 搜索 URL,会得到状态码 200 的响应,但正文中几乎没有任何房源信息。有两个因素会阻碍您。首先,Agoda 在浏览器中通过 JavaScript 构建搜索结果,只有在您滚动时才会加载更多房源卡片,因此初始 HTML 只是一个薄壳,在页面脚本运行并用户向下滚动后才会填充内容。解析第一次响应只能捕获少数几张卡片,而不是完整页面。其次,Agoda 能快速识别自动化流量:数据中心 IP 和不像真实浏览器的请求模式,在到达渲染内容之前就会被限速、IP 封锁或被挑战。
因此,一个可用的 Agoda 爬虫在单次请求中需要三件事:能渲染页面的浏览器、触发滚动以显示懒加载卡片的方式,以及让平台认为是真实访客的 IP。您可以自行组装无头浏览器、滚动例程和轮换住宅代理池,但维护这套技术栈才是大部分工作所在。Crawling API 将所有这些合并为一次调用:您发送带有 JavaScript 令牌和滚动选项的 URL,它在可信 IP 后面渲染并滚动页面,返回完整的 HTML 供您解析。
Crawlbase 提供两种令牌类型。普通令牌获取静态 HTML;JavaScript (JS) 令牌则先在真实浏览器中渲染页面。Agoda 在客户端填充搜索结果并在滚动时加载,因此此处需要 JS 令牌。普通令牌返回的结果与普通请求一样,只是一个薄壳,可解析的有用内容很少。
前提条件
开始编写代码前,您需要准备几样东西,每样都不需要很长时间。
基础 Python 知识。您应能编写和运行 Python 脚本,并使用 pip 安装包。如果您是解析方面的新手,BeautifulSoup 指南是本教程的好伴侣。
Python 3.8 或更高版本。使用 python --version 确认版本。如果尚未安装,请从 python.org 安装或通过 Anaconda 等发行版安装,并确保 Python 在您的 PATH 中。
Crawlbase 账户和 JS 令牌。注册后,打开控制台,从账户文档页面复制您的 JavaScript (JS) 令牌。Crawlbase 提供 1,000 次免费请求供起步使用,跟随本指南已完全足够。像对待密码一样保管令牌:它用于验证您的请求身份,请勿将其提交到版本控制系统。
项目设置
创建虚拟环境以隔离项目依赖,然后安装爬虫所需的库。
python --version python -m venv agoda_env source agoda_env/bin/activate pip install crawlbase beautifulsoup4
在 Windows 上,使用 agoda_env\Scripts\activate 代替 source 行来激活环境。两个依赖项各司其职:crawlbase 是 Crawling API 的官方客户端,beautifulsoup4 解析返回的 HTML 以便通过 CSS 选择器提取各字段。json 模块随标准库一同提供,因此导出步骤无需额外安装。
第一步:获取渲染后的 Agoda 页面
首先获取完整的页面。导入 CrawlingAPI 类,用您的 JS 令牌初始化,然后请求 Agoda 搜索 URL。Agoda 在滚动时加载卡片,因此需传入 scroll 和 scroll_interval 选项,让 API 在捕获页面前进行滚动。在解析前检查 Crawlbase 的 pc_status,可以让失败情况明显可见而不是悄悄忽略。
from crawlbase import CrawlingAPI api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"}) OPTIONS = { "scroll": "true", "scroll_interval": "20", } def fetch_agoda_page(page_url): response = api.get(page_url, OPTIONS) if response["headers"]["pc_status"] == "200": return response["body"].decode("utf-8") print(f"Request failed: {response['headers']['pc_status']}") return None if __name__ == "__main__": search_url = "https://www.agoda.com/search?city=14524&adults=2&rooms=1" html = fetch_agoda_page(search_url) print(html[:500] if html else "No HTML returned")
这两个滚动选项对于像 Agoda 这样的懒加载目标至关重要。scroll 告诉 API 滚动页面而不是立即捕获,scroll_interval 设置持续滚动的秒数,以便延迟渲染的卡片在捕获前出现。20 秒是文档记载的最大值,对于繁忙的结果页面来说是合理的起点。使用 python agoda_scraper.py 运行脚本,您应该能看到真实的 Agoda 搜索标记,而不是普通请求返回的空壳。这确认了在编写任何选择器之前渲染和滚动都能正常工作。
Agoda 需要在一次调用中获得经过滚动、位于可信 IP 后面的渲染页面,这正是上述 scroll 和 scroll_interval 选项所配置的。Crawling API 接受 JS 令牌,在真实浏览器中运行页面,滚动以触发懒加载卡片,在服务端轮换住宅 IP,并将完整的 HTML 交给您,让您无需自行运行无头浏览器群和代理池。先在免费套餐上指向公开搜索页面试试看。
第二步:检查页面并解析酒店卡片
在编写选择器之前,在浏览器中打开 Agoda 搜索页面,右键单击酒店卡片并选择"检查"来读取其 HTML 结构。在吉隆坡结果页面上,每个房源都位于一个属性卡片内,您可以定位到几个稳定的钩子:
-
酒店名称位于带有
data-selenium="hotel-name"属性的<h3>中。 -
价格位于带有
data-element-name="final-price"属性的<div>中。 -
评分位于带有
data-element-name="review-score"属性的<p>中。 -
房源链接是
a.PropertyCard__Link锚点的href。
将渲染后的 HTML 加载到 BeautifulSoup 中,遍历每张属性卡片并提取这四个字段。每次查找都有保护,确保某张卡片缺少某个字段时返回空字符串而不是导致运行崩溃;相对链接会拼接到 Agoda 域名上构成完整 URL。
from bs4 import BeautifulSoup CARD_SELECTOR = "div#contentContainer ol.hotel-list-container > li.PropertyCard" def text_of(card, selector): el = card.select_one(selector) return el.get_text(strip=True) if el else "" def extract_agoda_data(html, location): soup = BeautifulSoup(html, "html.parser") hotels = [] for card in soup.select(CARD_SELECTOR): link_el = card.select_one("a.PropertyCard__Link") href = link_el["href"] if link_el and link_el.get("href") else "" hotels.append({ "name": text_of(card, 'h3[data-selenium="hotel-name"]'), "price": text_of(card, 'div[data-element-name="final-price"]'), "rating": text_of(card, 'p[data-element-name="review-score"]'), "location": location, "link": f"https://www.agoda.com{href}" if href else "", }) return hotels
text_of 辅助函数查询卡片内的单个元素并返回其去空格文本,若元素缺失则返回空字符串,因此省略某字段的房源不会中断循环。卡片选择器从页面内容容器向下遍历至每个 li.PropertyCard,四个字段选择器直接来自 Agoda 的标记。对第一步中滚动后的 HTML 运行此函数,可为页面上的每家酒店得到一条记录。
Agoda 的 data-selenium 和 data-element-name 钩子以及其生成的类名会无预告变动。将此处的选择器视为起始模板而非固定约定。当列表返回空时,请在浏览器开发者工具中重新检查线上页面并更新选择器。定期维护选择器对任何生产级爬虫来说都是正常的,并不意味着出了什么问题。
第三步:组装完整脚本
现在将各部分整合成一个可运行的脚本:获取已滚动的搜索页面,解析每张酒店卡片,并将记录导出为 JSON 文件。
import json from crawlbase import CrawlingAPI from bs4 import BeautifulSoup api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"}) OPTIONS = { "scroll": "true", "scroll_interval": "20", } CARD_SELECTOR = "div#contentContainer ol.hotel-list-container > li.PropertyCard" def fetch_agoda_page(page_url): response = api.get(page_url, OPTIONS) if response["headers"]["pc_status"] == "200": return response["body"].decode("utf-8") print(f"Request failed: {response['headers']['pc_status']}") return None def text_of(card, selector): el = card.select_one(selector) return el.get_text(strip=True) if el else "" def extract_agoda_data(html, location): soup = BeautifulSoup(html, "html.parser") hotels = [] for card in soup.select(CARD_SELECTOR): link_el = card.select_one("a.PropertyCard__Link") href = link_el["href"] if link_el and link_el.get("href") else "" hotels.append({ "name": text_of(card, 'h3[data-selenium="hotel-name"]'), "price": text_of(card, 'div[data-element-name="final-price"]'), "rating": text_of(card, 'p[data-element-name="review-score"]'), "location": location, "link": f"https://www.agoda.com{href}" if href else "", }) return hotels def save_to_json(data, filename="agoda_hotels.json"): with open(filename, "w") as f: json.dump(data, f, indent=2) print(f"Saved {len(data)} hotels to {filename}") def main(): location = "Kuala Lumpur" search_url = "https://www.agoda.com/search?city=14524&adults=2&rooms=1" html = fetch_agoda_page(search_url) if html: hotels = extract_agoda_data(html, location) save_to_json(hotels) if __name__ == "__main__": main()
脚本获取已滚动的搜索页面,将每张属性卡片解析为一条记录,并以两格缩进将列表写入 agoda_hotels.json。city=14524 参数是 Agoda 对吉隆坡的内部 ID;将其替换为其他城市 ID,并添加您自己的入住、退房和入住人数参数,即可将爬虫指向不同市场。由于位置标签是传入的而非解析的,每条记录都带有您搜索的城市名称。
输出示例
使用 python agoda_scraper.py 运行完整脚本,您将得到每家酒店一条整洁的结构化记录,可直接用于分析、数据库或电子表格。价格和评分值以 Agoda 在卡片上显示的格式呈现。
[ { "name": "Summer Suites KLCC By Castle Classy", "price": "USD34", "rating": "8.4", "location": "Kuala Lumpur", "link": "https://www.agoda.com/summer-suites-klcc-by-castle-classy/hotel/kuala-lumpur-my.html" }, { "name": "Riveria City Kuala Lumpur by Guestonic", "price": "USD20", "rating": "9.2", "location": "Kuala Lumpur", "link": "https://www.agoda.com/riveria-city-kuala-lumpur-by-guestonic/hotel/kuala-lumpur-my.html" } ]
如果您希望在电子表格中处理相同的数据,可以使用第一条记录的键作为表头,将 save_to_json 替换为 Python 的 csv.DictWriter。记录结构保持不变,可直接传入 pandas 按价格区间筛选或按评分排序。
跨城市扩展与保持不被封锁
上面的单个搜索页面只是基础构件。要覆盖更多市场,可以遍历 Agoda 城市 ID 列表,为每个城市调用 fetch_agoda_page,并为每批数据标记其位置。Agoda 使用基于滚动的加载而非分页,因此 scroll_interval 控制您能捕获多少结果集;在卡片持续加载的密集城市页面上,可向最大值方向调高。
即使渲染和滚动都已处理,Agoda 仍会监控具有爬虫特征的流量。以下几个习惯可以让长时间运行保持健康,它们适用于任何高难度的商业目标。
- 控制请求速率。在紧密循环中获取大量城市页面是触发限速或被挑战的最快方式。在请求之间添加短暂停顿,交替不同目标,而不是全速爬取同一条路径。
- 依赖轮换。住宅 IP 池将请求分散到许多真实用户地址,不会让任何单一地址触发限速。Crawling API 为您处理这一切;如果您自建技术栈,这是最需要做对的部分。
-
读取状态码。当运行开始返回非 200 的
pc_status值时,说明当前速率或 IP 层级已不再足够。将其视为退让的信号,而不是可以忽略的噪声。
对于更大规模的抓取,异步 Crawler 可以将请求排队并通过 webhook 传递结果,适合在不保持长连接的情况下运行多个城市页面。更广泛的策略请参阅如何在不被封锁的情况下抓取网站和爬取 JavaScript 网站指南。如果您想跨平台比较酒店数据,同样的方法也适用于抓取 Expedia、TripAdvisor 和Google 酒店。
抓取 Agoda 是否合法?
抓取 Agoda 是否被允许,取决于 Agoda 的服务条款、您所在的司法管辖区以及您对数据的使用方式。Agoda 的条款限制自动访问和批量数据采集,因此无论您的工具多么谨慎,抓取都可能违反这些条款。本文中的代码并不会改变这一点,只是让技术层面的工作能够运行。请阅读 Agoda 的服务条款及其 robots.txt,将请求量控制在不给服务器造成压力的合理水平,并将两份文件都视为您采集内容的边界。网络抓取处于法律灰色地带,最稳妥的立场是只采集公开可见的内容,并严格遵守合理的速率限制。
几条值得坚守的底线。只采集公开房源字段:酒店名称、每晚价格、宾客评分、位置,以及任何人无需账号即可看到的房源链接。避免涉及可识别个人的任何内容,包括评论中附带的宾客姓名或任何房东或宾客联系方式,这些属于个人数据,超出了公开房源的范围。一旦个人数据进入画面,GDPR 和 CCPA 等隐私法律即适用,因此请将数据集限于事实性的房产信息。不要再发布 Agoda 的受版权保护内容(如照片或完整评论文本),也不要尝试访问登录墙、已保存的预订或账号后的任何内容。
本指南有意将范围限定在公开搜索结果,因为这是让工作具有可辩护性的边界。如果您的项目需要更多内容,或您正在酒店数据上构建商业产品,正确的做法是签订许可协议:Agoda 为获批准的使用场景运营官方合作伙伴和联盟计划,并提供经授权使用的 API,这才是批量或生产用途的正确路径,而不是设计更精巧的爬虫。
核心要点
- Agoda 是客户端渲染且基于滚动加载的。普通请求只返回带有少数卡片的薄壳,因此必须先渲染并滚动页面再进行解析。
-
渲染、滚动和可信 IP 需要同时具备。带有 JS 令牌的 Crawling API 在一次调用中完成三者;
scroll和scroll_interval控制结果集加载的量。 -
定位 Agoda 的稳定钩子。从
data-selenium="hotel-name"读取酒店名称,从data-element-name="final-price"读取价格,从data-element-name="review-score"读取评分,从a.PropertyCard__Link读取链接。 - 导出结构化记录。将属性卡片循环提取为名称、价格、评分、位置和链接,然后写入 JSON(或 CSV)进行分析。
- 坚守公开数据范围。遵守 Agoda 的服务条款和 robots.txt,将个人数据和受版权保护的媒体排除在数据集外,商业或批量使用请通过 Agoda 的官方合作伙伴 API。
常见问题
为什么普通请求只返回少数几家 Agoda 酒店?
因为 Agoda 通过 JavaScript 在客户端构建搜索结果,只有在滚动时才会加载更多房源卡片。初始 HTML 是一个外壳,在页面脚本运行并页面滚动后才会填充内容,因此原始 HTTP 请求返回状态码 200 但大多数卡片缺失。要获得完整页面,必须先渲染并滚动,这正是 Crawling API 的 JS 令牌和 scroll 选项为您处理的事情。
Agoda 需要普通令牌还是 JS 令牌?
JS 令牌。普通令牌获取静态 HTML,而 Agoda 上的静态 HTML 与普通请求返回的薄壳相同。JS 令牌在真实浏览器中渲染页面,并在设置了 scroll 选项后,在返回 HTML 前先进行滚动,因此当 BeautifulSoup 解析时房源卡片已经存在。
我可以从 Agoda 房源中抓取哪些数据?
公开房源字段:酒店名称、每晚价格、宾客评分、位置,以及房源链接。只取任何访客无需账号即可看到的数据,避免评论中绑定宾客姓名或任何房东或宾客联系方式,这些属于个人数据,超出了本指南涵盖的公开房源范围。
如何抓取不同城市的酒店?
Agoda 在搜索 URL 中使用数字型 city 参数来标识每个城市(例如,吉隆坡为 city=14524)。在浏览器中运行搜索并从 URL 中读取该 ID,然后将其与您自己的入住、退房和入住人数参数一起替换到 search_url 中。要覆盖多个城市,可以遍历 ID 列表并为每个城市调用 fetch_agoda_page。
我的选择器返回空字符串,发生了什么变化?
几乎可以肯定是 Agoda 的标记发生了变化。其 data-selenium 和 data-element-name 钩子以及生成的类名会无预告变动,因此上个月有效的选择器可能已失效。请在浏览器开发者工具中重新检查线上页面并更新选择器。定期维护选择器对任何生产级爬虫来说都是正常的。
如何处理 Agoda 上的 CAPTCHA 和封锁?
Agoda 对自动化流量使用 CAPTCHA 和其他反爬虫技术。Crawling API 通过基于浏览器的渲染和服务端 IP 轮换为您处理这些问题,因此大多数挑战在 HTML 到达您的代码之前就已被处理。此外,控制请求速率,交替不同目标,并在开始看到非 200 的 pc_status 值时退让而非强行重试。
大规模爬取任何站点,无需与基础设施对抗。
Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。
