Booking.com 抓取:把可用性当作日历
把可用性建模为按日期为键的日历,房间与价格作为维度;记录每次页面所投影的查询,并为每个单元格保留五种状态。
BotBrowser Team
抓取 Booking.com 的可用性不是文档抽取问题,因为可用性页面不是文档。它是查询的投影:你询问的日期、入住人数、房型与价格决定了页面上究竟会出现哪些日历单元格。BotBrowser 文档说明了地理对齐的出口与由配置文件驱动的区域设置取值,因此销售点比较可以声明它所代表的区域与语言。它不提供可用性数据、不持有库存、不预订房间,也不保证同一查询两次返回同一份日历。存在官方合作方或联盟数据源时,应优先使用。
摘要
- 可用性页面是其查询的投影。请记录查询,否则之后无法解释这个页面。
- 日历以日期为键,而不是以页面为键。三晚住宿是三行日期,退房日期是排他的。
- 房型与价格是行上的维度,不是各自的日历。同一日期的两种价格是两个单元格,而不是同一单元格的两次观察。
- 每个单元格用五种状态:观察到可订、观察到关闭、不在窗口内、响应中缺失、未被接受。缺失的单元格不等于房间关闭。
- 存在官方合作方或联盟数据源时优先使用,并以平台条款作为可采集范围的权威依据。
目录
把可用性当作日历
住宿搜索回答的是关于一段日期范围的问题。范围是数据源定价的单位,而单独一晚是日历可以设定键的单位。把这一关系搞错是可用性工作中最常见的建模错误,因为一个写着「412 起」的三晚页面看起来像一个值,实际上它是三行可能各自不同可用性的汇总。
退房日期是排他的。2026-12-10 到 2026-12-13 的住宿占用 10 日、11 日和 12 日三晚,客人 13 日退房。把退房日当作第四晚会让每次住宿的日历都偏移一天,而这个错误直到有人比较同一房型的两晚与三晚预订时才会暴露。
可用性也不是整个酒店的属性。酒店可能有空房,而某个具体房型已售罄;也可能某个房型可用,而含早餐的价格不可用。因此日历在单元格有意义之前至少需要两个维度。
键与维度
键是日期加上被观察的对象。维度描述这次观察是如何被限定的。把它们分开,才能比较两次抓取而不悄悄混入房型、价格计划或销售点。
| 部分 | 内容 | 为什么不能并入页面级的值 |
|---|---|---|
date_key | 该晚的 ISO 日期,按酒店时区 | 两晚是两次观察,不是一次 |
room_code | 数据源命名的房型或单元 | 套房售罄不代表酒店关闭 |
rate_code | 数据源命名的价格计划 | 价格关闭不代表房型关闭 |
occupancy | 查询请求的成人与儿童人数 | 入住人数改变可提供的库存 |
(date, room, rate, occupancy) 这样的键足以比较同一对象的两次观察。只用 (date) 会把套房与标准间并成一个单元格,得到的序列会在最便宜类别变化时显示房间「重新开放」。用 (page) 作键会把三晚并成一个值,并失去说明哪一晚发生变化的能力。
酒店时区也应进入键,但作为日历的已声明属性,而不是单独维度。一晚是本地概念,跨夏令时切换而没有记录时区的抓取会产生两行看起来像重复一晚的记录。
入住人数值得单独说明,因为它改变的是存在哪些库存,而不只是如何定价。两人查询可能返回四人查询看不到的房型,同一间实体房间也可能以两个不同价格代码作为两个不同报价出现。把入住人数与房型、价格一起记录,后来的比较才能判断变化来自库存还是来自所问的问题。
最短入住规则是最安静的维度。酒店可能要求周末至少两晚,因此单晚查询什么都不返回,而房间在连住两晚时确实可用。把这次单晚查询记为 observed_closed 的日历,把一条政策结果记成了可用性事实。页面声明最短入住时,把它记为维度或备注;没有声明时,把零结果的单晚视为一次带声明查询的观察,而不是关闭。
货币与销售点与票价四项输入并列,而不进入日历键。同一价格代码可以存在于多种货币与多种销售点规则下,金额会不同而可用性不变。把货币与销售点留在记录里,价格比较就不会被误当成可用性比较,货币切换也不会看起来像房间重新开放。
页面从查询中投影出什么
MDN 的 URL API 文档把 URL 描述为解析并暴露地址各组成部分(包括搜索参数)的接口。这正是本问题真正需要的浏览器表面:渲染出来的可用性页面由其地址中的参数选出,因此地址是关于页面的证据,而不是围绕它的装饰。
两个不同地址可以描述同一段住宿。日期范围可以写成两个参数、一个范围参数或路径片段,查询还可以附加不影响结果的跟踪参数。因此归一化查询是让两次抓取可比的第一步,而且必须发生在取页之前而不是之后。
// Constructed fixture: the same stay written two ways, then a changed check-in.
const raw = [
'https://example.test/hotel/alpha?checkin=2026-12-10&checkout=2026-12-13&adults=2&tracking=abc',
'https://example.test/hotel/alpha?stay=2026-12-10_2026-12-13&adults=2',
'https://example.test/hotel/alpha?checkin=2026-12-11&checkout=2026-12-14&adults=2',
];
const KEEP = ['checkin', 'checkout', 'adults', 'children', 'room', 'rate'];
const lines = raw.map(input => {
const url = new URL(input);
const stay = url.searchParams.get('stay');
const params = new Map();
if (stay) {
const [checkin, checkout] = stay.split('_');
params.set('checkin', checkin);
params.set('checkout', checkout);
}
for (const key of KEEP) {
const value = url.searchParams.get(key);
if (value) params.set(key, value);
}
const canonical = [...params.entries()]
.sort(([a], [b]) => a.localeCompare(b))
.map(([k, v]) => `${k}=${v}`)
.join('&');
return `canonical=${canonical} key=${params.get('checkin')}..${params.get('checkout')}`;
});
console.log(lines.join('\n'));
下面的构造输出显示前两个地址归并为同一条规范查询,而第三个因为日期不同而保持独立。
canonical=adults=2&checkin=2026-12-10&checkout=2026-12-13 key=2026-12-10..2026-12-13
canonical=adults=2&checkin=2026-12-10&checkout=2026-12-13 key=2026-12-10..2026-12-13
canonical=adults=2&checkin=2026-12-11&checkout=2026-12-14 key=2026-12-11..2026-12-14
跟踪参数在前两行消失,这正是重点:只差一个活动标签的两次抓取描述同一段住宿,应作为一条比较。第三行保留自己的键,因为入住日平移是另一个问题,即使晚数相同。
三条归一化规则让这一行为可靠。按名称排序保留的参数,使规范形式不依赖站点恰好使用的顺序。删除值不影响结果的参数,并保留一份明确的会影响结果的参数清单,让新的跟踪参数无法悄悄创建新序列。只用一种方式表示日期范围,把范围参数转换成两个显式日期,使站点的编码选择不会渗入键。
还有一条归一化器跨不过去的边界。两个看起来不同的地址可能通过重定向描述同一段住宿,两个看起来相同的地址也可能因为 cookie 或账户状态而解析出不同结果。因此规范查询是关于「请求了什么」的陈述,而不是关于「渲染了什么」的保证。把规范查询与重定向后的最终地址都记录下来,并把两者的分歧当作发现而不是噪声。
每个单元格的五种状态
可用性工作最常出错的地方,是「不可用」与「未被观察」之间的边界。可用/关闭的二元模型迫使每个缺失单元格落入这两个答案之一,而对于查询从未问到的单元格,两种选择都是错的。
| 状态 | 观察到了什么 | 它能支持什么 |
|---|---|---|
observed_available | 查询为该日期、房型与价格返回了报价 | 该单元格在那次查询中当时可用 |
observed_closed | 查询返回了明确的售罄或关闭信号 | 该单元格在那次查询中当时关闭 |
not_in_window | 查询窗口不包含该日期 | 关于该单元格什么都没有;窗口是有界的 |
absent_in_window | 窗口覆盖该日期,但响应未列出该单元格 | 关于该单元格什么都没有;响应漏掉了它 |
unaccepted | 没有可用响应:验证、错误、超时、解析失败 | 关于该单元格什么都没有;观察失败 |
两种缺失状态让序列免于凭空发明关闭,而且它们是不同的失败。如果抓取只请求三晚,第四晚是 not_in_window:在有界窗口之外,不是关闭。如果窗口覆盖了该日期,但响应没有列出单元格,那是 absent_in_window:在窗口之内、未被列出,同样不是关闭。把任一种记为关闭,都会让酒店看起来失去了它从未报告过的可用性。
unaccepted 状态防止相反的错误。验证页、500 与解析失败都产生零个单元格,把它们当作空可用性,会把基础设施问题变成供给观察。请把它们留在自己的状态里,让后来的读者看到日历究竟被观察了多少。
MDN 的 Performance API 文档描述了用于测量时间与资源加载的浏览器接口。它在这里只与「被接受的页面」和「很慢的页面」之间的边界有关:在日历渲染之前就放弃的抓取必须记为 unaccepted,而不是记为没有房间的酒店。把页面稳定所需的时间与状态一起记录,能让这个区分可审计。
还有一种情况不属于这五种状态,也不应并入:日历被观察到了,但这次观察属于与序列声称不同的范围。带企业价格代码、会员登录或促销活动的抓取可能显示公开序列看不到的可用性,混在一起会产生看起来可比而实际不可比的行。把范围与查询一起记录,并在范围变化时开始新序列。
五种状态还需要修订规则。周一观察到可用、周二观察到关闭的单元格是一次转变,而不是矛盾,记录应保留两行各自的观察时间,而不是覆盖第一行。覆盖会丢掉转变,也无法区分真实变化与更正,而这正是后来复核者在序列受到质疑时需要的区分。
最后,保持观察粒度的诚实。页面可能渲染一个月视图,其中只有部分日期带单元格;把整月记为已观察的抓取就过度声称了。记录响应中实际存在哪些日期,即使答案是「响应列出了三十一天中的三天」,把这些日期记为 absent_in_window 而不是关闭。
把查询与观察一起记录
可用性抓取记录必须携带查询,因为没有查询就无法解释页面。下面的字段是让两次抓取可比的最低要求。
| 字段 | 内容 | 为什么在记录里 |
|---|---|---|
canonical_query | 上一步归一化后的查询 | 定义所请求的是哪份日历 |
property_ref | 数据源范围内的酒店标识 | 把两个同名酒店分开 |
date_key | 该晚的 ISO 日期 | 每晚一行 |
room_code | 数据源命名的房型 | 把套房与标准间分开 |
rate_code | 数据源命名的价格计划 | 把灵活价与预付价分开 |
state | 五种状态之一 | 防止把缺失读成关闭 |
observed_at | 抓取时间(UTC) | 排序观察并界定运行范围 |
settle_ms | 页面稳定耗时(若测量) | 区分慢页面与缺失页面 |
有两个字段值得说明。property_ref 应当是数据源范围内的标识而不是名称,因为同一城市的两家酒店可能同名,连锁也可能给其中一家改名。settle_ms 是可选项但很有用:在 40 毫秒后就记录状态的抓取,很可能读到的是骨架而不是日历,这在信任该行之前值得知道。
票价抓取模型 把同一套纪律用于单张票价而不是日历:四项输入按每次抓取记录,因为缺少它们的比较测量的是环境。可用性需要这一套再加上日期键,因为一段住宿横跨多行,而每行可以独立变化。
条款边界
预订页面位于一个平台之后,平台的条款决定可以采集什么。这不是更好的浏览器配置能消除的技术限制;它是数据可以被使用的条件本身。存在官方合作方数据源、联盟接口或许可分发渠道时,它们以已定义的 schema 和已声明的使用权提供同样的可用性。
RFC 9110 定义了 HTTP 语义,包括缓存与 Vary 头,后者告诉缓存哪些请求字段影响了表示。这正是页面用来声明其内容依赖的不只是路径的机制,也提醒我们可用性响应是协商出来的而不是固定的。它不说明某个站点是否允许采集,也没有任何头能让条款问题消失。
房产挂牌模型 为房产数据划了同一条线:挂牌出现、实体房产与报价是三件事,卡片缺失不是成交。可用性有同样的形状,只是用日期范围代替房产,规则也一样:观察不是结论。
由此有两个实践边界。第一,观察不是预订:加载日历并记录它显示了什么,与占住一间房是完全不同的行为,后者完全不在范围内。第二,两次抓取之间的差异是一个观察,不是平台违反政策的证据。那些结论属于持有库存与协议的一方。
常见问题
三晚住宿是一个可用性值吗?
不是。它是三行日期,每行可以带不同状态。汇总这段住宿的页面展示的是投影,因此汇总必须先拆开才能逐晚比较。
缺失的日历单元格意味着房间不可用吗?
不是。要么窗口没有覆盖该日期(not_in_window),要么覆盖了但响应没有列出单元格(absent_in_window),要么响应未被接受(unaccepted)。请把这三者都与 observed_closed 分开,使任何一个都不会被读成关闭。
房型与价格属于键吗?
它们与日期一起属于键,因为套房售罄不代表酒店关闭,价格关闭也不代表房型关闭。把它们并入一个页面级的值,会合并本应分开的观察。
BotBrowser 提供可用性数据或订房吗?
不提供。它提供有文档的浏览器输入,包括地理对齐的出口与由配置文件驱动的区域设置取值,让比较可以声明自己的区域。它不提供可用性、不持有库存、不预订房间,也不保证结果。
来源
实践结论
把可用性建模为以日期为键的日历,房间、价格与入住人数作为维度,并把退房日期视为排他。取页之前先归一化查询,并在每次抓取中记录它,因为页面是这次查询的投影,而不是独立成立的文档。为每个单元格保留五种状态,让缺失与观察失败永远不会变成关闭。存在官方合作方或联盟数据源时使用它,并让平台条款决定浏览器运行可以采集什么。
本文属于指南: 代理配置总览:浏览器网络决策图