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。
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:它用于验证你的请求,不要将其提交到版本控制系统。
项目设置
创建项目文件夹,初始化并安装爬虫所需的两个库。
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。在解析之前检查状态码,可以让失败情况明显暴露而不是悄然无声。
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 中房源卡片已经存在。
第一次请求就返回了完整渲染的 Booking.com 搜索页面,你这边无需无头浏览器,也无需代理。Crawling API 在真实浏览器中运行页面,在服务端轮换住宅 IP,并处理 Booking.com 对爬虫发出的 CAPTCHA 验证,因此你只需一次调用就能拿到完整的 HTML。先在免费套餐上指向一个公开酒店搜索,然后再添加解析器。
第二步:用 Cheerio 解析每张房源卡片
拿到渲染后的 HTML,将其加载到 Cheerio 中并遍历房源卡片。Booking.com 将每条房源包裹在标有 data-testid="property-card" 的元素中,因此你选取每张卡片,然后使用页面自身的 data-testid 属性从中读取名称、位置、评分、价格和房源链接。对每个字段进行防御性读取,可以防止一个缺失值导致整次运行崩溃。
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 保存到文件,但将获取和解析合并到单次运行中可以减少活动部件。
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 文件为每个房源保存一个对象,各自包含名称、价格、公开评分、位置和房源链接。下面的值为示例,你的实际运行结果将反映你搜索日期的实时数据。
[ { "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 或任何能读取分隔符文件的数据管道。
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 获取每页,用同一个函数解析,并在某页不返回卡片时停止。由于每个结果页面共享相同的卡片结构,你已经写好的解析器无需修改就能跨所有页面使用。
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 次请求免费,无需信用卡。
