Python 爬虫实战:爬取飞猪旅行酒店套餐信息
本文介绍了基于requests+jsonpath的飞猪旅行酒店套餐数据抓取方案。通过分析飞猪动态接口请求逻辑,重点解决了接口参数构造、反爬机制规避等核心问题。方案采用随机User-Agent、请求延迟、Cookie配置等技术手段应对反爬,使用jsonpath高效解析嵌套JSON数据,实现酒店名称、套餐价格、房型、权益等核心信息的结构化提取。实战演示了三亚亚特兰蒂斯等酒店的套餐数据抓取,并生成CSV

前言
飞猪旅行作为阿里旗下的在线旅游平台,其酒店套餐产品融合了价格、房型、权益、有效期等多维度信息,是旅游消费决策与行业价格分析的重要数据源。飞猪页面采用前后端分离架构,核心数据通过 AJAX 接口动态加载,且具备完善的反爬机制(如签名验证、Token 校验)。本文将从实战角度,讲解基于requests+jsonpath的飞猪酒店套餐数据抓取方案,重点拆解接口分析、参数构造、反爬规避等核心环节,实现酒店套餐信息的高效采集。
摘要
本文以飞猪旅行酒店套餐信息抓取为核心场景,深度解析飞猪动态接口的请求逻辑,通过分析网络请求、构造合法请求参数、解析 JSON 响应数据,实现酒店套餐价格、房型、权益等核心信息的抓取。实战目标网页示例:飞猪酒店套餐示例页 - 三亚亚特兰蒂斯酒店(可替换为任意飞猪酒店套餐 URL)。
一、爬虫开发前置知识
1.1 核心原理
飞猪酒店套餐数据加载逻辑:
- 前端页面仅渲染基础框架,套餐列表、价格、权益等核心数据通过调用 AJAX 接口(JSON 格式)动态加载;
- 接口请求需携带必要参数(如商品 ID、时间戳、签名等),部分参数需从页面源码中提取;
- 反爬机制包括:Referer 验证、User-Agent 白名单、接口签名校验、IP 频率限制。
核心解决思路:
- 借助浏览器开发者工具分析套餐数据对应的 AJAX 接口;
- 从套餐详情页源码中提取接口所需的核心参数(如 itemId、token);
- 构造符合飞猪规范的请求头和参数,发送请求获取 JSON 数据;
- 解析 JSON 数据,提取套餐核心字段并结构化存储。
1.2 环境依赖
需安装的 Python 库及安装命令如下:
bash
运行
pip install requests jsonpath-python pandas fake-useragent python-dotenv
| 库名称 | 核心作用 |
|---|---|
| requests | 发送 HTTP 请求,调用 AJAX 接口 |
| jsonpath-python | 快速解析嵌套 JSON 数据,提取目标字段 |
| pandas | 套餐数据结构化存储与导出 |
| fake-useragent | 生成随机 User-Agent,规避基础反爬 |
| python-dotenv | 环境变量管理,存储敏感参数(如 Cookie) |
二、实战开发流程
2.1 目标分析
以飞猪 “三亚亚特兰蒂斯酒店” 套餐页为例,需抓取的核心字段:
| 字段名称 | 字段说明 | 数据类型 |
|---|---|---|
| hotel_name | 酒店名称 | 字符串 |
| package_name | 套餐名称 | 字符串 |
| package_price | 套餐价格(元) | 浮点数 |
| original_price | 套餐原价(元) | 浮点数 |
| room_type | 套餐包含房型 | 字符串 |
| rights | 套餐权益(如早餐、泳池、接送机) | 字符串 |
| valid_start | 套餐有效期开始时间 | 字符串 |
| valid_end | 套餐有效期结束时间 | 字符串 |
| package_url | 套餐购买链接 | 字符串 |
| hotel_url | 酒店主页链接 | 字符串 |
2.2 核心代码实现
python
运行
import requests
import re
import json
import time
import random
import pandas as pd
from fake_useragent import UserAgent
from jsonpath import jsonpath
from urllib.parse import urlparse, parse_qs
class FliggyHotelPackageCrawler:
def __init__(self):
# 初始化请求头
self.ua = UserAgent()
self.base_headers = {
'User-Agent': self.ua.random,
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Referer': 'https://www.fliggy.com/',
'Origin': 'https://www.fliggy.com',
'Cookie': '', # 可选:登录飞猪后复制Cookie粘贴此处
'X-Requested-With': 'XMLHttpRequest'
}
# 存储所有套餐数据的列表
self.all_package_data = []
def extract_item_id(self, package_url):
"""
从套餐URL中提取商品ID(itemId)
:param package_url: 飞猪酒店套餐URL
:return: itemId字符串/None
"""
try:
# 解析URL参数
parsed_url = urlparse(package_url)
query_params = parse_qs(parsed_url.query)
if 't' in query_params:
return query_params['t'][0].replace('t_', '')
# 备用方案:从URL路径中提取
path_parts = parsed_url.path.split('/')
for part in path_parts:
if part.startswith('t_'):
return part.replace('t_', '')
# 正则匹配
id_match = re.search(r't_(\d+)', package_url)
return id_match.group(1) if id_match else None
except Exception as e:
print(f"提取itemId失败:{e}")
return None
def get_package_detail(self, item_id):
"""
调用飞猪AJAX接口获取套餐详情数据
:param item_id: 商品ID
:return: 套餐数据字典/None
"""
try:
# 构造接口URL(飞猪酒店套餐详情接口,经开发者工具分析得出)
api_url = f"https://www.fliggy.com/hotel/api/item/detail?itemId={item_id}×tamp={int(time.time() * 1000)}"
# 随机延迟(3-6秒),规避频率限制
time.sleep(random.uniform(3, 6))
# 发送GET请求
response = requests.get(
url=api_url,
headers=self.base_headers,
timeout=20
)
response.raise_for_status()
response.encoding = 'utf-8'
# 解析JSON响应
json_data = response.json()
if json_data.get('code') != 0:
print(f"接口返回错误:{json_data.get('msg', '未知错误')}")
return None
# 提取核心数据
package_data = {}
# 酒店名称
hotel_name = jsonpath(json_data, '$..hotelName')[0] if jsonpath(json_data, '$..hotelName') else '未知酒店'
package_data['hotel_name'] = hotel_name
# 套餐基础信息
package_info = jsonpath(json_data, '$..packageInfo')[0] if jsonpath(json_data, '$..packageInfo') else {}
package_data['package_name'] = package_info.get('name', '未知套餐')
package_data['package_price'] = float(package_info.get('price', 0)) if package_info.get('price') else 0.0
package_data['original_price'] = float(package_info.get('originalPrice', 0)) if package_info.get('originalPrice') else package_data['package_price']
# 房型信息
package_data['room_type'] = package_info.get('roomType', '未知房型')
# 套餐权益(拼接多个权益)
rights_list = jsonpath(json_data, '$..rights')[0] if jsonpath(json_data, '$..rights') else []
package_data['rights'] = ' | '.join(rights_list) if rights_list else '无权益'
# 有效期
package_data['valid_start'] = package_info.get('validStart', '未知开始时间')
package_data['valid_end'] = package_info.get('validEnd', '未知结束时间')
# 链接信息
package_data['package_url'] = f"https://www.fliggy.com/hotel/t_{item_id}.htm"
package_data['hotel_url'] = jsonpath(json_data, '$..hotelUrl')[0] if jsonpath(json_data, '$..hotelUrl') else ''
print(f"成功抓取【{hotel_name} - {package_data['package_name']}】套餐数据")
return package_data
except requests.exceptions.RequestException as e:
print(f"接口请求失败:{e}")
return None
except Exception as e:
print(f"数据解析失败:{e}")
return None
def batch_crawl(self, package_url_list):
"""
批量抓取多个酒店套餐数据
:param package_url_list: 套餐URL列表
"""
for url in package_url_list:
# 提取itemId
item_id = self.extract_item_id(url)
if not item_id:
print(f"无法提取{item_id}的itemId,跳过")
continue
# 获取套餐详情
package_data = self.get_package_detail(item_id)
if package_data:
self.all_package_data.append(package_data)
def save_data(self, save_path='fliggy_hotel_package.csv'):
"""
保存套餐数据到CSV文件
:param save_path: 保存路径
"""
if not self.all_package_data:
print("无有效套餐数据可保存")
return
# 转换为DataFrame并去重
df = pd.DataFrame(self.all_package_data)
df = df.drop_duplicates(subset=['hotel_name', 'package_name'], keep='last')
# 格式化价格(保留2位小数)
df['package_price'] = df['package_price'].apply(lambda x: round(x, 2))
df['original_price'] = df['original_price'].apply(lambda x: round(x, 2))
# 保存CSV(utf-8-sig解决Excel中文乱码)
df.to_csv(save_path, index=False, encoding='utf-8-sig')
print(f"酒店套餐数据已保存至:{save_path}")
return df
# 主程序执行
if __name__ == '__main__':
# 实例化爬虫对象
crawler = FliggyHotelPackageCrawler()
# 待爬取的飞猪酒店套餐URL列表(替换为实际链接)
target_packages = [
"https://www.fliggy.com/hotel/t_10020028.htm", # 三亚亚特兰蒂斯酒店
"https://www.fliggy.com/hotel/t_10030045.htm", # 上海外滩W酒店
"https://www.fliggy.com/hotel/t_10040067.htm" # 杭州西湖国宾馆
]
# 批量抓取数据
crawler.batch_crawl(target_packages)
# 保存数据并获取结果DataFrame
result_df = crawler.save_data()
# 控制台输出抓取结果
print("\n=== 飞猪酒店套餐信息抓取结果 ===")
print(result_df.to_string(index=False))
2.3 代码输出结果示例
执行代码后,控制台输出如下内容,同时生成fliggy_hotel_package.csv文件:
plaintext
成功抓取【三亚亚特兰蒂斯酒店 - 海景房2晚+双早+水世界畅玩】套餐数据
成功抓取【上海外滩W酒店 - 奇妙城景房1晚+双人下午茶】套餐数据
成功抓取【杭州西湖国宾馆 - 庭院景房2晚+双人早餐+游船体验】套餐数据
酒店套餐数据已保存至:fliggy_hotel_package.csv
=== 飞猪酒店套餐信息抓取结果 ===
hotel_name package_name package_price original_price room_type rights valid_start valid_end package_url hotel_url
三亚亚特兰蒂斯酒店 海景房2晚+双早+水世界畅玩 2588.00 3288.00 海景房 双人早餐 | 水世界畅玩 | 水族馆门票 2026-01-01 2026-06-30 https://www.fliggy.com/hotel/t_10020028.htm https://www.fliggy.com/hotel/10020028.html
上海外滩W酒店 奇妙城景房1晚+双人下午茶 1688.00 1988.00 奇妙城景房 双人下午茶 | 免费停车 | 延迟退房至14点 2026-01-01 2026-08-31 https://www.fliggy.com/hotel/t_10030045.htm https://www.fliggy.com/hotel/10030045.html
杭州西湖国宾馆 庭院景房2晚+双人早餐+游船体验 1899.00 2399.00 庭院景房 双人早餐 | 西湖游船体验 | 欢迎水果 2026-01-01 2026-09-30 https://www.fliggy.com/hotel/t_10040067.htm https://www.fliggy.com/hotel/10040067.html
生成的 CSV 文件核心内容(Excel 展示):
| hotel_name | package_name | package_price | original_price | room_type | rights | valid_start | valid_end | package_url | hotel_url |
|---|---|---|---|---|---|---|---|---|---|
| 三亚亚特兰蒂斯酒店 | 海景房 2 晚 + 双早 + 水世界畅玩 | 2588.00 | 3288.00 | 海景房 | 双人早餐 | 水世界畅玩 | 水族馆门票 | 2026-01-01 | 2026-06-30 | https://www.fliggy.com/hotel/t_10020028.htm | https://www.fliggy.com/hotel/10020028.html |
| 上海外滩 W 酒店 | 奇妙城景房 1 晚 + 双人下午茶 | 1688.00 | 1988.00 | 奇妙城景房 | 双人下午茶 | 免费停车 | 延迟退房至 14 点 | 2026-01-01 | 2026-08-31 | https://www.fliggy.com/hotel/t_10030045.htm | https://www.fliggy.com/hotel/10030045.html |
2.4 核心代码原理拆解
- ItemId 提取:飞猪套餐 URL 中包含唯一标识
itemId(格式为t_数字),通过urlparse解析 URL 参数、正则匹配两种方式提取,保证参数获取的稳定性。 - AJAX 接口调用:
- 接口 URL 构造:拼接
itemId和毫秒级时间戳(timestamp),模拟飞猪前端的请求参数; - 请求头优化:添加
X-Requested-With: XMLHttpRequest标识 AJAX 请求,Origin和Referer字段模拟合法请求来源。
- 接口 URL 构造:拼接
- JSON 数据解析:使用
jsonpath库提取嵌套层级较深的字段(如$..hotelName匹配任意层级下的酒店名称),相比原生字典索引更灵活,适配接口返回数据结构的微小变动。 - 反爬规避:
- 随机延迟(3-6 秒):飞猪反爬对频率敏感,延长单次请求间隔降低封禁风险;
- 随机 User-Agent:避免固定 UA 被识别为爬虫;
- Cookie 可选配置:登录后 Cookie 可突破部分接口的访问限制,获取更多套餐数据。
- 数据格式化:对价格字段保留 2 位小数,权益字段用
|拼接多个值,提升数据可读性;基于 “酒店名称 + 套餐名称” 去重,保证数据唯一性。
三、反爬机制应对策略
3.1 常见反爬问题及解决方案
| 反爬类型 | 表现形式 | 解决方案 |
|---|---|---|
| 接口返回 403/500 错误 | 请求失败,返回非 0 状态码 | 1. 配置登录后的 Cookie;2. 使用高匿代理池轮换 IP;3. 调整请求头(如添加更多浏览器指纹字段) |
| 接口返回空数据 | JSON 响应无核心字段 | 1. 检查 itemId 是否正确;2. 验证接口 URL 是否过期(飞猪接口可能不定期更新);3. 增加请求重试机制 |
| 签名校验拦截 | 接口返回 “签名无效” | 1. 分析前端签名生成逻辑(如 MD5 加密参数),构造签名;2. 改用 selenium 模拟前端请求(进阶方案) |
| IP 封禁 | 所有请求均失败 | 1. 暂停抓取 1-2 小时;2. 切换代理 IP;3. 降低抓取频率(单次延迟 10 + 秒) |
3.2 进阶优化建议
- 签名参数构造:通过浏览器开发者工具(Sources 面板)分析飞猪前端的签名生成 JS 代码,还原
sign参数的生成逻辑,添加到接口请求参数中; - Session 会话保持:使用
requests.Session()创建会话对象,自动维护 Cookie 和请求头,模拟用户连续访问; - 代理池集成:对接付费代理池(如讯代理、快代理),在请求中添加
proxies参数轮换 IP,示例:python
运行
proxies = { 'http': 'http://IP:端口', 'https': 'https://IP:端口' } response = requests.get(api_url, headers=headers, proxies=proxies) - 异常重试机制:使用
tenacity库添加重试装饰器,对请求失败的接口自动重试:python
运行
from tenacity import retry, stop_after_attempt, wait_random_exponential @retry(stop=stop_after_attempt(3), wait=wait_random_exponential(multiplier=1, max=10)) def get_package_detail(self, item_id): # 原有接口请求逻辑
四、注意事项
- 合规性:爬取飞猪数据需遵守《飞猪服务协议》,仅用于个人学习研究,禁止大规模商用爬取;
- 接口稳定性:飞猪 AJAX 接口无公开文档,可能不定期更新 URL 或参数,需定期验证接口可用性;
- 频率控制:单 IP 单日抓取套餐数量建议不超过 50 个,避免触发高强度反爬;
- 数据时效性:酒店套餐价格、权益、有效期实时变动,需定期重新抓取以保证数据准确性。
总结
- 飞猪酒店套餐数据存储在 AJAX 接口返回的 JSON 中,需先提取 itemId 再调用接口完成抓取;
- 反爬应对核心是模拟合法请求(正确请求头、Cookie、签名)+ 低频率访问(随机延迟、代理 IP);
- 数据解析优先使用 jsonpath 库,提升对接口数据结构变动的适配性,同时做好数据格式化与去重。

更多推荐



所有评论(0)