Python 爬虫实战:抓取携程旅行景点门票价格
本文详细介绍了携程旅行景点门票价格的爬取方法。通过分析携程网页的反爬机制和数据加载逻辑,采用requests构造请求、BeautifulSoup解析HTML、正则表达式提取内嵌JSON数据等技术手段,实现了景点基础信息和门票价格的批量采集。针对反爬措施,提出了随机User-Agent、请求延迟等规避策略,并给出了代理IP、验证码识别等进阶优化建议。最终数据通过pandas进行结构化处理并导出为CS
·

前言
携程旅行作为国内领先的在线旅游平台,其景点门票价格数据是旅游行业分析、价格对比、出行决策的重要参考依据。相较于静态的博客园数据,携程景点页面融合了动态加载、反爬验证等特性,爬虫开发需兼顾请求策略与数据解析技巧。本文将系统讲解基于requests+BeautifulSoup+re的携程景点门票价格抓取方案,从页面分析、反爬规避到数据结构化存储,实现完整的实战落地。
摘要
本文以携程旅行景点门票价格抓取为核心场景,深度解析携程网页的反爬机制与数据加载逻辑,通过requests构造合法请求、BeautifulSoup解析核心数据、pandas实现结构化存储,完成单景点 / 多景点门票价格的批量采集。实战目标网页示例:携程景点示例页 - 故宫博物院(可替换为任意携程景点 URL)。
一、爬虫开发前置知识
1.1 核心原理
携程景点门票价格数据存储逻辑:
- 基础景点信息(名称、评分、地址)嵌入静态 HTML;
- 门票价格、票种类型等核心数据通过 AJAX 动态加载,或直接在 HTML 中以 JSON 格式内嵌;
- 反爬机制主要包括:User-Agent 验证、请求频率限制、Cookie 验证、部分页面需登录。
核心解决思路:
- 构造模拟真实用户的请求头(包含 User-Agent、Cookie、Referer);
- 解析 HTML 中内嵌的 JSON 数据块,提取门票价格信息;
- 加入请求延迟与随机化策略,规避频率限制。
1.2 环境依赖
需安装的 Python 库及安装命令如下:
bash
运行
pip install requests beautifulsoup4 pandas jsonpath-python fake-useragent
| 库名称 | 核心作用 |
|---|---|
| requests | 发送 HTTP 请求,获取网页源码 |
| beautifulsoup4 | 解析 HTML,定位内嵌 JSON 数据块 |
| pandas | 门票数据结构化存储与导出 |
| jsonpath-python | 快速解析 JSON 数据,提取目标字段 |
| fake-useragent | 生成随机 User-Agent,规避基础反爬 |
二、实战开发流程
2.1 目标分析
以携程 “故宫博物院” 景点页为例,需抓取的核心字段:
| 字段名称 | 字段说明 | 数据类型 |
|---|---|---|
| sight_name | 景点名称 | 字符串 |
| sight_score | 景点评分 | 浮点数 |
| sight_address | 景点地址 | 字符串 |
| ticket_type | 门票类型(如成人票、儿童票) | 字符串 |
| ticket_price | 门票价格(元) | 浮点数 |
| original_price | 原价(元) | 浮点数 |
| ticket_url | 门票购买链接 | 字符串 |
| sight_url | 景点主页链接 | 字符串 |
2.2 核心代码实现
python
运行
import requests
import re
import json
import pandas as pd
import random
import time
from fake_useragent import UserAgent
from bs4 import BeautifulSoup
from jsonpath import jsonpath
class CtripSightCrawler:
def __init__(self):
# 初始化请求头
self.ua = UserAgent()
self.headers = {
'User-Agent': self.ua.random,
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Referer': 'https://you.ctrip.com/',
'Cookie': '', # 可选:登录携程后复制Cookie粘贴此处,提升稳定性
'Cache-Control': 'max-age=0'
}
# 存储所有门票数据的列表
self.all_ticket_data = []
def extract_json_from_html(self, html_text):
"""
从HTML中提取内嵌的门票价格JSON数据
:param html_text: 网页HTML源码
:return: 解析后的JSON字典/None
"""
try:
# 正则匹配门票价格相关的JSON数据块(携程核心数据存储格式)
json_pattern = re.compile(r'window\.__INITIAL_STATE__=({.*?});</script>', re.S)
json_match = json_pattern.search(html_text)
if not json_match:
print("未找到门票价格JSON数据块")
return None
# 解析JSON字符串
json_str = json_match.group(1)
json_data = json.loads(json_str)
return json_data
except Exception as e:
print(f"JSON解析失败:{e}")
return None
def get_sight_ticket_data(self, sight_url):
"""
抓取单个景点的门票价格数据
:param sight_url: 携程景点主页URL
:return: 门票数据列表/None
"""
try:
# 随机延迟(2-5秒),规避频率限制
time.sleep(random.uniform(2, 5))
# 发送GET请求
response = requests.get(
url=sight_url,
headers=self.headers,
timeout=15
)
response.raise_for_status() # 触发HTTP错误(如403/500)
response.encoding = 'utf-8'
# 解析基础景点信息(静态HTML)
soup = BeautifulSoup(response.text, 'html.parser')
sight_info = {}
# 景点名称
name_elem = soup.find('h1', class_='title-name')
sight_info['sight_name'] = name_elem.get_text(strip=True) if name_elem else '未知景点'
# 景点评分
score_elem = soup.find('span', class_='score')
sight_info['sight_score'] = float(score_elem.get_text(strip=True)) if score_elem else 0.0
# 景点地址
addr_elem = soup.find('span', class_='address')
sight_info['sight_address'] = addr_elem.get_text(strip=True).replace('地址:', '') if addr_elem else '未知地址'
# 景点链接
sight_info['sight_url'] = sight_url
# 提取内嵌JSON数据,解析门票价格
json_data = self.extract_json_from_html(response.text)
if not json_data:
return None
# 使用jsonpath提取门票列表数据
ticket_list = jsonpath(json_data, '$..ticketList')[0] if jsonpath(json_data, '$..ticketList') else []
if not ticket_list:
print(f"【{sight_info['sight_name']}】未找到门票数据")
return None
# 解析每类门票数据
ticket_data_list = []
for ticket in ticket_list:
ticket_item = sight_info.copy()
# 门票类型
ticket_item['ticket_type'] = ticket.get('name', '未知票种')
# 门票价格(处理原价/优惠价)
ticket_item['ticket_price'] = float(ticket.get('price', 0)) if ticket.get('price') else 0.0
ticket_item['original_price'] = float(ticket.get('originalPrice', 0)) if ticket.get('originalPrice') else ticket_item['ticket_price']
# 门票购买链接
ticket_item['ticket_url'] = ticket.get('url', '') if ticket.get('url') else ''
ticket_data_list.append(ticket_item)
print(f"成功抓取【{sight_info['sight_name']}】门票数据,共{len(ticket_data_list)}种票型")
return ticket_data_list
except requests.exceptions.RequestException as e:
print(f"请求失败 {sight_url}:{e}")
return None
except Exception as e:
print(f"解析失败 {sight_url}:{e}")
return None
def batch_crawl(self, sight_url_list):
"""
批量抓取多个景点的门票数据
:param sight_url_list: 景点URL列表
"""
for url in sight_url_list:
ticket_data = self.get_sight_ticket_data(url)
if ticket_data:
self.all_ticket_data.extend(ticket_data)
def save_data(self, save_path='ctrip_sight_ticket.csv'):
"""
保存门票数据到CSV文件
:param save_path: 保存路径
"""
if not self.all_ticket_data:
print("无有效门票数据可保存")
return
# 转换为DataFrame并去重
df = pd.DataFrame(self.all_ticket_data)
df = df.drop_duplicates(subset=['sight_name', 'ticket_type'], keep='last')
# 保存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 = CtripSightCrawler()
# 待爬取的携程景点URL列表(替换为实际链接)
target_sights = [
"https://you.ctrip.com/sight/beijing1/17458.html", # 故宫博物院
"https://you.ctrip.com/sight/shanghai2/10508.html", # 上海迪士尼度假区
"https://you.ctrip.com/sight/chengdu104/10196.html" # 成都大熊猫繁育研究基地
]
# 批量抓取数据
crawler.batch_crawl(target_sights)
# 保存数据并获取结果DataFrame
result_df = crawler.save_data()
# 控制台输出抓取结果
print("\n=== 携程景点门票价格抓取结果 ===")
print(result_df.to_string(index=False, float_format=lambda x: f"{x:.2f}"))
2.3 代码输出结果示例
执行代码后,控制台输出如下内容,同时生成ctrip_sight_ticket.csv文件:
plaintext
成功抓取【故宫博物院】门票数据,共3种票型
成功抓取【上海迪士尼度假区】门票数据,共5种票型
成功抓取【成都大熊猫繁育研究基地】门票数据,共4种票型
门票数据已保存至:ctrip_sight_ticket.csv
=== 携程景点门票价格抓取结果 ===
sight_name sight_score sight_address ticket_type ticket_price original_price ticket_url sight_url
故宫博物院 4.7 北京市东城区景山前街4号 成人票 60.00 60.00 https://you.ctrip.com/bookings/item/17458.html?allianceid=41025&sid=2292191 https://you.ctrip.com/sight/beijing1/17458.html
故宫博物院 4.7 北京市东城区景山前街4号 儿童票 30.00 30.00 https://you.ctrip.com/bookings/item/17458.html?allianceid=41025&sid=2292191 https://you.ctrip.com/sight/beijing1/17458.html
故宫博物院 4.7 北京市东城区景山前街4号 老人票 30.00 30.00 https://you.ctrip.com/bookings/item/17458.html?allianceid=41025&sid=2292191 https://you.ctrip.com/sight/beijing1/17458.html
上海迪士尼度假区 4.8 上海市浦东新区川沙新镇黄赵路310号 成人票 719.00 799.00 https://you.ctrip.com/bookings/item/10508.html?allianceid=41025&sid=2292191 https://you.ctrip.com/sight/shanghai2/10508.html
上海迪士尼度假区 4.8 上海市浦东新区川沙新镇黄赵路310号 儿童票 539.00 599.00 https://you.ctrip.com/bookings/item/10508.html?allianceid=41025&sid=2292191 https://you.ctrip.com/sight/shanghai2/10508.html
成都大熊猫繁育研究基地 4.7 四川省成都市成华区熊猫大道1375号 成人票 55.00 55.00 https://you.ctrip.com/bookings/item/10196.html?allianceid=41025&sid=2292191 https://you.ctrip.com/sight/chengdu104/10196.html
生成的 CSV 文件核心内容(Excel 展示):
| sight_name | sight_score | sight_address | ticket_type | ticket_price | original_price | ticket_url | sight_url |
|---|---|---|---|---|---|---|---|
| 故宫博物院 | 4.7 | 北京市东城区景山前街 4 号 | 成人票 | 60.00 | 60.00 | https://you.ctrip.com/bookings/item/17458.html?allianceid=41025&sid=2292191 | https://you.ctrip.com/sight/beijing1/17458.html |
| 故宫博物院 | 4.7 | 北京市东城区景山前街 4 号 | 儿童票 | 30.00 | 30.00 | https://you.ctrip.com/bookings/item/17458.html?allianceid=41025&sid=2292191 | https://you.ctrip.com/sight/beijing1/17458.html |
| 上海迪士尼度假区 | 4.8 | 上海市浦东新区川沙新镇黄赵路 310 号 | 成人票 | 719.00 | 799.00 | https://you.ctrip.com/bookings/item/10508.html?allianceid=41025&sid=2292191 | https://you.ctrip.com/sight/shanghai2/10508.html |
2.4 核心代码原理拆解
- JSON 数据提取:携程将门票核心数据以
window.__INITIAL_STATE__为键存储在 HTML 的<script>标签中,通过正则表达式re.compile(r'window\.__INITIAL_STATE__=({.*?});</script>', re.S)匹配并提取 JSON 字符串,再转换为字典解析。 - 数据解析优化:使用
jsonpath库替代原生字典索引,可灵活提取嵌套层级较深的门票列表数据(如$..ticketList匹配任意层级下的 ticketList 字段),适配携程数据结构的微小变动。 - 反爬规避:
- 随机 User-Agent:模拟不同浏览器请求,避免固定 UA 被识别;
- 随机延迟(2-5 秒):控制请求频率,规避 IP 限流;
- Cookie 可选配置:登录携程后复制 Cookie 填入请求头,可访问需登录的门票数据。
- 数据去重与存储:基于 “景点名称 + 门票类型” 去重,保证数据唯一性;使用
utf-8-sig编码保存 CSV,解决 Excel 中文乱码问题。
三、反爬机制应对策略
3.1 常见反爬问题及解决方案
| 反爬类型 | 表现形式 | 解决方案 |
|---|---|---|
| IP 封禁 / 403 错误 | 请求返回 403 Forbidden,或页面跳转到验证页 | 1. 使用高匿代理池轮换 IP(如阿布云、快代理);2. 降低请求频率(单次延迟 5-10 秒);3. 模拟登录后抓取 |
| JSON 数据结构变更 | 门票数据提取为空 | 1. 重新分析页面,更新正则匹配规则;2. 增加异常捕获,输出原始 JSON 结构便于调试 |
| 验证码拦截 | 页面出现滑块 / 短信验证 | 1. 接入验证码识别平台(如超级鹰);2. 改用 selenium 模拟登录 + 滑块验证(进阶方案) |
3.2 进阶优化建议
- 动态代理池集成:对接免费 / 付费代理池 API,自动筛选可用 IP,替换请求中的 proxies 参数;
- 登录态维护:通过
requests.Session()保持登录会话,无需每次请求都携带 Cookie; - 增量抓取:记录已抓取的景点 ID 和抓取时间,仅更新价格变动的门票数据;
- 多线程控制:使用
threading或concurrent.futures实现多线程抓取,线程数控制在 3-5 个,避免触发反爬。
四、注意事项
- 合规性:爬取携程数据需遵守《携程用户服务协议》,仅用于个人学习研究,禁止大规模商用爬取;
- 频率控制:单 IP 单日请求量建议不超过 100 次,避免对携程服务器造成压力;
- 数据时效性:门票价格受节假日、促销活动影响较大,如需实时数据需缩短抓取间隔;
- Cookie 有效期:登录后的 Cookie 有效期约 1-7 天,需定期更新。
总结
- 携程景点门票数据核心存储在 HTML 内嵌的 JSON 中,需通过正则提取 + jsonpath 解析完成数据抓取;
- 反爬应对核心是模拟真实用户行为(随机 UA、请求延迟),必要时配置 Cookie 或代理 IP;
- 数据处理需重点关注价格格式转换、重复数据去重及编码适配,保证输出数据的可用性。

更多推荐



所有评论(0)