前言

飞猪旅行作为阿里旗下的在线旅游平台,其酒店套餐产品融合了价格、房型、权益、有效期等多维度信息,是旅游消费决策与行业价格分析的重要数据源。飞猪页面采用前后端分离架构,核心数据通过 AJAX 接口动态加载,且具备完善的反爬机制(如签名验证、Token 校验)。本文将从实战角度,讲解基于requests+jsonpath的飞猪酒店套餐数据抓取方案,重点拆解接口分析、参数构造、反爬规避等核心环节,实现酒店套餐信息的高效采集。

摘要

本文以飞猪旅行酒店套餐信息抓取为核心场景,深度解析飞猪动态接口的请求逻辑,通过分析网络请求、构造合法请求参数、解析 JSON 响应数据,实现酒店套餐价格、房型、权益等核心信息的抓取。实战目标网页示例:飞猪酒店套餐示例页 - 三亚亚特兰蒂斯酒店(可替换为任意飞猪酒店套餐 URL)。

一、爬虫开发前置知识

1.1 核心原理

飞猪酒店套餐数据加载逻辑:

  • 前端页面仅渲染基础框架,套餐列表、价格、权益等核心数据通过调用 AJAX 接口(JSON 格式)动态加载;
  • 接口请求需携带必要参数(如商品 ID、时间戳、签名等),部分参数需从页面源码中提取;
  • 反爬机制包括:Referer 验证、User-Agent 白名单、接口签名校验、IP 频率限制。

核心解决思路:

  1. 借助浏览器开发者工具分析套餐数据对应的 AJAX 接口;
  2. 从套餐详情页源码中提取接口所需的核心参数(如 itemId、token);
  3. 构造符合飞猪规范的请求头和参数,发送请求获取 JSON 数据;
  4. 解析 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}&timestamp={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 核心代码原理拆解

  1. ItemId 提取:飞猪套餐 URL 中包含唯一标识itemId(格式为t_数字),通过urlparse解析 URL 参数、正则匹配两种方式提取,保证参数获取的稳定性。
  2. AJAX 接口调用
    • 接口 URL 构造:拼接itemId和毫秒级时间戳(timestamp),模拟飞猪前端的请求参数;
    • 请求头优化:添加X-Requested-With: XMLHttpRequest标识 AJAX 请求,OriginReferer字段模拟合法请求来源。
  3. JSON 数据解析:使用jsonpath库提取嵌套层级较深的字段(如$..hotelName匹配任意层级下的酒店名称),相比原生字典索引更灵活,适配接口返回数据结构的微小变动。
  4. 反爬规避
    • 随机延迟(3-6 秒):飞猪反爬对频率敏感,延长单次请求间隔降低封禁风险;
    • 随机 User-Agent:避免固定 UA 被识别为爬虫;
    • Cookie 可选配置:登录后 Cookie 可突破部分接口的访问限制,获取更多套餐数据。
  5. 数据格式化:对价格字段保留 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 进阶优化建议

  1. 签名参数构造:通过浏览器开发者工具(Sources 面板)分析飞猪前端的签名生成 JS 代码,还原sign参数的生成逻辑,添加到接口请求参数中;
  2. Session 会话保持:使用requests.Session()创建会话对象,自动维护 Cookie 和请求头,模拟用户连续访问;
  3. 代理池集成:对接付费代理池(如讯代理、快代理),在请求中添加proxies参数轮换 IP,示例:

    python

    运行

    proxies = {
        'http': 'http://IP:端口',
        'https': 'https://IP:端口'
    }
    response = requests.get(api_url, headers=headers, proxies=proxies)
    
  4. 异常重试机制:使用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):
        # 原有接口请求逻辑
    

四、注意事项

  1. 合规性:爬取飞猪数据需遵守《飞猪服务协议》,仅用于个人学习研究,禁止大规模商用爬取;
  2. 接口稳定性:飞猪 AJAX 接口无公开文档,可能不定期更新 URL 或参数,需定期验证接口可用性;
  3. 频率控制:单 IP 单日抓取套餐数量建议不超过 50 个,避免触发高强度反爬;
  4. 数据时效性:酒店套餐价格、权益、有效期实时变动,需定期重新抓取以保证数据准确性。

总结

  1. 飞猪酒店套餐数据存储在 AJAX 接口返回的 JSON 中,需先提取 itemId 再调用接口完成抓取;
  2. 反爬应对核心是模拟合法请求(正确请求头、Cookie、签名)+ 低频率访问(随机延迟、代理 IP);
  3. 数据解析优先使用 jsonpath 库,提升对接口数据结构变动的适配性,同时做好数据格式化与去重。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐