Google Hotels 是大多数旅行者在预订前比较各家住宿的地方:价格、评分、照片和房态并排呈现在同一处。对任何从事价格监控、竞品分析或旅行需求建模的人来说,那个界面都是一个丰富的结构化数据来源。问题在于,Google Hotels 在客户端渲染它的列表,并对自动化流量进行严密防御,所以一个普通 HTTP 请求交给你的是一个近乎空白的页面,而不是你想要的酒店。
本指南将向你展示如何以可靠的方式用 Python 抓取 Google Hotels。你会构建一个小巧、可运行的抓取脚本,它通过 Crawling API 获取已渲染的结果,用 BeautifulSoup 解析它们,并逐家酒店提取:名称、价格、评分和链接。整个演示始终限定在公开的结果数据范围内,文末附近的合法性章节并非样板话,所以在把它指向任何真实规模的目标之前请先读它。
你将构建什么
一个 Python 脚本,它接收一个 Google Hotels 搜索 URL,通过 Crawling API 取回已渲染的 HTML,并为页面上的每家酒店提取一条结构化记录。我们以一次城市搜索作为贯穿示例,提取以下字段:
- 名称 卡片上显示的酒店名称。
- 价格 所选日期的每晚价格。
- 评分 平均住客评分,当展示了某个评分时。
- 链接 指向该酒店在 Google 上结果的 URL。
为什么普通获取在 Google Hotels 上会失败
用一个裸 HTTP 客户端请求一个 Google Hotels 搜索 URL,你会得到一个状态为 200 的响应,而正文里几乎没有列表数据。有两股力量在跟你作对。第一,Google Hotels 用 JavaScript 在浏览器里构建它的结果卡片,所以初始 HTML 只是一个外壳,要等页面脚本运行之后才会填充。第二,Google 会很快标记自动化流量:数据中心 IP 以及看起来不像真实浏览器的请求模式,还没触及渲染好的内容就会被挑战或封禁。
因此,一个能用的 Google Hotels 抓取器在一次请求里需要两样东西:一个真正渲染页面的浏览器,以及一个被平台视为真实访客的 IP。你可以用一个无头浏览器加一池轮换的住宅代理自己搭建这一切,但把它们拼起来并保持健康才是大部分工作量所在。Crawling API 把两者收进单次调用:你把 URL 连同一个 JavaScript token 发给它,它在受信任的 IP 背后渲染页面,并返回成品 HTML 供你解析。
Crawlbase 提供两种 token 类型。普通 token 获取静态 HTML;JavaScript(JS)token 会先在一个真实浏览器里渲染页面。Google Hotels 是客户端渲染的,所以这里你需要 JS token。使用普通 token 返回的是与普通获取相同的空壳,里面没有什么可解析的。
前置条件
在写任何代码之前,你需要准备好几样东西。它们都不会花太久。
基本的 Python。 你应当能自如地编写并运行一个 Python 脚本,并用 pip 安装包。如果你刚接触这门语言,官方 Python 文档和任何入门课程都能把你带到本教程所假定的水平。
Python 3.8 或更高版本。 用 python --version 确认你的版本。如果没有,请从 python.org 或像 Anaconda 这样的发行版安装它。
一个 Crawlbase 账户和 JS token。 注册、打开你的仪表板,并从账户文档页复制你的 JavaScript(JS)token。新账户包含免费请求,无需绑卡。把 token 当作密码对待:它用于验证你的请求身份,所以请别把它放进版本控制里。
搭建项目
创建一个虚拟环境以便让项目依赖保持隔离,然后安装抓取器所需的两个库。
python --version python -m venv hotels_env source hotels_env/bin/activate pip install crawlbase beautifulsoup4
在 Windows 上,用 hotels_env\Scripts\activate 而不是 source 那一行来激活环境。两个依赖完成全部工作:crawlbase 是 Crawling API 的官方客户端,而 beautifulsoup4 解析返回的 HTML,让你能按 CSS 选择器提取各个字段。
第 1 步:获取已渲染的结果
先取到成品页面。导入 CrawlingAPI 类,用你的 JS token 初始化它,并请求搜索 URL。在解析之前读取状态码能让失败响亮地暴露出来,而不是悄无声息,这在一个会限流的目标上很重要。
from crawlbase import CrawlingAPI api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_TOKEN"}) def crawl(page_url): options = {"ajax_wait": "true", "page_wait": 5000} response = api.get(page_url, options) status = response["headers"].get("pc_status") if status == "200": return response["body"].decode("utf-8") print(f"Request failed: {status}") return None if __name__ == "__main__": page_url = "https://www.google.com/travel/search?q=hotels+in+New+York¤cy=USD" html = crawl(page_url) print(html[:500] if html else "No HTML returned")
这两个等待选项对一个像这样的客户端渲染目标很重要。ajax_wait 告诉 API 等待异步内容加载完成,而 page_wait 在加载后保持固定的毫秒数,让那些迟渲染的卡片在页面被捕获之前出现。五秒是一个合理的起点;如果结果列表回来时偏短或为空,就把它调高。注意 pc_status 请求头:那是 Crawlbase 自身对底层请求的状态,当你判断一次获取是否成功时,要相信这个值,而不是外层的 HTTP 码。运行脚本,你应当看到真实的列表标记,而不是普通获取返回的那个空壳。
Google Hotels 需要一个在受信任 IP 背后渲染好的页面,一次调用搞定。Crawling API 接收一个 JS token,在一个真实浏览器里运行页面,在服务端通过住宅 IP 轮换,并把成品 HTML 交给你,所以你无需自己运行一支无头舰队和一个代理池。先在免费套餐上把它指向一个公开搜索。
第 2 步:用 BeautifulSoup 解析酒店卡片
手里有了已渲染的 HTML,就把它加载进 BeautifulSoup,并从每张结果卡片里取出每家酒店。Google 把列表布局为重复的卡片元素,所以你找出所有卡片,然后从每张里读取名称、价格、评分和链接。给每个字段加上防护,这样一个缺失的评分或价格就不会让整次运行崩溃。
from bs4 import BeautifulSoup def parse_hotels(html): soup = BeautifulSoup(html, "html.parser") hotels = [] for card in soup.find_all("div", class_="BcKagd"): name = card.find("h2", class_="BgYkof") price = card.find("span", class_="qQOQpe prxS3d") rating = card.find("span", class_="KFi5wf lA0BZ") link = card.find("a", class_="PVOOXe") hotels.append({ "name": name.text.strip() if name else None, "price": price.text.strip() if price else None, "rating": rating.text.strip() if rating else None, "link": "https://www.google.com" + link["href"] if link else None, }) return hotels
每个 if name else None 防护做的是同一件事:当某个元素缺失时返回 None,而不是在对着空值的 .text 调用上抛错。这能在某张卡片缺少价格或还没有评分时让提取保持稳健,而这在一个结果页里很常见。链接是从锚点的 href 读取并加上 Google 主机名前缀的,因为该标记使用相对路径。
Google 的类名(BcKagd、BgYkof、qQOQpe prxS3d 等等)是混淆过的,会不经通知地变化。把上面的选择器当作一个起始模板,而非一份契约。当某个字段在每张卡片上都返回 None 时,在你的浏览器开发者工具里重新检查实时页面,并更新选择器。对任何生产级抓取器来说,定期的选择器维护都属正常,不是出了什么问题的迹象。
第 3 步:把它组装起来
现在把抓取和解析接成一个可运行的脚本。获取已渲染的 HTML,把它交给解析器,并把结构化记录写入一个 JSON 文件。
import json from crawlbase import CrawlingAPI from bs4 import BeautifulSoup api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_TOKEN"}) def crawl(page_url): options = {"ajax_wait": "true", "page_wait": 5000} response = api.get(page_url, options) status = response["headers"].get("pc_status") if status == "200": return response["body"].decode("utf-8") print(f"Request failed: {status}") return None def parse_hotels(html): soup = BeautifulSoup(html, "html.parser") hotels = [] for card in soup.find_all("div", class_="BcKagd"): name = card.find("h2", class_="BgYkof") price = card.find("span", class_="qQOQpe prxS3d") rating = card.find("span", class_="KFi5wf lA0BZ") link = card.find("a", class_="PVOOXe") hotels.append({ "name": name.text.strip() if name else None, "price": price.text.strip() if price else None, "rating": rating.text.strip() if rating else None, "link": "https://www.google.com" + link["href"] if link else None, }) return hotels def main(): page_url = "https://www.google.com/travel/search?q=hotels+in+New+York¤cy=USD" html = crawl(page_url) if not html: return hotels = parse_hotels(html) with open("google_hotels.json", "w", encoding="utf-8") as f: json.dump(hotels, f, ensure_ascii=False, indent=2) print(f"Saved {len(hotels)} hotels to google_hotels.json") if __name__ == "__main__": main()
输出长什么样
运行完整脚本,你会得到一份干净的结构化记录列表,可以写入 CSV、写入数据库,或喂进一个价格追踪任务。
[ { "name": "The One Boutique Hotel", "price": "$90", "rating": "3.3", "link": "https://www.google.com/travel/search?q=New+York&..." }, { "name": "Ly New York Hotel", "price": "$153", "rating": "4.4", "link": "https://www.google.com/travel/search?q=New+York&..." } ]
加载更多结果
一个搜索页只展示有限的一片酒店;Google 把其余的藏在一个“更多结果”按钮后面,它会就地加载更多卡片。Crawling API 可以替你驱动那次交互。为按钮传入一个 css_click_selector 并设置 ajax_wait,这样 API 就会点击、等待新卡片渲染,然后返回扩展后的 HTML。同一个 parse_hotels 函数无需任何改动就能读取更长的页面。
def crawl_expanded(page_url, click_selector): options = { "ajax_wait": "true", "page_wait": 5000, "css_click_selector": click_selector, } response = api.get(page_url, options) if response["headers"].get("pc_status") == "200": return response["body"].decode("utf-8") return None
那个按钮的选择器本身就是一个 Google 会轮换的混淆值,所以在依赖它之前先检查实时页面并确认它。把展开次数控制得克制一些。每次点击都是又一次针对一个敏感目标的渲染请求,把许多次堆在一个紧凑的循环里,恰恰是会让一次运行被限流的模式。
保持不被封锁
即便渲染已被处理好,Google 监视抓取器形态流量的力度也比几乎任何其他目标都狠。几个习惯能让一次运行保持健康,它们适用于任何咄咄逼人的商业站点。
- 给你的请求设定节奏。 在一个紧凑的循环里猛攻搜索,是招来一次挑战的最快方式。把请求分散开来,并变换你的查询,而不要全速爬一个地点。
- 依靠轮换。 一池住宅 IP 会把请求分散到许多真实用户地址上,这样就没有任何单个地址会触发速率限制。Crawling API 替你处理这件事;如果你自己搭建技术栈,这就是要做好的那一部分。
-
读懂状态码。 一次运行开始返回一个非 200 的
pc_status或可见的挑战标记,就是在告诉你当前的速率或 IP 等级已经不够了。把它当作后退的信号,而不是可忽略的噪声。
关于更宽泛的策略手册,参阅抓取 Google 时如何绕过验证码,以及抓取 Google 搜索结果时如何轮换代理。如果你更想把自己的流量通过一池轮换代理路由,而不是使用托管的 API,Smart AI Proxy(也叫 AI Proxy)以一个即插即用的代理端点为你提供同样的住宅 IP 轮换。关于挑战处理的更深入探讨,网页抓取时如何绕过验证码这篇指南涵盖了一般情形。
抓取 Google Hotels 合法吗?
抓取 Google Hotels 是否被允许,取决于 Google 的服务条款、你所在的司法辖区,以及你拿数据做什么。Google 的条款限制自动化访问,所以无论你的工具有多谨慎,抓取都可能违背这些条款。这里的任何代码都改变不了这一点;它只是把技术部分做通而已。请阅读 Google 服务条款以及旅行界面的 robots.txt,并把两者都当作你采集内容的边界。
有几条值得守住的底线。只采集公开的结果数据:任何人不登录就能在搜索里看到的酒店名称、价格、评分和链接。尊重 Google 所声明的速率预期,并把你的请求量保持得足够低,以免给它的服务器造成压力。不要抓取任何登录后面的内容、任何针对已登录账户个性化的内容,或任何由认证把守的数据,也绝不要尝试绕过认证去触及它。避开个人数据,包括任何超出公开列出范围、与可识别个人相关联的东西。
本指南刻意限定在公开搜索结果,因为那是让这项工作站得住脚的那条线。只用公开结果,合理的量。如果你的项目需要超出公开界面的内容,正确的路径是一个有授权的旅行数据源或一份官方合作关系,而不是一个更聪明的抓取器。
核心要点
- Google Hotels 是客户端渲染的。 普通获取返回一个空壳,所以你必须先渲染页面再解析它。
-
你需要渲染和受信任的 IP 一起到位。 带 JS token 的 Crawling API 一次调用就把两件事都做了;
ajax_wait和page_wait控制它等待内容多久。 - BeautifulSoup 负责提取。 遍历结果卡片,从每张里读取名称、价格、评分和链接,每个字段都加上防护,并预期那些混淆过的选择器会漂移。
-
设定节奏并轮换。 Google 限流很狠,所以分散请求、轮换住宅 IP,并读取
pc_status请求头以判断何时后退。 - 坚守公开数据。 尊重 Google 的 ToS 和 robots.txt,只采集公开结果,把量保持得合理,并绝不触碰登录墙后面的、个性化的或已认证的数据。
常见问题
为什么普通获取从 Google Hotels 返回不了任何酒店?
因为 Google Hotels 用 JavaScript 在客户端构建它的结果卡片。初始 HTML 只是一个外壳,要等页面脚本在浏览器里运行之后才会填充,所以一个原始 HTTP 请求返回的是状态 200,而列表为空。要拿到真实数据,你必须先渲染页面,而这正是 Crawling API 的 JS token 替你处理的事。
对于 Google Hotels,我需要普通 token 还是 JS token?
JS token。普通 token 获取静态 HTML,在 Google Hotels 上那是与普通获取相同的空壳。JS token 会先在一个真实浏览器里渲染页面再交回 HTML,所以当 BeautifulSoup 解析时酒店卡片就在那里。
我的选择器在每张卡片上都返回 None。是什么变了?
几乎可以肯定是 Google 的标记。卡片上的类名(BcKagd、BgYkof 等等)是混淆过的,会不经通知地轮换,所以上个月还能用的选择器可能就坏了。在你的浏览器开发者工具里重新检查一个实时结果页,并更新选择器。对任何生产级抓取器来说,定期的选择器维护都属正常。
我该如何获取超过一页的结果?
Google 把额外的酒店藏在一个“更多结果”按钮后面,而不是经典的分页。把那个按钮的选择器用 css_click_selector 传给 Crawling API,并设置 ajax_wait,这样 API 就会点击它、等待新卡片渲染,并返回扩展后的 HTML。把展开次数控制得克制一些,以免招来一次挑战。
抓取 Google Hotels 时我该如何避免被封?
把你的每 IP 请求速率保持得低,变换你的查询而不是循环一个地点,并通过轮换的住宅 IP 路由,这样就没有任何单个地址会触发速率限制。Crawling API 替你管理轮换和一个受信任的 IP 池;如果你搭建自己的技术栈,Smart AI Proxy 以一个即插即用的端点为你提供那种轮换。留意 pc_status 请求头,并在你开始看到非 200 响应时后退。
我可以从 Google Hotels 抓取预订或账户数据吗?
不行,而且本指南也不涵盖它。任何与已登录账户、预订流程或个性化定价相关联的东西都坐落在认证后面,所以它不是公开结果数据。抓取登录墙后面的或个性化的内容,或绕过认证去触及它,都属于本文范围之外,并违背 Google 的条款。坚守公开的搜索界面,并把量保持得合理。
大规模爬取任何站点,无需与基础设施对抗。
Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。
