Booking.com 是互联网上最大的旅行市场之一,其公开搜索结果页面承载着大量结构化信号:酒店和房源名称、每晚价格、宾客评分、所在街区,以及每条房源的链接。价格情报团队用它来对标各城市的房价,研究人员按目的地研究供需关系,旅行规划者则并排比较不同房源。所有这些信息都以可预测的卡片布局呈现在公开搜索页面上。

本指南介绍如何用 JavaScript 和 Node.js 结合 Cheerio 抓取 Booking.com。你将构建一个小型可运行爬虫,通过 Crawling API 获取 Booking.com 公开搜索结果页面,解析每张卡片的房源名称、价格、评分、位置和房源链接,并将结果导出为 JSON 和 CSV。整个教程仅涉及公开酒店房源数据,靠近结尾的合法性章节不是样板文字,在你将此爬虫指向任何真实量级流量之前,请先阅读。

你将构建的内容

一个 Node.js 脚本,接收 Booking.com 公开搜索结果 URL,通过 Crawling API 获取渲染后的 HTML,并为页面上的每张房源卡片提取一条结构化记录。我们以旧金山酒店搜索作为贯穿全文的示例,并为每条房源抽取以下字段:

  • Name(名称)卡片上显示的酒店或房源名称,例如 "Hotel Zephyr San Francisco"。
  • Price(价格)显示的每晚价格,例如 "US$592"。
  • Rating(评分)Booking.com 为该房源显示的公开宾客评分(如有)。
  • Location(位置)房源所在的街区和城市。
  • Link(链接)指向该房源独立页面的 URL。

为什么普通请求在 Booking.com 上会失败

如果你用裸 HTTP 客户端请求 Booking.com 搜索 URL,很少能拿到房源卡片。两个因素对你不利。首先,Booking.com 在浏览器中通过 JavaScript 渲染结果列表,因此初始 HTML 几乎是空壳,直到页面脚本运行后才会填充内容。其次,Booking.com 会监控自动化流量:数据中心 IP 和不像真实浏览器的请求模式,会在到达渲染后的房源之前遭遇 CAPTCHA 验证、限流或封锁。

因此,一个能正常工作的爬虫需要在单次请求中同时具备两点:能够实际渲染页面的浏览器,以及平台认为是真实访客的 IP。你可以自己用无头浏览器加上轮换住宅代理池来实现,但维护这套架构才是大部分工作所在。Crawling API 将这两者合并为一次调用:你发送 URL,它在可信 IP 后面渲染页面,返回可供你用 Cheerio 解析的完整 HTML。

使用 JavaScript token

Crawling API 提供两种 token:普通 token 和 JavaScript token。Booking.com 需要在真实浏览器中渲染页面,因此本指南中的每次请求都请使用你的 JavaScript token。普通 token 返回未渲染的外壳,你的选择器将一无所获。

前置条件

在编写任何代码之前,你需要准备好以下几件事。每件都不会花太长时间。

基础 JavaScript 和 Node.js 知识。你应该熟悉编写和运行 Node 脚本、使用 npm 安装包,以及处理异步代码。更完整的教程请参阅我们的用 Node.js 构建网络爬虫指南,涵盖基础知识。

Node.js 16 或更高版本。使用 node --version 确认你的版本。如果没有,请从 Node.js 官网安装,或通过 nvm 等版本管理器安装。

Crawlbase 账号和 token。注册后,打开控制台,从账号文档页面复制你的 JavaScript token。免费套餐提供 1,000 次请求,无需信用卡,且仅对成功的请求收费。请像对待密码一样保管好 token:它用于验证你的请求,不要将其提交到版本控制系统。

项目设置

创建项目文件夹,初始化并安装爬虫所需的两个库。

bash
node --version

mkdir booking-scraper && cd booking-scraper
npm init -y

npm install crawlbase cheerio

两个依赖各司其职:crawlbase 是 Crawling API 的官方 Node 客户端,cheerio 以 jQuery 风格的 API 解析返回的 HTML,让你能通过 CSS 选择器提取各个字段。在该文件夹中创建名为 scraper.js 的文件,并添加以下各步骤的代码。

第一步:获取渲染后的搜索页面

从获取完整页面开始。导入 CrawlingAPI 类,用你的 JavaScript token 初始化它,并请求 Booking.com 公开搜索结果 URL。在解析之前检查状态码,可以让失败情况明显暴露而不是悄然无声。

javascript
const { CrawlingAPI } = require('crawlbase');

const api = new CrawlingAPI({ token: 'YOUR_CRAWLBASE_TOKEN' });

const bookingPageURL =
  'https://www.booking.com/searchresults.html?ss=San+Francisco&checkin=2025-12-25&checkout=2025-12-31&group_adults=2&no_rooms=1&group_children=0&selected_currency=USD';

api
  .get(bookingPageURL)
  .then((response) => {
    if (response.statusCode === 200) {
      console.log(response.body.slice(0, 500));
    }
  })
  .catch((error) => console.error('API request error:', error));

node scraper.js 运行脚本,你应该在正文顶部看到真实的 Booking.com 房源标记,而不是精简外壳。这确认了渲染在你编写任何选择器之前就已经正常工作。Crawling API 使用你提供的 JavaScript token 在真实浏览器中渲染页面,因此你拿到的 HTML 中房源卡片已经存在。

Crawlbase Booking.com Scraper

第一次请求就返回了完整渲染的 Booking.com 搜索页面,你这边无需无头浏览器,也无需代理。Crawling API 在真实浏览器中运行页面,在服务端轮换住宅 IP,并处理 Booking.com 对爬虫发出的 CAPTCHA 验证,因此你只需一次调用就能拿到完整的 HTML。先在免费套餐上指向一个公开酒店搜索,然后再添加解析器。

第二步:用 Cheerio 解析每张房源卡片

拿到渲染后的 HTML,将其加载到 Cheerio 中并遍历房源卡片。Booking.com 将每条房源包裹在标有 data-testid="property-card" 的元素中,因此你选取每张卡片,然后使用页面自身的 data-testid 属性从中读取名称、位置、评分、价格和房源链接。对每个字段进行防御性读取,可以防止一个缺失值导致整次运行崩溃。

javascript
const cheerio = require('cheerio');

function parseDataFromHTML(html) {
  const $ = cheerio.load(html);
  const properties = [];

  const cardSelector = '[data-testid="property-card"]';

  $(cardSelector).each((_, card) => {
    const currentCard = $(card);
    const extractText = (selector) =>
      currentCard.find(selector).text().trim();

    const name = extractText('[data-testid="title"]');
    const location = extractText('[data-testid="address"]');
    const rating = extractText(
      '[data-testid="review-score"] div.a3b8729ab1.d86cee9b25',
    );
    const price = extractText(
      'span[data-testid="price-and-discounted-price"]',
    );
    const link = currentCard
      .find('[data-testid="title-link"]')
      .attr('href');

    if (name) {
      properties.push({
        name,
        price: price || 'Price not available',
        rating: rating || 'Rating not available',
        location,
        link: link || '',
      });
    }
  });

  return properties;
}

以下几个细节让这段代码忠实于页面结构。每条房源位于 [data-testid="property-card"] 元素中。卡片内,名称来自 [data-testid="title"],位置来自 [data-testid="address"],评分来自 [data-testid="review-score"] 内的评分块,价格来自 span[data-testid="price-and-discounted-price"],房源 URL 来自 [data-testid="title-link"] 锚元素的 href。优先使用稳定的 data-testid 钩子,仅在必要时才回退到生成的类名,可以让解析器在标记变化时更加稳健。

选择器会漂移

Booking.com 生成的类名(上面 a3b8729ab1 风格的后缀)会在无预告的情况下变化,甚至 data-testid 钩子偶尔也会被重命名。请将选择器视为起始模板,而非约定。当某个字段返回空时,在浏览器开发者工具中重新检查实际页面并更新选择器。定期维护选择器对任何生产爬虫来说都是正常的事,不代表哪里出了问题。

第三步:组装完整脚本并导出 JSON 和 CSV

现在将获取和解析两个步骤串联成一个可运行的脚本,然后将记录写入磁盘,同时保存为 JSON 和 CSV。旧版指南在步骤间将原始 HTML 保存到文件,但将获取和解析合并到单次运行中可以减少活动部件。

javascript
const fs = require('fs');
const { CrawlingAPI } = require('crawlbase');
const cheerio = require('cheerio');

const api = new CrawlingAPI({ token: 'YOUR_CRAWLBASE_TOKEN' });

async function crawl(pageUrl) {
  const response = await api.get(pageUrl);
  if (response.statusCode === 200) return response.body;
  console.error(`Request failed: ${response.statusCode}`);
  return null;
}

function toCsv(rows) {
  const headers = ['name', 'price', 'rating', 'location', 'link'];
  const escape = (value) =>
    `"${String(value).replace(/"/g, '""')}"`;
  const lines = [headers.join(',')];
  for (const row of rows) {
    lines.push(headers.map((h) => escape(row[h])).join(','));
  }
  return lines.join('\n');
}

async function main() {
  const url =
    'https://www.booking.com/searchresults.html?ss=San+Francisco&checkin=2025-12-25&checkout=2025-12-31&group_adults=2&no_rooms=1&group_children=0&selected_currency=USD';
  const html = await crawl(url);
  if (!html) return;

  const properties = parseDataFromHTML(html);
  fs.writeFileSync('booking.json', JSON.stringify(properties, null, 2));
  fs.writeFileSync('booking.csv', toCsv(properties));
  console.log(`Saved ${properties.length} properties to JSON and CSV`);
}

main();

将第二步中的 parseDataFromHTML 函数粘贴到同一个文件中,使 main 可以调用它。用 node scraper.js 运行,你将得到两个文件:booking.json 包含完整的结构化记录,booking.csv 可直接在电子表格中打开。toCsv 辅助函数会对每个字段加引号,并将内嵌的引号双写,这一点在这里很重要,因为房源名称和街区标签中频繁包含逗号。

输出结果示例

JSON 文件为每个房源保存一个对象,各自包含名称、价格、公开评分、位置和房源链接。下面的值为示例,你的实际运行结果将反映你搜索日期的实时数据。

json
[
  {
    "name": "Hotel Zephyr San Francisco",
    "price": "US$592",
    "rating": "8.3",
    "location": "Fisherman's Wharf, San Francisco",
    "link": "https://www.booking.com/hotel/us/zephyr-san-francisco.html"
  },
  {
    "name": "Club Quarters Hotel Embarcadero, San Francisco",
    "price": "US$554",
    "rating": "8.0",
    "location": "Financial District, San Francisco",
    "link": "https://www.booking.com/hotel/us/club-quarters-san-francisco.html"
  }
]

CSV 以相同的房源行反映了同样的数据,附有表头行,可直接导入 Excel、Google Sheets 或任何能读取分隔符文件的数据管道。

csv
name,price,rating,location,link
"Hotel Zephyr San Francisco","US$592","8.3","Fisherman's Wharf, San Francisco","https://www.booking.com/hotel/us/zephyr-san-francisco.html"
"Club Quarters Hotel Embarcadero, San Francisco","US$554","8.0","Financial District, San Francisco","https://www.booking.com/hotel/us/club-quarters-san-francisco.html"

处理翻页

单页搜索只是演示;真正的任务需要拉取所有结果页面。Booking.com 通过搜索 URL 上的 offset 参数进行分页,每页跳过固定数量的房源,因此你可以遍历偏移量,通过 Crawling API 获取每页,用同一个函数解析,并在某页不返回卡片时停止。由于每个结果页面共享相同的卡片结构,你已经写好的解析器无需修改就能跨所有页面使用。

javascript
async function scrapeAllPages(baseUrl, maxPages) {
  const allProperties = [];
  const perPage = 25;

  for (let page = 0; page < maxPages; page++) {
    // Booking.com skips results with an offset parameter
    const pageUrl = `${baseUrl}&offset=${page * perPage}`;
    const html = await crawl(pageUrl);
    if (!html) break;

    const properties = parseDataFromHTML(html);
    if (properties.length === 0) break; // no more results

    allProperties.push(...properties);
    console.log(`Page ${page + 1}: ${properties.length} properties`);

    // Pace requests so you stay under the rate limit
    await new Promise((r) => setTimeout(r, 2000));
  }

  return allProperties;
}

确切的页面大小和参数可能会变化,因此在浏览器中查看几个真实的"下一页"链接,并匹配其模式。重要的习惯可以迁移到任何目标:循环直到结果耗尽,并在请求之间加入短暂延迟,避免高频访问网站。关于像这样的渲染密集型 JavaScript 页面的更多信息,请参阅我们的抓取 JavaScript 网站指南。

保持不被封锁

即使渲染问题已经解决,Booking.com 仍会监测爬虫特征的流量。以下几个习惯能让运行保持健康,适用于任何难度较高的商业目标。

  • 控制请求节奏。在页面请求之间引入延迟,而不是在紧密循环中高频搜索。分散请求是保持在 Booking.com 速率限制之下最重要的单一因素。
  • 依赖 IP 轮换。住宅 IP 池将请求分散到众多真实用户地址,确保没有单一地址触发限制或 CAPTCHA。Crawling API 为你处理这一切;如果你自己搭建方案,这是最需要做好的部分。
  • 读懂状态码。当运行开始返回验证挑战或非 200 响应时,说明当前速率或 IP 层级已不再足够。将其视为需要回退的信号,而不是可以忽略的噪声。

更宏观的操作手册请参阅如何抓取网站而不被封锁。如果你想从其他旅行平台获取类似的房源数据,同样的获取再解析模式可以直接迁移到抓取 Airbnb 价格用 JavaScript 抓取 Expedia。要跨网站比较这些价格,我们的网络抓取与价格情报指南介绍了分析层面的内容。

抓取 Booking.com 是否合法?

抓取 Booking.com 是否被允许,取决于 Booking.com 的服务条款、你所在的司法管辖区,以及你对数据的使用方式。Booking.com 的条款限制了自动化访问,因此无论你的技术手段多么谨慎,抓取行为都可能与这些条款相抵触。这里的任何代码都不会改变这一点,它只是让技术层面的事情能够运作。请阅读 Booking.com 的服务条款及其 robots.txt,遵守其中说明的任何速率预期,并将两者视为你采集内容的边界。

本指南刻意将范围限定在公开酒店房源数据:任何人无需登录即可在搜索页面上看到的房源名称、显示价格、汇总评分、街区和房源链接。这是关于房产的事实性商业列表信息,不属于个人数据。其他内容超出范围:个人宾客评论及其撰写者属于个人数据,登录或预订流程后的任何内容不在讨论范围,房源照片和描述是受版权保护的材料,不应整体再分发。如果个人数据确实进入了你的采集范围,隐私法(如 GDPR 和 CCPA)将适用,因此请将采集内容限于上述汇总的、非个人的字段。

如果你的项目需要超出公开房源范围的内容,正确的途径是合规渠道,而不是更聪明的爬虫。Booking.com 有官方合作伙伴计划,包括 Demand API,在明确的条款下提供房产、可用性和定价数据,附有归因规则和明确的商业权利。当你需要大量数据、保证结构或商业再利用权利时,这些才是正确的工具。当你不确定某种用途是否被允许时,请获取许可或签署数据协议,而不是假设沉默即默许。

回顾

核心要点

  • Booking.com 在客户端渲染房源,且封锁力度很强。普通请求返回空外壳或 CAPTCHA,因此你必须使用 JavaScript token 在可信 IP 后面渲染页面,才能解析。
  • Crawling API 一次调用完成所有工作。它在真实浏览器中渲染页面,轮换住宅 IP,处理 CAPTCHA,返回可用 Cheerio 解析的完整 HTML。
  • Cheerio 提取字段。选取每个 [data-testid="property-card"],然后读取名称、价格、评分、位置和房源链接,并预期生成的类名会发生漂移。
  • 翻页和导出。遍历 Booking.com 的 offset 参数直到结果耗尽,控制请求节奏,将结构化记录写入 JSON 和 CSV。
  • 坚守公开数据。只采集公开酒店房源,将个人宾客评论及评论者视为个人数据,遵守服务条款和 robots.txt,商业或批量用途使用 Booking.com 官方合作伙伴 API。

常见问题

我可以用 JavaScript 以外的语言构建 Booking.com 爬虫吗?

可以。本指南使用 JavaScript 和 Cheerio,但同样的方法适用于任何语言。Crawling API 为多种语言提供了库和 SDK,因此你以同样的方式获取渲染后的 HTML,再用你的技术栈偏好的 HTML 解析器(如 Python 中的 BeautifulSoup)解析。选择器和字段保持不变,只有解析语法会变。

为什么普通请求从 Booking.com 返回不完整的数据?

因为 Booking.com 通过 JavaScript 在客户端渲染房源列表,并以 CAPTCHA 挑战自动化流量。来自数据中心 IP 的原始 HTTP 请求通常返回空外壳或封锁页面,而不是房源卡片。要获取完整页面,你必须在可信 IP 后面渲染它,这正是当你使用 JavaScript token 时 Crawling API 为你处理的事情。

我的选择器返回空值。发生了什么变化?

几乎可以肯定是 Booking.com 的标记发生了变化。像 a3b8729ab1 这样的生成类名会在无预告的情况下改变,甚至 data-testid 钩子偶尔也会被重命名,因此上个月还能用的选择器可能就此失效。在浏览器开发者工具中重新检查实际页面,更新 parseDataFromHTML 中的选择器,即可恢复正常。定期维护选择器对任何生产爬虫来说都是正常的事。

抓取 Booking.com 时会被封锁吗?

如果你从单个地址发送过多过快的请求,是有可能的。Crawling API 通过轮换住宅 IP 和处理 CAPTCHA 降低了这种风险,但你仍应控制请求节奏,在页面间加入延迟,并监控状态码,以便在出现验证挑战时及时回退。这些习惯在任何难度较高的商业目标上都很重要。

我可以抓取个人宾客评论和评论者姓名吗?

这超出了本指南的范围,且有充分的理由。个人评论及其撰写者属于个人数据,会引入隐私法(如 GDPR 和 CCPA)的适用。将汇总的宾客评分作为房源质量的信号,不要建立个人评论者的档案,也不要将与其身份挂钩的某人评论再次发布。对于超出公开房源范围的任何内容,请使用 Booking.com 的官方合作伙伴计划。

Booking.com 是否提供官方 API?

是的。Booking.com 有官方合作伙伴计划,包括 Demand API,在明确的条款下提供房产、可用性和定价数据,附有归因规则和明确的商业权利。如果你需要大量数据、保证结构或商业再利用权利,合规渠道才是正确选择。这个公开数据爬虫最适合研究、原型开发和较小规模分析,即不需要官方协议的场景。

开始构建

大规模爬取任何站点,无需与基础设施对抗。

Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。

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