一、核心原理:为什么可以“无需Selenium”? 当你在携程网站(flights.ctrip.com)上搜索机票时,页面并不会一次性加载所有机票数据。而是在你点击查询后,由浏览器中的JavaScript代码向服务器发送一个或多个HTTP请求。服务器接收到请求后,并不会返回一个完整的HTML页面,而是返回一个纯数据的响应,通常是JSON(JavaScript Object Notation) 格式。浏览器的JavaScript引擎再根据这个JSON数据包,动态地渲染出机票列表、价格等信息。 这个“发送请求-获取JSON-渲染页面”的过程就是Ajax。 我们的目标就是绕过浏览器渲染这一步,直接:
- 发现:找到是哪个请求获取了机票JSON数据。
- 模拟:用Python的requests库完全模拟这个请求。
- 解析:从响应的JSON中直接提取我们需要的信息。 这种方法直接从数据源头获取信息,效率比操作浏览器快数个数量级。 二、实战:捕获并分析携程机票Ajax请求 第一步:使用浏览器开发者工具抓包
- 打开Chrome或Edge浏览器,进入携程国际机票页面(flights.ctrip.com)。
- 按 F12 键打开“开发者工具”。
- 切换到 “网络”(Network) 选项卡。
- 勾选 “保留日志”(Preserve log) 并点击 “XHR” 或 “Fetch/XHR” 筛选器。这样能过滤出最常见的Ajax请求。
- 在页面中选择出发地(如北京-BJS)、目的地(如上海-SHA)、日期等,点击“搜索”。
- 此时,“网络”面板会冒出大量请求。我们需要从中找到那个包含机票列表数据的请求。 https://img-blog.csdnimg.cn/direct/1c07d6c15c6546788b1c9a0c88c9c6f5.png (示意图:注意观察以关键词如Flight、Search命名的请求)
- 逐个点击新出现的请求,查看其“预览”(Preview)或“响应”(Response)选项卡。我们的目标是找到一个响应内容为JSON格式,并且里面包含可读的机票信息(如航班号、起飞时间、价格等)的请求。
- 一旦找到,记录下这个请求的详细信息: ○ 请求URL (Request URL):这是最重要的信息。 ○ 请求方法 (Request Method):通常是GET或POST。 ○ 请求头 (Request Headers):特别是User-Agent, Referer, 以及可能的认证信息。 ○ 查询参数 (Query String Parameters) 或 载荷 (Payload):如果是GET请求,参数在URL里;如果是POST请求,参数通常在“载荷”选项卡里,格式可能是Form Data或JSON。 以某次搜索为例,我们可能发现一个关键的请求: URL: https://flights.ctrip.com/itinerary/api/12808/products Method: POST Headers: 需要包含 Content-Type: application/json Payload: 是一个庞大的JSON对象,里面包含了查询的出发地、目的地、日期等信息。 第二步:用Python模拟请求 现在我们有了所需的信息,就可以用requests库来精确地模拟这个请求。 然后,开始编写代码。请注意,以下代码中的请求头和载荷(data)需要根据你实际抓包到的信息进行修改,否则无法成功。 这里提供一个高度仿真的模板。 import requests import json from pprint import pprint
def crawl_ctrip_flights(): # 1. 定义目标URL (从开发者工具中复制) url = 'https://flights.ctrip.com/itinerary/api/12808/products'
# 2. 定义请求头 (从开发者工具中复制并简化,User-Agent和Referer至关重要)
headers = {
'Content-Type': 'application/json',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Referer': 'https://flights.ctrip.com/itinerary/oneway/bjs-sha?date=2023-10-01', # 这个Referer需要根据你的搜索修改
'Connection': 'keep-alive',
# 有时可能需要其他Header,如Authorization等,请根据抓包实际情况添加。
}
# 3. 构建请求载荷 (Payload) - 这是最核心的部分,需要根据你的搜索条件构建
# 这个JSON结构非常复杂,通常直接从浏览器抓包复制,然后修改关键参数。
request_payload = {
"flightWay": "Oneway",
"classType": "ALL",
"hasChild": False,
"hasBaby": False,
"searchIndex": 1,
"airportParams": [
{
"dcity": "BJS", # 出发地城市代码
"acity": "SHA", # 目的地城市代码
"dcityname": "北京",
"acityname": "上海",
"date": "2023-10-01" # 出发日期,格式YYYY-MM-DD
}
],
"selectedInfos": None
# ... 这里可能还有大量其他字段,请务必使用你抓包到的完整JSON结构
}
# 4. 发送POST请求
print("正在发送请求...")
try:
# 将Python字典转换为JSON字符串并发送
response = requests.post(
url=url,
headers=headers,
data=json.dumps(request_payload) # 使用json.dumps转换
)
response.raise_for_status() # 检查请求是否成功
# 5. 解析响应
# 响应内容直接就是JSON,我们可以用.json()方法将其转换为Python字典
data = response.json()
print("请求成功!")
return data
except requests.exceptions.RequestException as e:
print(f"请求发生错误: {e}")
return None
if name == 'main': result_data = crawl_ctrip_flights() if result_data: # 使用pprint美化输出,初步查看数据结构 pprint(result_data) 重要提示:request_payload的构造是成功与否的最大关键。携程的请求载荷结构非常复杂且可能经常变动。最稳妥的方法是:在浏览器开发者工具中,找到该请求,在“载荷”选项卡中直接复制完整的JSON,然后使用 json.loads() 将其转换为Python字典,再在此基础上修改dcity, acity, date等参数。直接自己手写构造极易因缺少某些字段而失败。 第三步:解析JSON数据 得到response.json()后,我们面对的是一个多层嵌套的庞大字典(dict)。接下来的任务就是像剥洋葱一样,一层层地找到我们需要的数据。 def parse_flight_data(data): """ 从返回的JSON数据中解析出航班信息 """ # 1. 首先检查数据结构和状态码 if not data or data.get('status') != 0: print("数据获取失败或状态非零") return
# 2. 找到核心数据路径
# 这个路径需要通过在Preview中不断展开来摸索
try:
# 这是一个示例路径,实际路径请根据你获取到的JSON结构进行调整!
itinerary_list = data['data']['itineraryList']
print(f"共找到 {len(itinerary_list)} 个行程")
for itinerary in itinerary_list:
# 继续深入挖掘,找到航班信息列表和价格
legs = itinerary['legs']
for leg in legs:
flights = leg['flights']
for flight in flights:
# 提取具体信息
airline_name = flight['airlineName']
flight_number = flight['flightNumber']
departure_city = flight['departureCityName']
arrival_city = flight['arrivalCityName']
departure_time = flight['departureDate']
arrival_time = flight['arrivalDate']
# 提取价格信息 (价格可能在行程itinerary层,也可能在航班层,需仔细分析)
# 这里假设价格在itinerary层
price_info = itinerary.get('priceList', [{}])[0]
price = price_info.get('price', '无价格信息')
# 打印信息
print(f"""
航空公司: {airline_name} 航班号: {flight_number} 出发: {departure_city} - 时间: {departure_time} 到达: {arrival_city} - 时间: {arrival_time} 价格: ¥{price} {'-' * 50} """)
except KeyError as e:
print(f"解析数据时出错,键不存在: {e}")
pprint(data) # 再次输出数据,方便调试
最后,在主函数中调用解析函数: if name == 'main': result_data = crawl_ctrip_flights() if result_data: parse_flight_data(result_data) 三、注意事项与优化建议
- 反爬虫机制:携程一定有反爬措施。除了标准的User-Agent和Referer,还可能验证Cookie、IP频率等。你需要: ○ 使用代理IP池 来规避IP限制。如https://www.16yun.cn/ ○ 合理设置请求间隔(如time.sleep(random.uniform(1, 3))),避免过高频率的请求。 ○ 考虑维护一个有效的Cookie池。
- 参数化与规模化:将出发地、目的地、日期等参数提取出来,做成函数参数,方便批量抓取。
- 错误处理与日志:增加完善的异常捕获(try...except)和日志记录,保证爬虫长期稳定运行。
- 数据存储:将解析后的数据存入CSV、MySQL或MongoDB等数据库,而非仅仅打印出来。 结论 通过“捕获Ajax请求 -> 模拟请求 -> 解析JSON”这条技术路径,我们成功地实现了一个高效、专业的携程机票爬虫,完全摒弃了笨重低效的浏览器自动化方案。这个过程不仅适用于携程,也适用于绝大多数现代Web应用(如淘宝、美团、微博等),是中级爬虫工程师必须掌握的核心技能。