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 供你解析。

为什么用 JS token

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 当作密码对待:它用于验证你的请求身份,所以请别把它放进版本控制里。

搭建项目

创建一个虚拟环境以便让项目依赖保持隔离,然后安装抓取器所需的两个库。

bash
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。在解析之前读取状态码能让失败响亮地暴露出来,而不是悄无声息,这在一个会限流的目标上很重要。

python
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&currency=USD"
    html = crawl(page_url)
    print(html[:500] if html else "No HTML returned")

这两个等待选项对一个像这样的客户端渲染目标很重要。ajax_wait 告诉 API 等待异步内容加载完成,而 page_wait 在加载后保持固定的毫秒数,让那些迟渲染的卡片在页面被捕获之前出现。五秒是一个合理的起点;如果结果列表回来时偏短或为空,就把它调高。注意 pc_status 请求头:那是 Crawlbase 自身对底层请求的状态,当你判断一次获取是否成功时,要相信这个值,而不是外层的 HTTP 码。运行脚本,你应当看到真实的列表标记,而不是普通获取返回的那个空壳。

Crawlbase Crawling API

Google Hotels 需要一个在受信任 IP 背后渲染好的页面,一次调用搞定。Crawling API 接收一个 JS token,在一个真实浏览器里运行页面,在服务端通过住宅 IP 轮换,并把成品 HTML 交给你,所以你无需自己运行一支无头舰队和一个代理池。先在免费套餐上把它指向一个公开搜索。

第 2 步:用 BeautifulSoup 解析酒店卡片

手里有了已渲染的 HTML,就把它加载进 BeautifulSoup,并从每张结果卡片里取出每家酒店。Google 把列表布局为重复的卡片元素,所以你找出所有卡片,然后从每张里读取名称、价格、评分和链接。给每个字段加上防护,这样一个缺失的评分或价格就不会让整次运行崩溃。

python
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 的类名(BcKagdBgYkofqQOQpe prxS3d 等等)是混淆过的,会不经通知地变化。把上面的选择器当作一个起始模板,而非一份契约。当某个字段在每张卡片上都返回 None 时,在你的浏览器开发者工具里重新检查实时页面,并更新选择器。对任何生产级抓取器来说,定期的选择器维护都属正常,不是出了什么问题的迹象。

第 3 步:把它组装起来

现在把抓取和解析接成一个可运行的脚本。获取已渲染的 HTML,把它交给解析器,并把结构化记录写入一个 JSON 文件。

python
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&currency=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、写入数据库,或喂进一个价格追踪任务。

json
[
  {
    "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 函数无需任何改动就能读取更长的页面。

python
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_waitpage_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 的标记。卡片上的类名(BcKagdBgYkof 等等)是混淆过的,会不经通知地轮换,所以上个月还能用的选择器可能就坏了。在你的浏览器开发者工具里重新检查一个实时结果页,并更新选择器。对任何生产级抓取器来说,定期的选择器维护都属正常。

我该如何获取超过一页的结果?

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 次请求免费,无需信用卡。

自助开通 · 无需销售通话 · 提供企业级爬取量