news 2026/10/7 11:30:13

Python爬虫实战:飞猪机票折扣信息实时抓取完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python爬虫实战:飞猪机票折扣信息实时抓取完整指南

做机票比价这事儿,我一开始也天真地以为就是个"发个请求拿个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 技术选型清单

技术栈这块,我用了非常朴素的组合:requests+lxml+pandas+APScheduler。没上Scrapy,也没上Selenium,原因后面会说。

先说为什么不用Scrapy。Scrapy确实是个强大的框架,自带调度器、下载器、中间件、管道,但问题是它的学习曲线和项目结构都偏重。对于飞猪这种接口级抓取任务,你真正需要的只是一个能发请求、能解析JSON、能存数据的"轻量管道"——用Scrapy反而有种杀鸡用牛刀的感觉。而且Scrapy的异步机制在处理这种单目标、单接口的任务时,优势完全发挥不出来。

再说为什么不用Selenium。我知道很多人遇到动态页面第一个想到的就是它——模拟浏览器打开页面,等着渲染完,再用XPath去提取。但Selenium有两个硬伤:一是资源开销大,一个无头浏览器要占300MB以上的内存,你要是做"实时抓取",等于开着一台重型卡车在市区通勤;二是容易被识破,飞猪的前端有专门检测WebDriver的脚本,你一旦用了Selenium,navigator.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;q=0.9', 'Referer': 'https://www.fliggy.com/', 'Origin': 'https://www.fliggy.com', } return headers def random_delay(base_interval=60): """在基础间隔上增加±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=北京&depDate=2024-06-01&_appKey=h5huazhu&_sign=xxx&_time=1717...

这里有几个关键参数需要解释:

  • 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, params=params, headers=headers, cookies=cookies, timeout=15 ) 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 session

3.3 实时调度与低延迟策略

搞定单次请求之后,下一个要解决的问题是"实时"——如何让脚本按照预定策略持续运行。

我用的方案是:主循环 + APScheduler结合的方式。先说主循环方案,这是最简单的:

import time import random def run_monitor_loop(session, cities_list, date, interval=60): """主循环监控方案""" print("监控启动,按 Ctrl+C 停止") 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_recently=False): """自适应间隔调整策略""" 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', seconds=60, max_instances=1) print("调度器已启动,按 Ctrl+C 停止") 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, mode='a', header=False, index=False, encoding='utf-8-sig') else: df.to_csv(csv_path, mode='w', header=True, index=False, encoding='utf-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, json=payload, timeout=5) if response.status_code == 200: print("预警消息已发送") else: print(f"预警消息发送失败: {response.text}")

4. 常见问题与排查技巧实录

4.1 接口返回空数据怎么办

这是我在项目初期遇到的最多的问题。明明在浏览器里能看到数据,但用requests请求却返回空列表或者一个空壳结构。我排查下来,原因主要集中在以下几个方向:

第一,缺少必要的Cookie。飞猪的搜索接口如果你没有携带任何Cookie,服务端大概率直接拒绝返回数据,或者返回一个需要登录的提示。排查方式是:在Network面板里找到那条真实的搜索请求,把它的完整Cookie复制出来,替换到脚本里。

第二,签名过期。_sign参数在短时间内是有效的(我实测下来大概在几百秒到几十分钟不等),一旦过期,接口会返回"无效签名"或者"签名过期"之类的错误。这个没有太好的自动化方案,只能定期重新从浏览器复制。这也是为什么我一直在强调——这个项目里"半自动"是常态,"全自动"是理想。

第三,请求头不齐。有些接口对Referer和Origin字段做校验,少了一个就不会返回数据。解决办法很简单:打开DevTools,把真实请求的所有Header全部复制过来,逐项对齐。

我把排查过程整理成了一个小流程,步骤是固定的:

  1. 检查状态码——如果不是200,优先排查IP是否被限制、Cookie是否失效。
  2. 检查响应结构——如果返回了JSON但不是预期结构,先打印完整的response.json()看看。
  3. 对比与真实的请求差异——把脚本里发的请求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[@class="price-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_retries=3): """带退避机制的请求""" retry_count = 0 while retry_count < max_retries: response = requests.get(url, headers=headers, cookies=cookies, timeout=15) if is_captcha_page(response.text): print(f"触发风控验证,等待15分钟后重试...") retry_count += 1 time.sleep(15 * 60) continue return response return None

4.4 数据重复与波动误报的处置

实时抓取最烦的一件事就是"数据抖动"。同一个航班,可能上一秒显示8.5折,下一秒变成8.5折但价格整数部分变了1块钱,再下一秒又变回来。这种抖动如果直接当成"价格变化"去触发预警,你的手机会被通知轰炸到崩溃。

我的解决方案是:引入一个"价格变化判定阈值"。只有当折扣价的变动幅度超过3%(或者绝对值超过50元)时,才认为这是一次"有效变化";否则,只是更新数据库中的时间戳,不触发预警。

def is_significant_change(old_price, new_price, threshold_percent=3): """判断价格变化是否达到预警阈值""" 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去提取页面上的标题文本做补充,很多看似搞不定的数据,兜兜转转其实早就展示在了页面上。灵活切换"接口直取"和"页面解析"两种方式,才是爬虫实战中最实用的生存技能。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 11:29:22

MES系统落地实战:从PPT到产线稳定运行的5大核心模块

简介&#xff1a;本资源是一份面向制造业数字化转型从业者、自动化工程师及MES系统初学者的深度入门课件&#xff0c;聚焦制造执行系统&#xff08;MES&#xff09;的核心功能与落地实践。内容涵盖MES定义、架构设计、典型模块&#xff08;如设备监控管理&#xff09;、工业4.0…

作者头像 李华
网站建设 2026/10/7 11:29:10

AI大模型零基础学习路径:从环境搭建到微调部署的实战指南

1. 这套“600集AI大模型零基础教程”到底在教什么&#xff1f;——先拆穿标题里的信息密度陷阱 看到“【全600集】(允许白嫖)绝对是26年最好的AI大模型零基础全套教程”这个标题&#xff0c;我第一反应不是点开&#xff0c;而是拿起笔在本子上画了三道横线&#xff1a;一道划在…

作者头像 李华
网站建设 2026/10/7 11:27:35

AI智能体技能库设计:从工具调用到动态编排实践

1. 内容整体设计与思路拆解1.1 核心需求解析在AI功能开发领域&#xff0c;agent-skills是一个很特别的项目方向&#xff0c;它解决的是大语言模型和AI智能体在“动手做事”时的能力边界问题。我把这块的核心思路拆成三层来说。我们先从最基础的痛点聊起。如果你用过早期的AI助手…

作者头像 李华
网站建设 2026/10/7 11:27:31

C++ STL中map和set的高效使用:有序容器、红黑树与工程实践

写map和set之前&#xff0c;先说说我为什么觉得这两个容器被严重低估了。在C的STL里&#xff0c;map和set是我最愿意跟新人聊的两个关联容器&#xff0c;因为只要用对了&#xff0c;很多原本要手写排序、查找、去重的场景&#xff0c;几行代码就干净利落地解决了。map就好比一本…

作者头像 李华
网站建设 2026/10/7 11:27:16

智慧电厂工程落地指南:UWB定位、智能两票与三维电子围栏实战

简介&#xff1a;本资源是一份面向电力企业及发电集团数字化转型实践者的系统性建设方案&#xff0c;聚焦智慧电厂整体架构设计与落地路径&#xff0c;解决传统电厂在安全管控、运行优化、智能维护与科学决策等方面的数字化升级痛点。文档以Word格式呈现&#xff0c;共1个.docx…

作者头像 李华
网站建设 2026/10/7 11:25:47

ponytail插件与技能实战:从零搭建高效工作流

1. 从“ponytail”这个词说起&#xff1a;它到底是什么第一次看到“ponytail”这个词&#xff0c;大多数人脑子里蹦出来的画面是扎在脑后的那束马尾辫。但在技术圈和效率工具圈里&#xff0c;ponytail 已经悄悄变成了一个代名词——它指的是一类把复杂操作收束成一条主线、像扎…

作者头像 李华