返回博客
指南
Raluca PenciucLast updated on May 8, 20263 min read

网络抓取 Booking.com:酒店、价格和评论(2026 年指南)

网络抓取 Booking.com:酒店、价格和评论(2026 年指南)
简而言之:本指南将详细介绍如何使用 Python 端到端地抓取 Booking.com 的数据:提取搜索列表、酒店页面、每晚价格以及客人评论。您将获得两种互补的方法:一种是针对 JavaScript 渲染页面的 Selenium Wire 工作流,另一种是更快捷的方案,该方案直接调用 Booking.com 的内部 /dml/graphql 端点,此外还包含反屏蔽策略、货币处理以及针对约 1,000 条结果分页限制的解决方法。

Booking.com 正是旅游和酒店业团队反复使用的数据集:实时每晚房价、竞争对手定位、各区域房源供应情况、各酒店的客人评价。问题在于,这些数据均未通过面向公众的开放 API 提供,因此若想通过编程获取,最终只能自行对 Booking.com 进行某种形式的网页抓取。本教程展示了两种实用的 Python 实现路径,并结合了通常在项目第二周就会困扰开发者的生产环境注意事项。

截至本文撰写之时,Booking.com 是网络上最大的住宿预订平台之一,涵盖酒店、度假村及短期住宿等数百万家可预订住宿。(我们将具体房源数量保持在近似值范围内;该公司的公开数据常有波动。)该平台高度依赖 JavaScript 驱动,并配备了真正的反机器人防御机制,因此简单的 requests.get 脚本往往在发挥作用前就已失败。

你将学习如何运行基于 Selenium 的搜索结果抓取工具,如何通过反向工程从内部 GraphQL 接口提取相同数据,如何获取酒店详情页、价格及评论,以及如何利用站点地图和查询分区突破结果限制。代码基于 Python 3.10+,并假设你已熟悉开发者工具和 CSS 选择器。

为何值得花精力抓取 Booking.com

在某些应用场景中,抓取 Booking.com 的数据几乎能立竿见影地带来回报。房价情报团队可实时比较竞争对手酒店的每晚价格;收益经理通过追踪房源可用性和折扣模式来把握促销时机;市场研究和旅游分析团队则利用评论数量、评分及设施覆盖率来评估目的地。此外,任何开发元搜索或 AI 旅行代理的人,都需要结构化的酒店数据——而这些数据在公开网站上仅通过 JavaScript 呈现。

在本指南中,我们将提取五种具体的实体类型:搜索结果列表(查询页面上的酒店卡片)、酒店详情页(描述、地址、设施、地理位置)、每晚价格和房源情况、客人评论,以及基于网站地图的酒店库存(用于批量检索)。每种数据都有其独特之处,而将它们混合起来,才能为您提供真正的数据集,而非仅仅是一张搜索结果页面的截图。

选择抓取方案:浏览器自动化 vs 隐藏 API

针对 Booking.com 进行任何规模的网页抓取,有两种合理的方法,它们是互补而非相互竞争的。

Selenium配合Selenium Wire驱动真实的Chrome实例,执行页面的JavaScript代码,并允许您读取渲染后的DOM。当您尚不清楚页面隐藏的请求时,这是阻力最小的选项,且由于查询的是用户所见的相同DOM,因此能很好地容忍布局漂移。其代价是速度和资源消耗:每个页面都需要占用一个完整的浏览器标签页。对于精心筛选的几千家酒店列表而言,这尚可接受。 但用于持续监控时,成本会变得很高。

调用内部 /dml/graphql 端点并使用 httpx 则完全绕过了浏览器。Booking.com 自身的前端正是通过此端点获取搜索结果,因此只要复制该请求格式,你就能获得与网站相同的 JSON 数据,速度比 Selenium 快 10 到 50 倍,且内存占用极小。其代价是稳定性较差:请求体和必需的头部信息会发生变化,你必须保持它们的同步。

一个稳妥的默认方案:先用 Selenium 进行原型开发,在理解数据后锁定 GraphQL 请求,并在生产环境中使用该 API 路径。

配置 Python 环境

请在全新的虚拟环境中使用 Python 3.10 或更高版本,以确保依赖项相互隔离:

mkdir booking_scraper && cd booking_scraper
python -m venv .venv && source .venv/bin/activate
pip install selenium selenium-wire webdriver-manager httpx parsel
touch app.py

selenium-wireselenium 该库会暴露底层网络请求,这正是我们进行分页同步所必需的。 webdriver-manager 会自动下载匹配的 chromedriver 二进制文件,因此您无需在不同机器上费心管理驱动程序版本。 httpx 为方法 2 提供了一个支持 HTTP/2 的客户端,而 parsel 并提供 Scrapy 风格的 CSS 和 XPath 选择器用于解析酒店 HTML。如果您之前从未使用 Selenium 进行过爬取,我们的分步 Selenium 教程将是一个有用的入门指南。

方法 1:使用 Selenium 和 Selenium Wire 抓取搜索结果

这是进入 Booking.com 网页抓取的最友好切入点:在真实的 Chrome 会话中打开搜索 URL,让 JavaScript 渲染房源卡片,然后遍历 DOM。我们使用 Selenium Wire 而不是原生 Selenium,因为搜索页面是通过后台 XHR/fetch 调用加载结果的。Selenium Wire 允许我们检查这些单独的请求,并等待特定响应实际返回,这对避免竞争条件的分页至关重要。

加载搜索页面并提取房源卡片

请务必在 URL 中明确包含入住和退房日期。若未包含,Booking.com 将回退到默认房源,导致您的价格列与用户在实际预订窗口中看到的显示不一致。

from seleniumwire import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.common.by import By

driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
url = ('https://www.booking.com/searchresults.html'
       '?ss=London&checkin=2026-05-10&checkout=2026-05-12&group_adults=2')
driver.get(url)

cards = driver.find_elements(By.CSS_SELECTOR, "div[data-testid='property-card']")
print(f'Found {len(cards)} property cards on page 1')

Booking.com 在其结果卡片上使用 data-testid 属性,这使得它们比自动生成的类名更稳定,更易于定位。

提取名称、地址、评分、评论数、价格和图片

每张房源卡片都包含相同的几个 data-testid 钩子,因此每张卡片解析器主要是一个小型选择器字典。在此场景下通常应选用 CSS 选择器(简洁且高效),但当需要遍历父节点或兄弟节点时,XPath 也是可行的。若您正在抉择,请参阅我们的《XPath 与 CSS 选择器指南》

def parse_card(card):
    def text(sel):
        nodes = card.find_elements(By.CSS_SELECTOR, sel)
        return nodes[0].text.strip() if nodes else None

    def attr(sel, name):
        nodes = card.find_elements(By.CSS_SELECTOR, sel)
        return nodes[0].get_attribute(name) if nodes else None

    score_block = text("div[data-testid='review-score']") or ''
    score_lines = [s.strip() for s in score_block.split('\n') if s.strip()]
    score = score_lines[0] if score_lines else None
    review_count = next((l for l in score_lines if 'review' in l.lower()), None)

    return {
        'name':         text("div[data-testid='title']"),
        'url':          attr("a[data-testid='title-link']", 'href'),
        'address':      text("span[data-testid='address']"),
        'score':        score,
        'review_count': review_count,
        'price':        text("span[data-testid='price-and-discounted-price']"),
        'image':        attr("img[data-testid='image']", 'src'),
    }

listings = [parse_card(c) for c in cards]

关于价格有两点需要特别注意。首先,Booking.com 上的 review-score Booking.com 上的块元素将数值评分和评论数量文本合并为一个元素,因此我们将其拆分为多行并分别提取。其次,从搜索卡片中抓取的价格几乎总是不含税费;包含所有费用的总价只有在您进入预订流程的后续步骤后才会显示。请将其视为标价,而非最终费用,并在后续流程中注明这一点。

避免竞争条件下的分页点击

每次点击“下一页”控件都会触发一个 POST 请求 /dml/graphql 并等待 JSON 响应返回。若点击后立即抓取 DOM,读取到的将是上一页的数据。Selenium Wire 通过允许你阻塞在实际响应上解决了此问题。

from selenium.webdriver.common.by import By

def total_pages(driver):
    nums = driver.find_elements(By.CSS_SELECTOR, "div[data-testid='pagination'] li")
    return max((int(n.text) for n in nums if n.text.isdigit()), default=1)

pages = total_pages(driver)
all_listings = [parse_card(c) for c in cards]

for page in range(2, pages + 1):
    del driver.requests  # clear so the next wait does not match an old response
    next_btn = driver.find_element(
        By.CSS_SELECTOR, "button[aria-label='Next page']")
    next_btn.click()
    driver.wait_for_request(r'/dml/graphql', timeout=10)
    cards = driver.find_elements(
        By.CSS_SELECTOR, "div[data-testid='property-card']")
    all_listings.extend(parse_card(c) for c in cards)

del driver.requests 是关键的一行代码。若缺少它, wait_for_request 系统会误将前一页的 GraphQL 请求视为有效,导致在新数据到达前就跳转。请从分页控件中获取总页数而非硬编码;繁忙的查询可能分页至二十页,而闲置的查询可能仅分两页。

方法 2:直接调用 Booking.com 的 GraphQL 搜索接口

一旦 Selenium 向你表明搜索页面由 /dml/graphql,更快捷的做法是直接调用该端点并跳过浏览器。这正是 Booking.com 网页抓取真正实现可扩展性的关键。

发现过程与处理任何 [隐藏的 JavaScript API] 时相同:打开开发者工具(F12),切换至“网络”标签页,按 Fetch/XHR 过滤,随后触发一次真实搜索并点击进入第二页。你会看到一个 POST 请求发送到 /dml/graphql ,其中包含一个带有 operationName,一个 variables 对象(包含目的地、日期、入住人数以及一个 offset),以及一个 queryextensions 字段用于固定查询哈希。右键单击该请求并选择“复制为 cURL”,这就是您的起点。

在发布前,请根据您自己的开发者工具捕获结果重新核对确切的字段名称;Booking.com 会定期重命名 GraphQL 操作,而最安全的参考依据是当前前端发送的数据。

import httpx

ENDPOINT = 'https://www.booking.com/dml/graphql'
HEADERS = {
    'content-type':    'application/json',
    'origin':          'https://www.booking.com',
    'referer':         'https://www.booking.com/searchresults.html',
    'user-agent':      'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                       'AppleWebKit/537.36 (KHTML, like Gecko) '
                       'Chrome/124.0 Safari/537.36',
    'accept-language': 'en-US,en;q=0.9',
}

def search_page(client, payload, offset):
    body = {**payload}
    body['variables']['input']['pagination'] = {'offset': offset, 'rowsPerPage': 25}
    r = client.post(ENDPOINT, json=body, headers=HEADERS, timeout=30)
    r.raise_for_status()
    return r.json()

def search_all(payload, max_results=1000):
    results = []
    with httpx.Client(http2=True) as client:
        for offset in range(0, max_results, 25):
            page = search_page(client, payload, offset)
            hits = (page.get('data', {})
                        .get('searchQueries', {})
                        .get('search', {})
                        .get('results', []))
            if not hits:
                break
            results.extend(hits)
    return results

有两个细节容易让人踩坑。该端点每次调用返回 25 条结果,由一个偏移量变量控制,该变量以 25 条结果为增量进行调整。此外,请求必须看起来像是来自网站本身: origin 以及 referer 并设置为 booking.com, content-type: application/json,并设置 accept-language 与您的 IP 区域相符的。若移除这些标头,您将在数次请求内收到通用 400 错误或遭遇软封锁。请使用 HTTP/2(当您传入 http2=True时,httpx 会自动支持此协议),因为 Booking.com 的边缘服务器似乎会识别仅协商 HTTP/1.1 的客户端。

抓取单个酒店页面以获取描述、地址和设施信息

搜索结果卡片仅是抓取 Booking.com 数据的一小部分;它们仅提供名称和价格,却无法获取旅行团队真正需要的丰富酒店详情。为此,请直接抓取酒店 URL。酒店页面大多由服务器渲染,因此一个简单的 GET 请求加上 parsel 即可,无需浏览器。

import httpx
from parsel import Selector

def scrape_hotel(url):
    html = httpx.get(url, headers=HEADERS, http2=True, follow_redirects=True).text
    sel = Selector(text=html)
    map_link = sel.css("a[data-atlas-latlng]::attr(data-atlas-latlng)").get('')
    lat, lng = (map_link.split(',') + [None, None])[:2]
    return {
        'name':        sel.css('h2.pp-header__title::text').get(default='').strip(),
        'description': ' '.join(sel.css("div[data-testid='property-description'] *::text").getall()).strip(),
        'address':     sel.css("span[data-testid='address']::text").get(default='').strip(),
        'lat':         lat,
        'lng':         lng,
        'amenities':   [a.strip() for a in sel.css("div[data-testid='facility-list-most-popular'] li::text").getall() if a.strip()],
    }

经纬度通常嵌入在地图链接的 data-atlas-latlng 属性中,这比从内联脚本中解析出来更可靠。设施被分组到功能块中;若希望按类别展示而非扁平化,请遍历这些组。

获取每晚价格和房态

每晚价格并不包含在酒店 HTML 中;它通过一个独立的 GraphQL 查询获取,该查询会返回日历形式的响应。以与搜索调用相同的方式捕获请求:在开发者工具中打开酒店页面,更改日期,并监听发送到 /dml/graphql。请求主体包含酒店标识符(数字 hotel_id、国家代码和货币) 以及日期范围。

酒店页面还在 HTML 中嵌入了 CSRF 风格的令牌,而价格查询要求该令牌出现在请求主体或头部中。请针对每家酒店从页面中提取一次该令牌,然后在每次价格查询中重复使用它。

def scrape_pricing(client, hotel_id, csrf, checkin, checkout, currency='EUR'):
    payload = {
        'operationName': 'AvailabilityCalendar',  # verify in DevTools
        'variables': {
            'input': {
                'hotelId': hotel_id,
                'checkIn': checkin,
                'checkOut': checkout,
                'currency': currency,
            }
        },
        'extensions': {'csrf': csrf},
    }
    r = client.post(ENDPOINT, json=payload, headers=HEADERS, timeout=30)
    r.raise_for_status()
    return r.json()

从隐藏的评论接口提取客人评论

当您在酒店页面点击“评论”标签时,客人评论会通过单独的 XHR 加载。打开开发者工具,切换到“Fetch/XHR”选项卡,点击该标签,并复制请求。它通过 skip (或 offset) 整数进行分页,每次约 25 条,并返回评论正文、评分、语言、评论者国家及日期。

一旦有一个请求成功,您可以通过在 httpx.AsyncClient:

import asyncio, httpx

async def fetch_reviews(client, hotel_id, skip):
    r = await client.post(ENDPOINT, json=review_payload(hotel_id, skip), headers=HEADERS)
    return r.json()

async def all_reviews(hotel_id, total):
    async with httpx.AsyncClient(http2=True) as c:
        tasks = [fetch_reviews(c, hotel_id, s) for s in range(0, total, 25)]
        return await asyncio.gather(*tasks)

请将每家酒店的并发请求控制在个位数;评论请求受严格的速率限制。

通过网站地图和位置自动完成 API 发现酒店

若要批量抓取 Booking.com 的库存数据,与其逐次查询,不如从 https://www.booking.com/robots.txt 开始。Booking.com 在此处发布其 Sitemap: 条目发布于此,包括酒店、景点和机场的站点地图索引。每个站点地图索引指向子站点地图,其URL数量上限为50,000个(根据站点地图协议),这也是酒店索引被拆分为多个文件的原因。 遍历索引可获取数千万个酒店 URL(含重复项),您可以通过 URL 短链接或解析后的酒店 ID 进行去重。我们的站点地图抓取指南中提供了可复用的处理模式。

针对定向搜索,Booking.com 自带的地点自动补全接口能将城市或社区字符串解析为搜索 GraphQL 调用所期望的目的地标识符,这比手动硬编码要高效得多。

规避封禁:头部信息、代理、速率限制与验证码

无论规模大小,成功抓取 Booking.com 的关键在于模拟普通浏览器的行为,并在网站要求时及时停止请求。截至 2026 年,Booking.com 的反机器人系统似乎会同时分析 TLS 和 HTTP/2 的行为特征,因此以下基础要求不可或缺:支持 HTTP/2 的客户端(httpxhttp2=True),以及包含 accept-languagesec-ch-ua-*,以及一个稳定的 user-agent ,且需与当前Chrome版本匹配。(请定期重新验证HTTP/2敏感度;该机制会发生变化。)

请使用家庭用户或 ISP 代理,而非数据中心 IP 范围;数据中心 IP 访问 Booking.com 时,仅需几十次请求就会触发验证码。保持并发数保守(每个 IP 5 到 10 次),添加时延抖动,并在 429403。若您不愿重新构建相关基础设施,WebScrapingAPI 的住宅代理网络和 Scraper API 均可处理轮换、重试及 TLS 指纹识别等环节。对于最难抓取的页面,反检测浏览器是最后的手段。

处理货币、语言及 1,000 条结果的分页限制

Booking.com 会根据您的出口 IP 的地理位置推断显示的货币,因此位于美国的爬虫默认会看到美元(USD),而位于欧盟的爬虫则会看到欧元(EUR)。若需保持货币一致,请通过针对特定国家的代理进行路由,或在每次请求中传递 selected_currency 查询参数。(请定期重新测试此行为;参数名称和 IP 推断逻辑往往会悄然变更。)

该平台还限制单次搜索结果约为 1,000 条。若要枚举繁华城市的房源,请将查询拆分:先按伦敦各街区(肖迪奇、卡姆登、肯辛顿)抓取,再按星级分类,随后按价格区间筛选,最后通过酒店 ID 合并结果。

总结与后续步骤

在生产环境中运行时,请将此代码集成到 Scrapy 中,由其处理重试、数据持久化和分布式运行。将规范化后的输出数据持久化到 Postgres 或列式数据库中,每天进行快照,并通过 robots.txt Booking.com的服务条款保持合规。

关键要点

  • 抓取 Booking.com 的最佳实践是结合使用两种方法:Selenium Wire 用于原型设计和 DOM 稳定性,以及内部 /dml/graphql 端点 httpx
  • 应提取全套实体(搜索列表、酒店详情页、每晚价格和客人评论),而不仅仅是搜索结果页面,否则数据集过于单薄,无法用于价格情报分析。
  • 使用 data-testid 选择器和 wait_for_request on /dml/graphql ,以确保搜索页面抓取工具能够应对布局漂移和分页竞争条件。
  • 提前规划平台限制:住宅代理、HTTP/2 头、基于 IP 的货币选择,以及强制查询分区的约 1,000 条结果分页上限。
  • /robots.txt 用于批量发现酒店 URL,并利用位置自动完成 API 解析目的地标识符。

常见问题

从 Booking.com 抓取酒店和价格数据是否合法?

在大多数司法管辖区,只要以合理的频率进行且不绕过身份验证,抓取公开可见的酒店列表、价格和汇总评论通常被视为允许的。话虽如此,服务条款、欧盟数据库指令以及 GDPR(针对任何可识别评论者身份的信息)都至关重要。在商业部署前请让法律顾问审查您的具体用例,并避免存储个人数据。

如何控制 Booking.com 向我的爬虫返回的货币?

有两种可靠的方法:通过位于目标货币所在国家的代理服务器发送请求(Booking.com 会根据出口 IP 推断货币),或者在每次请求中传递 selected_currency=EUR- 风格的查询参数来覆盖推断出的默认值。建议结合使用这两种方法以确保一致性,因为当 IP 和参数发生冲突时,对于以固定本地货币定价的酒店,覆盖设置偶尔会被忽略。

如何为伦敦或纽约等繁忙城市提取超过 1,000 条结果?

将查询进行分区。Booking.com 限制单次搜索结果约为 1,000 条,因此解决方法是将城市划分为多个小于该限制的小子集:先按街区划分,再按星级划分,如有需要再按价格区间划分。将得到的酒店 ID 合并并去除重复项。若需枚举全部库存,请改用遍历酒店站点地图索引,而非通过搜索界面。

我应该使用 Selenium 还是直接调用 Booking.com 的 GraphQL 接口?

建议使用 Selenium 进行探索和处理小规模任务;处理大规模任务时则使用 GraphQL 接口。当前端发生变化时,Selenium 更具容错性,因为它是通过查询渲染后的 DOM 来操作的。GraphQL 的响应速度更快,且每次请求的成本更低,但必须确保请求负载和头部信息与实时网站保持同步。一种常见的做法是同时维护这两种方案,并在 API 出现故障时从 API 切换到浏览器抓取。

为什么我的爬虫抓取到的价格与我在浏览器中看到的不同?

几乎总是以下三种情况之一:您的入住/退房日期未在 URL 中固定,您的退出 IP 更改了货币或应用了地区折扣,或者搜索卡上的价格不含税费,而浏览器仅在下一步显示这些税费。请固定日期、修正货币,并明确标注抓取的价格为每晚税前价格。

综合总结

一旦不再将 Booking.com 视为单一页面,而是将其视为一个端点生态系统,网页抓取便成为一个可解决的问题。Selenium Wire 为您提供了处理搜索结果和分页的容错入口,内部 /dml/graphql 端点则提供了持续监控所需的速度,而针对酒店详情页、每晚价格及评论的专用调用则完善了数据集。叠加网站地图发现、查询分区和明确的货币控制功能,您便拥有了一个超越简单单次查询示例、具备扩展能力的爬虫。

大多数团队往往低估了基础设施层面的关键要素:TLS 和 HTTP/2 指纹识别、住宅代理质量、重试与退避逻辑,以及随着网站演变而保持选择器和 GraphQL 有效负载同步所需的耐心。

如果您不愿亲自维护这一防封层,WebScrapingAPI 团队提供了一款 Scraper API,它通过轮换的住宅 IP 返回原始 HTML,并为您管理 CAPTCHA 和 TLS 处理;此外还有一款 Browser API,用于处理本指南中 Selenium 负责的多步骤交互。 只需将其中任意一种接口置于上述代码之前,您便能专注于解析和数据模型——这才是真正让您的产品脱颖而出的关键所在。

关于作者
Raluca Penciuc, 全栈开发工程师 @ WebScrapingAPI
Raluca Penciuc全栈开发工程师

Raluca Penciuc 是 WebScrapingAPI 的全栈开发工程师,主要负责开发爬虫、优化规避机制,并探索可靠的方法以降低在目标网站上的被检测概率。

开始构建

准备好扩展您的数据收集规模了吗?

加入2,000多家企业,使用WebScrapingAPI在无需任何基础设施开销的情况下,以企业级规模提取网络数据。