1. 项目概述:Playwright爬取携程机票价格日历
最近在做一个旅行规划工具时,需要获取未来90天内不同航线的机票价格波动数据。传统爬虫方案在面对携程这类动态渲染的网站时往往力不从心,经过多次尝试,最终选择Playwright作为核心工具,成功实现了机票价格日历数据的自动化采集。
这个方案最大的价值在于解决了三个痛点:1)动态API请求的自动拦截与解析;2)反爬机制的绕过;3)数据清洗与矩阵构建。整个过程涉及网络请求监听、数据清洗算法和可视化呈现等多个技术环节,下面我会详细拆解每个关键步骤的实现逻辑。
提示:本文所有代码示例均基于Python 3.8+和Playwright 1.40+版本,建议在阅读时同步准备开发环境。
2. 技术选型与架构设计
2.1 为什么选择Playwright?
相比传统的Requests+BeautifulSoup组合或Selenium方案,Playwright具有几个不可替代的优势:
- 全浏览器支持:可自动处理Chromium、WebKit和Firefox的差异
- 网络拦截能力:通过route()方法可以直接拦截和修改网络请求
- 无头模式稳定性:在无界面环境下运行更加稳定,不易被检测
- 自动等待机制:内置智能等待,减少人工设置sleep时间的需要
特别是在处理携程这样的动态网站时,机票价格数据通常通过XHR请求动态加载,Playwright的请求拦截功能可以直接捕获这些API调用,省去了逆向分析JavaScript的麻烦。
2.2 整体流程设计
整个系统的工作流程分为四个核心模块:
- 请求层:负责启动浏览器实例、导航到目标页面并拦截API请求
- 解析层:处理原始JSON数据,提取关键字段并进行初步清洗
- 存储层:将结构化数据持久化到数据库或文件系统
- 可视化层:生成价格日历矩阵和热力图
python复制# 伪代码展示核心流程
async def main():
async with async_playwright() as p:
browser = await p.chromium.launch(headless=False)
page = await browser.new_page()
# 设置请求拦截
await page.route("**/api/flight/**", handle_request)
# 导航到目标页面
await page.goto("https://flights.ctrip.com/")
# 模拟用户操作
await page.fill("#departCity", "北京")
await page.fill("#arriveCity", "上海")
await page.click("#searchBtn")
# 等待数据加载
await page.wait_for_selector(".flight-list")
# 处理数据...
3. 环境准备与依赖安装
3.1 基础环境配置
建议使用Python 3.8+版本,并创建独立的虚拟环境:
bash复制python -m venv ctrip_env
source ctrip_env/bin/activate # Linux/Mac
ctrip_env\Scripts\activate # Windows
3.2 核心依赖安装
需要安装以下关键包:
bash复制pip install playwright beautifulsoup4 pandas matplotlib
playwright install # 安装浏览器驱动
各包的作用说明:
playwright:核心浏览器自动化工具beautifulsoup4:辅助HTML解析(虽然主要用API数据,但有时需要备用方案)pandas:数据清洗和矩阵构建matplotlib:基础可视化(也可替换为pyecharts等更专业的库)
4. 核心实现:请求拦截与数据处理
4.1 设置请求拦截器
携程的机票数据主要通过/api/flight/系列接口返回,我们需要监听这些请求:
python复制async def handle_request(route, request):
if "flight/list" in request.url:
# 放行原始请求
response = await route.continue_()
# 获取响应数据
try:
json_data = await response.json()
await process_flight_data(json_data) # 处理数据
except:
print("Failed to parse JSON response")
else:
await route.continue_()
4.2 数据解析关键点
从接口返回的JSON中,需要特别关注以下几个字段:
flightList:航班列表主体数据departDate和price:构成价格日历的基础cabinClass:舱位等级信息discount:折扣信息(需要特别处理)
python复制def parse_flight_item(flight):
return {
"flight_no": flight.get("flightNumber"),
"depart_time": flight.get("departureDate"),
"arrive_time": flight.get("arrivalDate"),
"price": flight.get("priceInfo", {}).get("price"),
"discount": calculate_discount(
flight.get("priceInfo", {}).get("price"),
flight.get("priceInfo", {}).get("originalPrice")
),
"airline": flight.get("airlineName")
}
4.3 反爬应对策略
携程的反爬机制主要集中在:
- 请求频率检测:需要合理设置延迟,建议在2-5秒之间随机波动
- 行为验证:模拟真实用户操作序列,包括鼠标移动、点击等
- IP限制:建议使用住宅代理(注意合规使用)
python复制# 模拟人类操作模式
async def human_like_actions(page):
await page.mouse.move(100, 100)
await asyncio.sleep(random.uniform(0.5, 1.5))
await page.mouse.move(200, 150)
await asyncio.sleep(random.uniform(0.2, 0.7))
5. 数据存储与矩阵构建
5.1 数据结构设计
为了构建价格日历矩阵,我们需要将数据组织为以下结构:
python复制{
"route": "北京-上海",
"dates": {
"2024-03-01": {"min_price": 580, "avg_price": 620, "flights": [...]},
"2024-03-02": {"min_price": 620, "avg_price": 650, "flights": [...]},
# ...
}
}
5.2 使用Pandas进行转换
将原始数据转换为矩阵形式:
python复制def build_price_matrix(flight_data):
df = pd.DataFrame(flight_data)
df['depart_date'] = pd.to_datetime(df['depart_time']).dt.date
# 按日期聚合
price_matrix = df.groupby('depart_date')['price'].agg(['min', 'mean'])
return price_matrix.reset_index()
6. 可视化呈现
6.1 日历热力图生成
使用Matplotlib生成价格日历热力图:
python复制def plot_calendar_heatmap(price_matrix):
plt.figure(figsize=(12, 6))
plt.imshow(price_matrix, cmap='YlOrRd', aspect='auto')
plt.colorbar(label='Price (CNY)')
plt.xticks(range(7), ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'])
plt.title('Flight Price Calendar')
plt.show()
6.2 进阶可视化建议
对于更专业的可视化,可以考虑:
- 使用
pyecharts制作交互式日历图 - 添加航线对比功能
- 集成价格预警线标记
7. 常见问题与解决方案
7.1 请求被拦截
现象:返回403错误或验证码页面
解决方案:
- 检查User-Agent是否设置合理
- 增加请求间隔时间
- 使用
page.set_extra_http_headers()添加更多请求头
7.2 数据解析失败
现象:JSON解析出错或字段缺失
解决方案:
- 添加异常捕获和重试机制
- 保存原始响应供后续分析
- 检查接口版本是否更新
python复制async def safe_json_parse(response):
try:
return await response.json()
except:
print(f"Raw response: {await response.text()}")
return None
8. 进阶优化方向
8.1 分布式采集架构
对于大规模采集需求,可以考虑:
- 使用Scrapy+Playwright组合
- 通过Redis实现任务队列
- 多节点协同工作
8.2 数据更新策略
实现智能更新机制:
- 差异检测:只获取价格变动的航班
- 动态频率:旺季增加采集频次
- 历史版本:保留历史数据供分析
8.3 异常检测算法
引入机器学习检测价格异常:
- 基于历史数据的价格波动模型
- 突发性降价/涨价的实时预警
- 节假日价格模式识别
在实际开发过程中,我发现携程的接口结构大约每3-4个月会有一次较大更新,建议在代码中加入接口版本检测机制。另外,对于高频访问,使用住宅代理IP轮换可以有效降低封禁风险,但要注意控制请求频率在合理范围内。