可从 Expedia 提取哪些数据以及其重要性
当您抓取 Expedia 酒店搜索结果时,一个构建良好的爬虫可以从每个列表卡片中提取以下字段:
- 酒店名称 — 搜索结果中显示的酒店名称
- 每晚价格 — 您所选日期的房价,包括任何促销价格
- 星级评定 — 官方星级分类(1–5)
- 客人评分 — 汇总的用户评分(例如 8.4/10)
- 评论数量 — 构成该评分的评论总数
- 位置/周边区域 — 适用于地理筛选和地图定位
该数据集可扩展以捕获促销标识或缩略图,从而支持更丰富的下游分析。
实际应用场景包括价格监控(追踪不同日期和目的地的价格变动)、旅行比价应用(聚合多家在线旅行社的房源信息)以及竞争对手基准分析(了解某住宿设施的价格与周边酒店相比处于何种水平)。抓取的Expedia数据还可用于驱动推荐引擎和面向客户的旅行工具。
开始前的法律与伦理考量
在编写任何代码之前,请先核对以下清单:
- 检查 robots.txt — 访问 https://www.expedia.com/robots.txt 并遵守禁止访问的路径。(发布时请核实 — 指令可能变更。)
- 审阅服务条款 — Expedia 的服务条款限制了自动化访问。个人研究与商业转售属于不同的风险类别。如有疑问,请咨询律师。
- 仅抓取公开数据 — 向任何匿名访客展示的酒店列表均属公开信息。切勿尝试访问受账户限制的内容或自动提交表单。
- 对请求进行速率限制 — 添加有意延迟(请求间隔至少 2–5 秒)。频繁轰炸服务器既不符合道德规范,也是被封禁的捷径。
- 负责任地存储数据 — 仅保留所需数据,并避免以直接与 Expedia 自身产品形成竞争的方式重新发布抓取的内容。
为何 Expedia 难以抓取
Expedia 通过 JavaScript 动态加载酒店列表,这意味着仅抓取原始 HTML 的静态爬虫将无法获取实际内容。服务器发送的通常是空壳页面;浏览器需执行 JavaScript 才能获取并渲染酒店卡片。若不执行该 JavaScript,便无法看到数据。JavaScript 渲染是网页抓取面临的核心挑战,而 Expedia 正是其中防御最为严密的案例之一。
除了渲染问题外,Expedia 还采用了 IP 封禁(同一 IP 的重复请求会触发封禁)、浏览器指纹识别(无头浏览器因缺少 API 和时间异常而可被检测到)以及动态类名(CSS 类在构建时生成,且随每次部署而变化,会无预警地破坏硬编码的选择器)。
单纯的 requests 请求虽能获取导航和元数据,却无法获取任何酒店列表。正是这一缺口,使得您必须使用无头浏览器或爬取 API。
选择方案:自建无头浏览器 vs. 爬虫 API
DIY 方案虽提供最大灵活性,但需要您自行配置无头浏览器、管理代理池,并随着浏览器版本更新维护环境。而爬取 API 则将这些工作全部封装:您只需发送包含目标 URL 和提取规则的请求,API 便会自动处理渲染、代理轮换和重试。
对于大多数 Expedia 爬取用例——价格监控、定期数据提取、市场调研——API 方案不仅部署更快,长期维护成本也更低。您无需承担保持浏览器二进制文件最新、寻找可靠的住宅代理以及调试环境特定渲染故障等额外工作。相应的权衡是您需要依赖外部服务,因此在决定采用哪种方案之前,请务必评估其正常运行时间保证和定价层级。
环境设置与先决条件
您需要 Python 3.8 或更高版本(可通过 `python --version` 检查)。安装所需库:
pip install webscrapingapi pandas
webscrapingapi 是 WebScrapingAPI 的官方 Python 客户端——它封装了 HTTP 请求层并处理身份验证。pandas 负责数据清洗和 CSV 导出。
从 WebScrapingAPI 控制台获取 API 密钥,并将其存储为环境变量,而非在脚本中硬编码:
export WSAPI_KEY="your_api_key_here"
随后在 Python 中通过 os.environ.get("WSAPI_KEY") 加载该密钥。请将脚本文件(例如 expedia.py)保存在专用的项目文件夹中,以确保 CSV 导出的相对路径在每次运行时保持一致。您只需使用 Python 内置的 os 模块即可,无需额外安装。如需了解基于 Python 的爬取模式的更全面介绍,请参阅我们的 Python 网页爬取指南。
如何在 Expedia 上识别正确的 CSS 选择器
这是大多数教程都会跳过的步骤。以下是具体的开发者工具操作指南。
- 打开 Expedia 搜索页面并等待其完全加载。
- 右键点击酒店卡片 → 选择“检查”以打开开发者工具,此时该元素将被高亮显示。
- 识别列表卡片容器——即包裹每个酒店结果的重复出现的 <div> 或 <article> 标签。这就是您的根选择器;每个列表中应只出现一次。
- 深入查看子元素——找出包含酒店名称、价格、评分和评论数量的元素。右键单击每个元素 → “复制 > 复制选择器”。
- 验证唯一性——在开发者工具控制台运行 document.querySelectorAll("YOUR_SELECTOR"),确认结果数量与酒店卡片数量一致。
- 使用相对选择器 — 子元素选择器应相对于卡片容器,而非绝对定位于文档根节点。
重要提示:Expedia 上的 CSS 类名是动态生成的,且会随网站部署而变化。在正式运行前,请务必对照实时页面验证选择器。我们的 CSS 选择器速查表详细介绍了选择器语法和特异性。
构建 Expedia 酒店搜索抓取工具
该爬虫的核心是两个字典——extract_rules 和 js_scenario——它们作为参数传递给 API 客户端。二者共同告知 API 需要提取哪些内容,以及在开始提取前如何渲染页面。正确配置这两个对象是整个 Expedia Python 爬取工作流中最关键的一步,因为所有后续结果都依赖于它们。
定义提取规则与 JS 渲染指令
`extract_rules` 用于告知 API 使用哪些 CSS 选择器以及应返回哪些数据。`js_scenario` 则为内置的无头浏览器提供指令:`wait` 会暂停执行指定毫秒数;`evaluate` 则在页面上下文中运行自定义 JavaScript(用于滚动、点击等操作)。
import os, json
import pandas as pd
import webscrapingapi
API_KEY = os.environ.get("WSAPI_KEY")
client = webscrapingapi.WebScrapingAPIClient(API_KEY)
# Verify these selectors against a live Expedia page before use
CARD_SELECTOR = "[data-stid='lodging-card-responsive']"
extract_rules = {
"hotels": {
"selector": CARD_SELECTOR,
"type": "list",
"output": {
"name": {"selector": "[data-stid='content-hotel-title']", "output": "text"},
"price": {"selector": "[data-stid='price-summary']", "output": "text"},
"rating": {"selector": ".uitk-rating-medium", "output": "text"},
"reviews": {"selector": "[data-stid='reviews-summary']", "output": "text"},
"location": {"selector": "[data-stid='content-hotel-neighborhood']", "output": "text"},
}
}
}
# Wait 2 s → scroll to bottom → wait 2 s to trigger lazy-loaded cards
js_scenario = {"instructions": [
{"wait": 2000},
{"evaluate": "window.scrollTo(0, document.body.scrollHeight)"},
{"wait": 2000}
]}
这种两阶段的等待模式——先暂停再滚动,随后再次暂停——是经过深思熟虑的。Expedia 利用 JavaScript 渲染机制,随着视口向下滚动页面,延迟加载酒店卡片。若跳过任一等待步骤,都可能导致返回的酒店列表不完整,尤其是在网络连接较慢或目的地搜索结果较多时。
发起 API 请求与处理响应
关键参数:wait_for 会在提取数据前等待 CSS 选择器出现;country_code 用于设置代理出口国家以实现价格本地化;premium_proxy 用于启用住宅代理轮换。
def scrape_expedia_hotels(destination, check_in, check_out, page=1):
q = destination.replace(" ", "+")
url = (f"https://www.expedia.com/Hotel-Search"
f"?destination={q}&startDate={check_in}&endDate={check_out}&page={page}")
try:
response = client.get(url, params={
"wait_for": CARD_SELECTOR,
"extract_rules": json.dumps(extract_rules),
"js_scenario": json.dumps(js_scenario),
"country_code": "us",
"premium_proxy": "true",
})
except Exception as e:
print(f"Request failed: {e}"); return []
if response.status_code == 401:
print("Invalid API key."); return []
if response.status_code == 500:
print(f"HTTP 500 on page {page} — retry with backoff."); return []
if response.status_code != 200:
print(f"Unexpected status {response.status_code}."); return []
try:
hotels = response.json().get("hotels", [])
except ValueError:
return []
if not hotels:
print(f"No results on page {page}. CSS selectors may have drifted.")
return hotels
返回 200 状态码的空酒店列表几乎总是意味着您的 CSS 选择器已偏移。来自 Expedia 的 HTTP 500 错误通常是暂时的——请在调用处构建采用指数退避的重试逻辑。请注意,page 参数已通过函数签名传递,这使得在下一节的分页循环中调用此函数变得非常简单。
抓取多页酒店结果
Expedia 使用的页面查询参数具有规律性,因此分页处理非常简单。下面的循环将持续迭代,直到结果集为空或达到页面限制:
import time
def scrape_all_pages(destination, check_in, check_out, max_pages=5, delay=3):
all_hotels = []
for page in range(1, max_pages + 1):
hotels = scrape_expedia_hotels(destination, check_in, check_out, page=page)
if not hotels:
print(f"No results on page {page}. Stopping."); break
all_hotels.extend(hotels)
print(f"Page {page}: {len(hotels)} hotels (total: {len(all_hotels)})")
if page < max_pages:
time.sleep(delay) # Respect rate limits
return all_hotels
results = scrape_all_pages("Rome, Italy", "2026-10-05", "2026-10-10", max_pages=5, delay=3)
延迟参数至关重要。密集的请求会可靠地触发 IP 封禁。3 秒的暂停是合理的下限;对于大规模抓取,应在 2–5 秒范围内随机调整,以避免可预测的定时模式。
Expedia 的搜索结果深度因目的地和日期范围而异。与其假设固定的页数,不如让循环的提前退出条件(if not hotels: break)干净利落地处理终止——当 API 返回空列表时,即表示已到达结果末尾。
数据清理与导出至 CSV
原始文本在导出前需要进行清理——价格、评分和评论数量以无类型字符串的形式返回。首先对其进行标准化处理:
import re
def clean_price(raw):
if not raw: return None
try: return float(re.sub(r"[^\d.]", "", raw.split()[0]))
except ValueError: return None
def clean_rating(raw):
if not raw: return None
m = re.search(r"(\d+\.?\d*)", raw)
return float(m.group(1)) if m else None
def clean_review_count(raw):
if not raw: return None
d = re.sub(r"[^\d]", "", raw)
return int(d) if d else None
def clean_and_export(hotels, filename="expedia_hotels.csv"):
df = pd.DataFrame([{
"name": h.get("name", "").strip(),
"price_usd": clean_price(h.get("price")),
"rating": clean_rating(h.get("rating")),
"review_count": clean_review_count(h.get("reviews")),
"location": h.get("location", "").strip(),
} for h in hotels])
df.dropna(subset=["name"], inplace=True)
df.to_csv(filename, index=False, encoding="utf-8")
print(f"Exported {len(df)} hotels to {filename}")
return df
df = clean_and_export(results)
CSV 列已指定数据类型——price_usd(浮点数)、rating(浮点数)、review_count(整数)——无需手动后处理即可直接用于分析。
完整脚本参考
所有函数(fetch、scrape、to_csv)均已在上述章节中定义。将它们合并到名为 expedia.py 的单个文件中,设置 WSAPI_KEY 环境变量,并通过以下入口点触发完整运行。
if __name__ == "__main__":
to_csv(scrape("Rome, Italy", "2026-10-05", "2026-10-10"))
使用 `python expedia.py` 执行。结果将写入工作目录中的 `expedia_hotels.csv` 文件,数据已清理完毕,可立即进行分析。
当 Expedia 更改布局时如何维护您的爬虫
Expedia 爬虫停止工作的最常见原因之一是选择器漂移——Expedia 会定期更新其前端,类名发生变化,元素层级发生位移,导致上个月还能正常工作的选择器会悄无声息地停止返回数据。
如何检测选择器漂移:您的爬虫运行无误但返回空列表。所有清理后的字段中同时出现 None 值。这些都是选择器已发生变化的可靠信号。
重新识别工作流:
- 在浏览器中打开 Expedia 并进行一次新搜索。
- 右键点击酒店卡片 → 检查元素。
- 将当前 DOM 与您的 extract_rules 选择器进行对比。即使类名已更改,也要找到具有相同语义的元素(如酒店名称标题、价格容器)。
- 更新 CARD_SELECTOR 及其子选择器,然后在重新启用完整循环前进行单页测试。
轻量级监控:针对固定目标设置每日金丝雀测试。若结果为零则触发警报,以便当天即可察觉选择器漂移。关于 JavaScript 密集型网站如何影响选择器稳定性的更多信息,请参阅我们关于 JavaScript 如何影响网页抓取的指南。
扩展与最佳实践
- 限制请求频率。页面间至少间隔 3–5 秒;随机化延迟以避免可预测的时序模式。
- 实施指数退避。遇到 HTTP 500 或 429 响应时,每次重试将延迟时间加倍(5 秒、10 秒、20 秒)。
- 轮换 country_code。将出口国与目标市场匹配,以实现准确的价格本地化。
- 安排定期运行。使用 cron、Airflow 或云函数进行价格监控。将结果带时间戳存储,以便追踪随时间的变化。
- 记录每页的状态码和结果计数。当出现故障时,您需要准确知道是哪一页和哪个目标地址触发了故障。
如需了解如何在大规模场景下避免IP封禁,请参阅我们关于网络爬虫中解除IP封禁的指南。
关键要点
- 对于 Expedia 而言,JavaScript 渲染是不可或缺的。静态 HTTP 请求无法返回酒店列表——您需要使用无头浏览器或能为您渲染 JS 的爬取 API。
- CSS 选择器会发生漂移。Expedia 会定期更新其前端。请在您的处理流程中集成选择器漂移检测机制,并掌握当选择器失效时如何使用开发者工具重新识别它们。
- 分页处理需要循环。使用 Expedia 的 page 查询参数,并在结果集为空时停止——切勿假设页面数量是固定的。
- 导出前请清理数据。在提取时去除货币符号、解析数值评分,并将评论数量转换为整数。
- 实施速率限制与流量控制。在请求之间有意设置延迟,既符合道德规范,也是避免被封禁的实际必要措施。
常见问题
如何判断我的 Expedia 爬虫是否因 HTML 结构变更而失效?
最明显的信号是结果列表为空——API 调用成功(HTTP 200)但返回零条酒店记录。次要信号是所有清理后的字段均返回 None 值。建议针对固定目标设置每日“金丝雀”测试,并在结果为零时触发警报。
抓取 Expedia 搜索结果页面与酒店详情页有什么区别?
搜索结果页面返回的是分页列表中的摘要数据——名称、价格、评分、位置。酒店详情页面则包含某家酒店的更丰富数据:设施列表、房型明细、取消政策以及评论内容。两者的选择器及渲染要求各不相同。
在抓取大型数据集时,如何避免触发 Expedia 的速率限制?
使用随机延迟而非固定间隔——固定间隔更容易被反机器人系统识别。将目的地列表分散在数小时或数天内处理,并在收到 429 和 500 响应时采用指数退避策略。
能否在单次请求中同时抓取 Expedia 的评论、评分和价格数据?
可以。如果评论评分和数量显示在搜索结果页面上,请将这两个字段的筛选器添加到 extract_rules 字典中。完整的评论正文位于酒店详情页面,需要单独发起请求。
结论
虽然可以用 Python 抓取 Expedia 的酒店数据,但这不仅仅需要一个基本的 HTTP 请求。您需要 JavaScript 渲染来查看实际列表,需要可靠的代理轮换来避免 IP 被封,还需要制定明确的策略来识别和维护 CSS 选择器,以适应 Expedia 前端界面的演变。
本指南采用的方法——利用爬虫 API 处理基础设施层,并结合显式的 extract_rules 和 js_scenario 参数——能让您比搭建和维护本地无头浏览器环境更快地获得可运行的爬虫。分页循环、数据清洗函数以及选择器漂移监控策略,使其不仅能作为概念验证,更具备投入生产环境的条件。
若您希望完全省去基础设施的开销,WebScrapingAPI 的 Scraper API 可在单一接口后端处理 JavaScript 渲染、代理轮换及验证码破解——让您专注于数据本身,而非底层技术实现。探索我们的旅游及酒店业抓取用例,了解更多的在线旅行社(OTA)数据采集模式,或查阅我们关于抓取 Booking.com 及 Airbnb 房源的相关指南。




