
做机票比价这事儿我一开始也天真地以为就是个发个请求拿个HTML的活儿。直到我盯着飞猪的页面看着那些折扣数字在眼前跳动Network面板里刷出几十个XHR请求才意识到这事儿没那么简单。飞猪作为阿里系的旅行平台数据全在前端动态渲染接口还带着签名参数想要实时抓取折扣信息得把前端渲染、接口分析、数据解析、反爬应对这整条链路都趟一遍。这篇就完整记录一下我是怎么用Python把飞猪的机票折扣信息实时抓下来的从头到尾的踩坑实录希望能给准备入坑爬虫或者正在跟动态页面较劲的朋友一点参考。1. 整体思路与设计拆解1.1 为什么选飞猪开刀先说一个核心问题市面上旅行平台那么多携程、去哪儿、同程为什么非得跟飞猪较劲我的判断是飞猪的页面结构和技术栈非常典型它代表了当前Web开发的主流形态——前端MVVM框架渲染、数据通过XHR异步加载、接口带签名校验。如果你能拿下一个飞猪那么再去处理其他同类型的动态站点思路和方法论基本是通用的。相比之下有些老牌OTA平台的页面还带着服务端渲染的影子抓起来太简单了练不出东西。另一个原因是飞猪的机票折扣信息展示得足够丰富。同一个航班它在页面上会同时呈现原价、折扣价、折扣力度几折、剩余票量、航班时段等结构化字段。这种信息密度高、字段结构完整的目标站点对做数据解析的人来说是很好的练手对象你不需要在字段提取上耗费太多精力可以更专注于请求链路和反爬对抗。不过也得说清楚飞猪的接口签名机制一直在变2024年之后还加入了不少风控策略。我的经验是核心的搜索接口和详情页接口可以搞定但某些敏感的报价接口会走得比较艰难。这个项目的真正价值是让你掌握一套前端页面逆向分析接口参数构造的完整方法论而不是死磕某一个接口。1.2 实时抓取的三种策略对比实时抓取这四个字说起来容易做起来有讲究。你得先想明白一个问题所谓实时到底要多实时我实测下来机票折扣信息其实不是一个高频变化的数据。同一个航班的价格通常以分钟级到小时级的频率在变不会像股票行情那样每秒跳动。所以对抓取策略来说关键是在时效性和请求成本之间找一个平衡点。策略实现方式时效性请求成本可行性短轮询固定间隔如30秒请求一次准实时高容易触发风控简单粗暴适合初期长轮询服务端hold住请求直到数据变化才返回实时中需服务器配合飞猪不支持基本不可行WebSocket主动订阅建立长连接服务端主动推送真实时低但需逆向协议飞猪APP端有Web端未开放我做这个项目时选了短轮询动态间隔调整的方案。具体来说核心逻辑是这样的设置一个基础轮询间隔比如60秒如果检测到折扣信息发生了显著变化比如折扣从8折跳到6折就自动把轮询间隔缩短到20秒快速跟踪这个变化的波段如果连续多次请求返回的数据都没变化就把间隔拉长到180秒降低对目标服务器的压力和自身被封的风险。这种自适应轮询的思路比固定频率请求要优雅得多。说到底爬虫跟目标站点之间是一场博弈你完全可以在不越过红线的前提下用更聪明的方式拿到数据。1.3 技术选型清单技术栈这块我用了非常朴素的组合requestslxmlpandasAPScheduler。没上Scrapy也没上Selenium原因后面会说。先说为什么不用Scrapy。Scrapy确实是个强大的框架自带调度器、下载器、中间件、管道但问题是它的学习曲线和项目结构都偏重。对于飞猪这种接口级抓取任务你真正需要的只是一个能发请求、能解析JSON、能存数据的轻量管道——用Scrapy反而有种杀鸡用牛刀的感觉。而且Scrapy的异步机制在处理这种单目标、单接口的任务时优势完全发挥不出来。再说为什么不用Selenium。我知道很多人遇到动态页面第一个想到的就是它——模拟浏览器打开页面等着渲染完再用XPath去提取。但Selenium有两个硬伤一是资源开销大一个无头浏览器要占300MB以上的内存你要是做实时抓取等于开着一台重型卡车在市区通勤二是容易被识破飞猪的前端有专门检测WebDriver的脚本你一旦用了Seleniumnavigator.webdriver这个属性就暴露了风控系统一眼就能认出来。所以我最终的方案是直接分析XHR接口用requests模拟请求拿JSON数据用lxml做必要时的HTML解析。这套组合轻量、高效、可控性强跑起来的资源开销几乎可以忽略不计。# 基础依赖安装 # requests: HTTP请求库 # lxml: HTML/XML解析库支持XPath # pandas: 数据处理与存储 # APScheduler: 定时调度 pip install requests lxml pandas apscheduler这套组合装完整个环境不超过200MB跑起来的内存占用在50MB以内相当轻快。2. 核心细节解析与实操要点2.1 找到真正的数据入口抓飞猪这类动态页面最核心的一步是找到真实的数据入口。很多人一上来就对着页面源码硬找结果发现什么都找不到——因为页面源码只是一个空壳子真正的数据全在JavaScript里通过XHR动态加载。用Chrome DevTools的Network面板是我用过最直接的办法。具体操作流程是打开开发者工具切到Network面板勾选Fetch/XHR这个过滤项非常关键它会帮你把脚本、样式、图片等静态资源全部过滤掉只留下Ajax请求然后在页面上执行一次真实的搜索操作比如搜一个杭州到北京的机票。这时候你会看到Network面板里哗啦啦刷出一串请求。不要慌逐个点开看重点关注Response里是JSON数据的请求。我当时的做法是看响应体的大小和内容类型——JSON请求的响应通常比较小几KB到几十KB而且内容类型是application/json这种情况基本可以断定是数据接口。找到数据接口之后还有一个更重要的点看请求头信息。飞猪的接口请求体里有几个字段非常关键_appKey标识请求来源的应用ID_sign请求签名用于服务端校验mtop系列参数阿里系网关的通用参数包含API名称、版本、数据格式等信息这些参数的构造规则就是后面工作的难点和重点。但这里我不过度展开签名逆向的内容那是另一篇长文的体量。我只告诉你一个原则飞猪Web端的接口签名复杂度在同类平台里属于中等偏上如果你对加密算法不熟悉先从复制浏览器请求的完整Header开始构造一个和浏览器几乎一致的请求能解决很大一部分问题。2.2 XPath与JSON解析的取舍拿到数据之后紧接着就是解析环节。这里我想说一个很多爬虫教程没讲透的点动态页面的数据解析JSON解析是首选XPath反而是次选。为什么这么说因为动态页面的数据在传到前端时本来就是JSON格式你直接拿response.json()解析字段结构一目了然提取路径非常清晰。而XPath解析是给服务端渲染的静态HTML准备的你需要在HTML结构这个中间层里去提取数据效率低不说还容易因为页面改版而失效。我实际抓飞猪时90%以上的字段都是直接从JSON里取的import requests import json def parse_ticket_data(response_data): 从飞猪接口返回的JSON中提取机票折扣信息 result [] # 飞猪接口的返回结构data - itemList - items try: item_list response_data[data][itemList][items] for item in item_list: flight_info { flight_no: item.get(flightNo), # 航班号 dep_city: item.get(depCityName), # 出发城市 arr_city: item.get(arrCityName), # 到达城市 dep_time: item.get(depTime), # 出发时间 arr_time: item.get(arrTime), # 到达时间 original_price: item.get(orgPrice), # 原价 discount_price: item.get(price), # 折扣价 discount_rate: item.get(discount), # 折扣力度如85即8.5折 remaining_tickets: item.get(ticketLeft) # 剩余票量 } result.append(flight_info) except KeyError as e: print(f字段解析失败接口结构可能已变化: {e}) return result那XPath是不是完全用不上也不是。有一种场景我确实要用到XPath当你需要从页面中提取某个非结构化的信息时比如页面上某个文案、某个CSS类名暗示的状态、或者是接口里没返回但页面上显示的附加信息。这时候用lxml的etree.HTML()把HTML转成Element对象再用XPath提取是个很好的补充方案。from lxml import etree def parse_html_with_xpath(html_content): 用XPath从HTML中提取数据 tree etree.HTML(html_content) # 提取页面中所有带折扣字样的文本节点 discount_nodes tree.xpath(//*[contains(text(), 折)]/text()) return [node.strip() for node in discount_nodes if node.strip()]这里有个细节需要注意XPath的text()函数多种用法带参数的text()比如text()和string()的结果是不一样的。我踩过的坑是——用//div[contains(text(), 折)]在有些场景下匹配不到任何节点因为contains(text(), ...)是针对第一个直接文本子节点做判断而不是整段文本。正确做法是用//div[contains(string(.), 折)]它是针对整个元素的文本内容做判断覆盖面更广。2.3 反爬策略的边界认知聊到爬虫绕不开反爬这个话题。飞猪作为阿里系产品反爬体系之完善在行业内是公认的顶配水平。我的经验是你需要分清哪些是可以通过技术手段合规处理的和哪些是绝对不能碰的。第一类合理地模拟真人行为。这包括设置合适的请求头User-Agent、Referer、Accept-Language等、控制请求频率单IP下每秒不超过1次、随机化请求间隔在基础间隔上增加随机抖动。import random import time def get_headers(): 构造浏览器风格的请求头 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Accept: application/json, text/plain, */*, Accept-Language: zh-CN,zh;q0.9, Referer: https://www.fliggy.com/, Origin: https://www.fliggy.com, } return headers def random_delay(base_interval60): 在基础间隔上增加±30%的随机抖动模拟真人操作节奏 delay base_interval * random.uniform(0.7, 1.3) time.sleep(delay)第二类坚决不碰的领域。比如伪造身份信息、尝试绕过风控系统的验证码机制、大规模分布式抓取也就是热搜词里提到的分布式爬虫steam爬虫这类思路用在飞猪上风险极大、使用代理池轮换IP。这些操作不仅在技术上难度陡增而且会直接触犯法律红线。特别是《数据安全法》《个人信息保护法》实施之后对爬虫行为的法律边界有了更明确的界定一定要有敬畏之心。我做这个项目时给自己定的原则是仅用于个人学习和研究控制请求频率在不会对目标服务器造成影响的范围内单IP每分钟不超过1次请求每次抓取的数据量控制在极小规模并且绝不公开传播抓取到的数据。这个边界意识比技术本身重要得多。3. 实操过程与核心环节实现3.1 环境准备说干就干先把环境搭起来。我默认你已经装好了Python 3.8如果还没有装自己去官网下载安装包安装时记得勾选Add Python to PATH这个选项有很多人在这步翻车导致后续在命令行里输入python会提示python不是内部或外部命令。装好Python之后我强烈建议用虚拟环境来管理这个项目的依赖不要直接装到全局环境里。虚拟环境的好处是每个项目都有一套独立的依赖包互不干扰不会出现这个项目要requests 2.x那个项目要requests 1.x的窘境。# 创建虚拟环境 python -m venv venv # 激活虚拟环境Windows venv\Scripts\activate # 激活虚拟环境Linux/macOS source venv/bin/activate # 安装依赖 pip install requests lxml pandas apscheduler这一步做完你的环境就准备好了。别小看这几行命令我见过太多人在环境配置上浪费大把时间——不是依赖装不上而是装到了错误的环境里或者是Python版本不匹配导致的兼容性问题。3.2 构造一个真实的抓取请求核心环节来了。这一步的目标是用requests库模拟浏览器发起一个机票搜索请求并把返回的JSON数据解析出来。先说一个实操上的关键点飞猪的搜索接口要求请求方式为GET搜索参数全部放在URL的query string里。我从浏览器Network面板里复制的第一个请求URL是这样的已脱敏处理https://www.fliggy.com/async/search/priceList?depCity杭州arrCity北京depDate2024-06-01_appKeyh5huazhu_signxxx_time1717...这里有几个关键参数需要解释depCity和arrCity出发地和目的地注意这里传的是城市名称不是机场三字码。如果你需要精确到机场还需要额外的映射关系。depDate出发日期格式是YYYY-MM-DD。_sign签名参数。这个值我在开发初期是直接从浏览器复制过来用的后来发现它有有效期过期之后请求会返回签名验证失败。_time请求时间戳用于防止请求重放。我当时的破局思路是这样的既然手动维护_sign不现实那就换一种半自动的方式——先用Selenium或者浏览器插件的方式人工触发一次请求把最新的完整URL和Header复制出来再拿给requests去用。这种方式虽然不能做到完全自动化但至少能保证我的脚本在签名过期之前能够有效运行。import requests import json def fetch_flight_data(city_from, city_to, date, headers, cookies): 发起机票搜索请求 params { depCity: city_from, arrCity: city_to, depDate: date, # 下面这些参数需要从浏览器复制 _appKey: your_app_key, _sign: your_sign, _time: your_timestamp, } url https://www.fliggy.com/async/search/priceList try: response requests.get( url, paramsparams, headersheaders, cookiescookies, timeout15 ) response.raise_for_status() # 如果状态码是4xx/5xx这里会抛出异常 return response.json() except requests.exceptions.RequestException as e: print(f请求失败: {e}) return None有一个细节我想多说一句cookies参数。飞猪的接口对Cookie的依赖非常重特别是登录态的Cookie。你不带Cookie去请求返回的数据可能只是一个空列表或者直接跳转到登录页。Cookie的获取方式跟Header一样——从浏览器DevTools里复制。你需要在请求头信息里找到Cookie这个字段把它完整复制到脚本里。我还试过requests.Session()的方式把Cookie绑定到Session对象上这样同一个Session里的多次请求会自动带上这些Cookie不需要每次手动传def create_session_with_cookies(cookie_str): 创建带Cookie的请求Session session requests.Session() # 把Cookie字符串解析成字典 cookies {} for item in cookie_str.split(;): key, value item.strip().split(, 1) cookies[key] value session.cookies.update(cookies) return session3.3 实时调度与低延迟策略搞定单次请求之后下一个要解决的问题是实时——如何让脚本按照预定策略持续运行。我用的方案是主循环 APScheduler结合的方式。先说主循环方案这是最简单的import time import random def run_monitor_loop(session, cities_list, date, interval60): 主循环监控方案 print(监控启动按 CtrlC 停止) while True: for city_from, city_to in cities_list: data fetch_flight_data(city_from, city_to, date, session) if data: flights parse_ticket_data(data) process_and_alert(flights) # 每个城市之间间隔3-5秒避免请求过于密集 time.sleep(random.uniform(3, 5)) # 完成一轮全量扫描后进入自适应等待 next_interval adapt_interval(interval) time.sleep(next_interval)这里有个很关键的自适应逻辑adapt_interval()。它的功能是根据最近几轮抓取结果中折扣变化的情况动态调整下一轮的等待间隔。如果连续3轮都没有任何价格变化就把间隔拉长如果刚发现一次价格跳动就缩短间隔紧盯着抓。def adapt_interval(current_interval, changed_recentlyFalse): 自适应间隔调整策略 if changed_recently: # 刚发生过价格变化提高抓取频率 return max(20, int(current_interval * 0.5)) else: # 数据稳定降低抓取频率 return min(300, int(current_interval * 1.5))跑起来之后我发现主循环方案有一个不便之处它会把调度逻辑和业务逻辑耦合在一起如果你的监控任务复杂了比如同时监控多个城市对、多组日期代码会越来越乱。这时候APScheduler的优势就出来了——它能让你把抓取任务定义成一个独立的函数然后单独去配置它的执行计划。from apscheduler.schedulers.blocking import BlockingScheduler def scheduled_flight_task(): 定时任务抓取指定航线的最新折扣信息 session create_session_with_cookies(COOKIE_STR) data fetch_flight_data(杭州, 北京, 2024-06-01, session) if data: flights parse_ticket_data(data) process_and_alert(flights) # 创建调度器 scheduler BlockingScheduler() # 每60秒执行一次任务 scheduler.add_job(scheduled_flight_task, interval, seconds60, max_instances1) print(调度器已启动按 CtrlC 停止) scheduler.start()我最终的方案是把两种方式结合了外层用APScheduler控制每个城市对的独立轮询周期内层用自适应逻辑控制单城市对内部的请求密度。这样既能做到多任务隔离又能保持足够的灵活性。3.4 数据落地与预警通知实时抓取的数据光在命令行里打印出来是没用的得想办法落地和通知。数据落地的存储方案我分了三个层级原始JSON数据存档把每次接口返回的完整JSON dump成一个文件文件名带时间戳data/raw_20240601_153000.json这样后续要做任何重算、排查问题都有最原始的数据可查。结构化数据存储用pandas把解析出来的字段整理成DataFrame再追加写入CSV文件。每个城市对单独一个CSV字段按抓取时间、航班号、出发时间、到达时间、原价、折扣价、折扣率、剩余票量来组织。变动日志记录哪些航班在什么时间点发生了价格变化变化幅度是多少这是后面做数据分析和预警的基础。import pandas as pd from datetime import datetime def save_to_csv(flights, csv_path): 把解析后的航班折扣数据追加写入CSV if not flights: return df pd.DataFrame(flights) df[capture_time] datetime.now().strftime(%Y-%m-%d %H:%M:%S) # 第一次写入时带上表头后续追加不带表头 import os if os.path.exists(csv_path): df.to_csv(csv_path, modea, headerFalse, indexFalse, encodingutf-8-sig) else: df.to_csv(csv_path, modew, headerTrue, indexFalse, encodingutf-8-sig)预警通知这里我尝试过两种方案。先说邮件通知通过smtplib发送邮件配置好SMTP服务器地址、账号密码用的是授权码不是登录密码就能实现检测到特价机票时自动发邮件给你的效果。但这有个痛点——邮件有延迟而且很容易被扔进垃圾箱。再说即时通讯工具的通知方式很多团队在用飞书或钉钉它们的机器人Webhook机制非常适合做这种实时预警。import requests import json def send_feishu_alert(flights): 通过飞书机器人Webhook发送特价预警 webhook_url https://open.feishu.cn/open-apis/bot/v2/hook/your_webhook_id # 只保留折扣力度在7折以下的航班 hot_deals [f for f in flights if f[discount_rate] and float(f[discount_rate]) 7.0] if not hot_deals: return message 【特价机票预警】\n for deal in hot_deals[:5]: # 最多展示5条 message f航班 {deal[flight_no]}: {deal[dep_city]} - {deal[arr_city]}, message f时间 {deal[dep_time]}-{deal[arr_time]}, message f原价 {deal[original_price]}元, 折扣价 {deal[discount_price]}元, message f{deal[discount_rate]}折\n payload {msg_type: text, content: {text: message}} response requests.post(webhook_url, jsonpayload, timeout5) if response.status_code 200: print(预警消息已发送) else: print(f预警消息发送失败: {response.text})4. 常见问题与排查技巧实录4.1 接口返回空数据怎么办这是我在项目初期遇到的最多的问题。明明在浏览器里能看到数据但用requests请求却返回空列表或者一个空壳结构。我排查下来原因主要集中在以下几个方向第一缺少必要的Cookie。飞猪的搜索接口如果你没有携带任何Cookie服务端大概率直接拒绝返回数据或者返回一个需要登录的提示。排查方式是在Network面板里找到那条真实的搜索请求把它的完整Cookie复制出来替换到脚本里。第二签名过期。_sign参数在短时间内是有效的我实测下来大概在几百秒到几十分钟不等一旦过期接口会返回无效签名或者签名过期之类的错误。这个没有太好的自动化方案只能定期重新从浏览器复制。这也是为什么我一直在强调——这个项目里半自动是常态全自动是理想。第三请求头不齐。有些接口对Referer和Origin字段做校验少了一个就不会返回数据。解决办法很简单打开DevTools把真实请求的所有Header全部复制过来逐项对齐。我把排查过程整理成了一个小流程步骤是固定的检查状态码——如果不是200优先排查IP是否被限制、Cookie是否失效。检查响应结构——如果返回了JSON但不是预期结构先打印完整的response.json()看看。对比与真实的请求差异——把脚本里发的请求Headers和浏览器里的逐项比对一个都不要漏。4.2 页面结构变了怎么办飞猪前端的页面结构迭代得很频繁今天用的XPath路径明天可能就变了。我踩过的坑是有一次我写了一个基于class名的XPath表达式跑得好好的结果第三天就失效了一查发现是前端把整个列表区域的重构了class名从price-list改成了ticket-list。这个问题的根本解法是尽可能从接口的JSON里取数而不是从HTML里取数。JSON的结构相对稳定因为它是后端接口的返回格式后端的改动成本比前端高得多所以稳定性也高得多。如果你确实需要在HTML上做XPath我有一个经验尽量用结构关系而不是具体class名来写表达式。比如用//div[contains(class, flight)]//span[contains(text(), 折)]这种模糊匹配比写死//div[classprice-item-3rd-2024]//span[2]要抗变更得多。4.3 如何优雅地处理请求被风控请求发得过于频繁迟早会触发风控。飞猪的风控策略我观察到的现象是先是返回一个需要滑块验证的HTML页面如果你执意继续高频请求会升级到IP级别的临时封禁封禁时间从几分钟到几小时不等。面对这种情况我的建议是第一严格遵守请求频率底线。单个IP的请求频率不要超过每秒1次最好控制在每5秒1次以下。你想想一个正常人再怎么猛刷页面也不太可能1秒内连续点十次搜索——你以为你在追求实时在风控系统看来这就是典型的机器行为。第二做好被限制时的降级策略。脚本里要写一个退避机制当检测到返回的是验证码页面判断依据可以是响应内容中是否包含silder、captcha、验证等关键词时立即停止当前任务等待一段较长的时间比如15分钟再继续。这个降级策略我实际跑下来非常有效它让你的脚本看起来懂规矩——被提醒了就停下来过一会儿再试。def is_captcha_page(response_text): 检测返回的页面是否是验证码页面 captcha_keywords [滑块, captcha, 验证码, 滑动验证] return any(keyword in response_text for keyword in captcha_keywords) def fetch_with_backoff(url, headers, cookies, max_retries3): 带退避机制的请求 retry_count 0 while retry_count max_retries: response requests.get(url, headersheaders, cookiescookies, timeout15) if is_captcha_page(response.text): print(f触发风控验证等待15分钟后重试...) retry_count 1 time.sleep(15 * 60) continue return response return None4.4 数据重复与波动误报的处置实时抓取最烦的一件事就是数据抖动。同一个航班可能上一秒显示8.5折下一秒变成8.5折但价格整数部分变了1块钱再下一秒又变回来。这种抖动如果直接当成价格变化去触发预警你的手机会被通知轰炸到崩溃。我的解决方案是引入一个价格变化判定阈值。只有当折扣价的变动幅度超过3%或者绝对值超过50元时才认为这是一次有效变化否则只是更新数据库中的时间戳不触发预警。def is_significant_change(old_price, new_price, threshold_percent3): 判断价格变化是否达到预警阈值 if old_price is None or new_price is None: return True # 首次抓取到的数据视为重要变化 change_percent abs(new_price - old_price) / old_price * 100 return change_percent threshold_percent还有一个需要处理的场景是数据去重。飞猪的搜索结果里同一个航班号可能会在多个列表位置出现直飞和联程都包含它如果不做去重逻辑你的数据表里会堆满重复记录。去重的逻辑很简单以航班号出发日期作为唯一键后面抓到的数据如果这个键已经存在就更新价格字段而不是新插入一行。4.5 关于实时的理性期待最后说说我对实时这件事的重新理解。做这个项目的过程中我逐步意识到机票折扣信息并不是一个需要秒级实时获取的数据。机票价格的变化频率远低于股票行情也低于加密货币的波动。它的合理监控粒度应该是在分钟级到小时级之间。过度追求秒级实时只会带来两个后果一是请求量呈指数级上升风控风险大大增加二是数据噪音变多——你抓到的所谓实时变化大部分都是系统缓存刷新造成的假波动。我最终跑的方案是基础轮询间隔120秒出现价格变化后自适应缩短到30秒连续稳定3轮后恢复到120秒。实测下来一场航班的促销放价从放出到被我捕捉到延迟基本控制在2分钟以内。对抢特价机票这个场景来说这个延迟完全够用。如果对实时性有更高要求的场景我的建议是优先考虑目标平台是否提供官方的低价提醒订阅接口。飞猪本身在前端就提供了降价提醒功能原理是用户订阅后由服务端主动推送。如果你想做的是给自己订阅降价提醒直接使用平台自己的功能比任何爬虫方案都更高效、更稳定、更合规。最后的一点个人体会跑了几周这个抓取脚本之后我最大的感受是飞猪这类平台的接口短期内手工复制签名、半自动运行是可行的但如果你想做一个长期稳定运行的生产级爬虫需要投入的逆向成本会成倍增长——签名算法会变、风控策略会升级、页面结构会改。这就像一场没有终点的军备竞赛你的维护成本会一直居高不下。所以我会把这种接口级抓取用在真正的刚需场景上比如短期内要买票集中盯一个航线比如方案验证用真实数据来跑通自己的分析模型。如果你只是想拿个脚本长期盯着所有航线的特价我更推荐组合几种数据源的思路——公开的折扣信息聚合平台加上目标航司官网的直营价格合理利用效果反而更好。最后分享一个实操小技巧飞猪页面上有些字段在接口里是加密过的但我发现折扣力度这个信息会同时出现在页面标题的文案里比如7.5折起。当你接口解析遇到困难时不妨退回一步用lxml去提取页面上的标题文本做补充很多看似搞不定的数据兜兜转转其实早就展示在了页面上。灵活切换接口直取和页面解析两种方式才是爬虫实战中最实用的生存技能。