用Python抓包App接口,实现海鲜价格监控与自动提醒
家里有位“海鲜采购总管”你就明白我为什么折腾这个项目了。我家那位隔三差五就念叨同一句话“基围虾今天又贵了你到底买不买”等来等去要么涨价要么卖完。与其碰运气捡漏不如自己动手给盒马的价格接口做个监控抓包拿到 App 里的真实 API用 Python 定时把海鲜价格抓下来存库一旦降到心理价位就提醒我下单。整个项目做完我顺手把抓包分析、接口复刻、数据入库、定时调度这一整套流程总结成这篇文章。如果你有 Python 基础知道 requests 怎么用又想知道别人是怎么从 App 里抓数据的那这篇文章应该能让你少走不少弯路。1. 项目立项为什么选“抓包 API”而不是“页面爬虫”1.1 需求场景等打折等到心累先说清楚我要解决的问题。盒马的商品价格不是固定不变的受进货、促销、库存影响挺大尤其是海鲜这类短保品早上和晚上的价格可能差出一截。我想做的不是看一次价格而是持续观察某一个商品的 price 波动哪种虾什么时段最便宜、周几会有折扣、降价的时候能不能及时收到提醒。把需求拆一下其实就三条拿到某个商品在某个门店的实时价格、持续记录价格变化、价格低于阈值时通知我。听起来不复杂但怎么“拿到实时价格”这一步恰恰是大多数人卡住的地方。1.2 为什么选“抓包 API”而不是“页面爬虫”很多人第一反应是直接用 requests 爬盒马的商品页面然后去解析 HTML。这个方案我一开始也试过结果很尴尬盒马的 H5 页面数据是异步加载的页面源代码里根本没有价格数字你得去找它背后的数据接口。既然要找数据接口那不如直接从根上拿——用抓包工具看 App 到底访问了哪个 URL、传了什么参数、返回了什么结构。把 App 和服务器之间的对话摸清楚然后用 Python 模拟它这种思路比解析 HTML 稳健得多。还有一个原因App 端接口的数据结构是 JSON解析起来比嵌在 HTML 里的标签干净太多了字段明确甚至有现成的商品名、价格、规格、库存状态。拿到 JSON 之后直接人脸识别级别地对着字段取数就行逻辑简单出错概率也低。1.3 整体方案和落地顺序我最后定的技术方案大致如下抓包工具Charles负责看 HTTPS 请求和响应内容抓包方式手机和电脑连同一 WiFi手机设置 HTTP 代理Charles 帮我们解密 HTTPS 流量分析重点找到价格接口的 URL、请求参数、必要的 Header 和 Cookie爬虫框架不引框架直接 requests Session轻量、可控、好排错数据存储SQLite SQLAlchemy个人项目不需要上 MySQL 那么重定时调度APScheduler每隔半小时跑一次控制频率稳步监控提醒控制台打日志为主可选接一个群机器人 Webhook整个落地过程我按“抓包—分析—复刻—落库—调度”的顺序来讲每一步都会标出我当时踩坑的点。2. 抓包环境搭建把盒马 App 的流量看穿2.1 工具选型Charles 还是 Fiddler抓包工具选哪个网上吵得挺凶。我自己 Windows 和 Mac 都用过简单说下感受工具平台上手难度核心优势CharlesWindows / macOS低界面直观Host 过滤方便支持全量历史搜索FiddlerWindows 为主中免费插件多能写脚本断点改包mitmproxy全平台中高Python 脚本控制适合把抓包过程自动化日常排查我用 Charles因为它的会话列表里能直接按域名过滤搜索历史请求也方便——这在后面“找接口”环节帮了大忙。如果你只用 WindowsFiddler 也完全够用思路都是互通的下面讲的证书和代理设置大同小异。2.2 HTTPS 抓包配置全流程抓包的核心思路其实一句话让手机的流量绕到我们的电脑上转一圈再把 HTTPS 的流量解密成明文。我一个一个步骤拆开你照着做就行。第一步启动 Charles记住电脑的局域网 IP一般是Settings – Network里能看到Charles 的默认代理端口是8888。第二步把手机和电脑连到同一个 WiFi然后在手机的 WiFi 设置里选择“手动代理”填入电脑 IP 和端口 8888。这一步之后手机访问网页、打开 App 的流量都会经过 Charles但这时候 HTTPS 还是加密的你只能看到一堆CONNECT记录看不到具体内容。第三步安装并信任 Charles 的根证书。手机浏览器访问chls.pro/ssl下载证书并安装。iPhone 装完证书还要去设置 – 通用 – 关于本机 – 证书信任设置里手动开启信任不然抓了一天全是乱码。这里必须多说一句证书不信任是新手遇到最多的拦路虎只要看到 HTTPS 请求体是乱码或者“No request received”八成就是证书没信任成功。第四步在 Charles 的Proxy – SSL Proxying Settings里勾选开启并添加*:443表示解密所有 HTTPS 流量。这一步不做只会看到加密流量一样白搭。第五步验证解密是否成功在手机上随便打开一个网页回到 Charles 看请求列表如果能看到具体的 URL 路径、Header 和返回内容说明证书装好了。如果只看到443端口的 CONNECT 但没有展开内容说明 SSL Proxying 没生效。提示Android 7.0 及以上版本的 App 默认不完全信任用户安装的证书抓包时可能出现部分请求是黑盒、部分请求能解析的情况。盒马 App 我实测下来整体能看但如果你以后抓其他加固过的 App 碰到类似情况需要的是研究如何把证书装进系统证书库这部分属于另一门课了不在本文范围。2.3 关键一步在几十个请求里捞出价格 API证书配好之后手机打开盒马 App随便进一个海鲜商品的详情页然后回 Charles你会看到海量的请求埋点、首页推荐、购物车状态、用户信息……一眼看过去全是噪音。这里我分享三个经验能迅速从噪音里捞出目标接口第一用底部 Filter 栏直接过滤域名。盒马的接口域名通常是freshhema.com或hemaos.com这类你在 Filter 里输入hema或者fresh列表会瞬间干净很多。App 的请求很多但目标商品的详情请求往往集中在某个固定的 API 域名下。第二用内容搜索做定位。Charles 的Edit – Find可以搜索历史流量里的文本内容。你打开商品详情页之前先想好一个特征词比如海鲜名称或者“price”这个字段名然后在 Charles 里直接搜。搜到的那条请求往往就是你要找的接口。第三反向定位。找到一条返回数据里明显包含商品信息的 JSON 响应点进去看对应的请求地址。比如我这边看到某个响应里包含skuId、price、originPrice那这个接口基本就是详情页价格接口。你要做的是把这条请求完整地“抄”下来包括 URL、请求方法、请求头、请求体。我当时抓到的是POST /gw/buy/v1/detail之类的地址请求体里带着storeId门店 ID、skuId商品 ID、channel渠道标识等参数。结构和具体路径不同版本会有变化但找到它的方法是不变的过滤域名、搜特征词、定位响应里带价格字段的那条请求。3. 核心 API 分析与请求复刻3.1 参数逐个拆解哪些能改哪些不能动抓到接口之后别急着写代码先把请求体和响应结构看懂。以我当时抓到的详情接口为例请求体大致的 JSON 长这样{ storeId: 9, skuId: 1000000123, channel: app, variable: {} }这里面最核心的是storeId和skuId。storeId决定价格来自哪个门店你人在哪个城市就填对应门店的 ID这个 ID 可以在 App 首页接口返回的门店列表里找到skuId就是商品 ID每个商品一个稳定不变适合作为监控的主键。channel这个参数一般别去动它它标识了请求来自 App 端。有些接口会把 Web 端和 App 端的请求区分对待如果你把channel改成别的值返回的数据结构可能是另一套平白增加解析成本。再看响应一个典型的返回包长这样{ code: 0, msg: success, data: { sku: { skuId: 1000000123, name: 活基围虾, price: 9900, originPrice: 12900, stock: 50, spec: 500g/盒 } } }这里有个大坑必须提醒你很多生鲜电商的价格字段是“分”不是“元”。9900对应的是99.00元12900对应129.00元。你要是图省事直接把数字存进去后面做价格对比时会发现差了 100 倍。我建议在解析层统一除以 100 并保留两位小数再入库后面所有比较、展示就都不会出问题。3.2 签名和请求头的处理思路大型 App 的接口通常都会带签名参数或者校验 Header盒马这边我测试时发现详情接口的校验相对宽松直接带上登录后的 Cookie把skuId换掉多数情况下能正常返回数据。但这不代表你也可以忽略签名和 Header这里我按“从轻到重”的顺序讲一下处理思路。第一优先看 Header。完整的User-Agent、Content-Type、Accept必须和 App 发的保持一致尤其是 UA。UA 里往往带 App 的版本号、渠道、机型信息服务器经常用它来判断请求是不是来自真机。我是直接从抓包记录里复制 UA 出来用的这个习惯建议你也保留。第二看 Cookie。App 登录后的会话数据会体现在 Cookie 里详情接口如果返回“未登录”或者“需要授权”大概率是 Cookie 过期了。解决办法很简单重新打开盒马 App 登录一次然后回到 Charles 复制新的 Cookie。这个操作看起来笨但对个人监控项目来说是最稳的。第三再看签名。如果接口里有类似sign、_sign这样的参数说明请求参数是参与签名的。常见做法是把所有参数按字典序排列后拼起来加上一个固定密钥做 MD5 或者 HMAC。这个密钥藏在 App 的代码里能不能找到取决于逆向经验。但有意思的是很多 App 的签名只校验“参数被篡改”不校验“请求是否为同一时刻发起的”所以在签名失效之前你可以按固定参数模板复用抓到的 sign。换句话说只要你的storeId和skuId不变原先抓到的那一版签名可能还能用一段时间。当然如果某天你的脚本突然报签名错误别纠结回到 Charles 重新抓一次最新的请求更新一下参数模板就行。注意事项不同版本、不同接口的校验强度不一样。本文讲的是学习和技术验证思路拿自己常用的账号监控几个商品的价格属于个人合理使用范畴这一点心里要有数不要去写大规模的分布式抓取也不要用同理绕过会员体系拿你的账号没权限的数据。3.3 用 Python 还原一个可复用的请求会话分析清楚之后写 Python 就是水到渠成的事了。我用requests.Session来保存 Cookie 和默认 Header这样每次请求都会带上统一的 UA 和 Cookie不用每个请求单独传一遍# -*- coding: utf-8 -*- import time from datetime import datetime import requests API_URL https://api.freshhema.com/gw/buy/v1/detail STORE_ID 9 # 换成你常用门店的 storeId HEADERS { User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Hema/2.3.0, Content-Type: application/json, Accept: application/json, } COOKIE 换成你从 Charles 里复制的 Cookie session requests.Session() session.headers.update(HEADERS) session.cookies.update({cookie 字段: COOKIE}) # 按实际抓到的 Cookie 内容填 def fetch_sku(sku_id: str) - dict: payload { storeId: STORE_ID, skuId: sku_id, channel: app, variable: {}, } resp session.post(API_URL, jsonpayload, timeout10) resp.raise_for_status() result resp.json() if result.get(code) ! 0: raise RuntimeError(f接口返回异常: {result}) sku result[data][sku] return { sku_id: sku_id, name: sku[name], price: round(sku[price] / 100, 2), # 分转元 origin_price: round(sku.get(originPrice, 0) / 100, 2), stock: sku.get(stock, 0), captured_at: datetime.now(), } if __name__ __main__: sku_list [1000000123, 1000000456] # 换成你想监控的 skuId for sku in sku_list: try: record fetch_sku(sku) print(record) except Exception as exc: print(f[错误] {sku} 抓取失败: {exc}) time.sleep(2)这套代码的核心价值在于把“抓包拿到的东西”变成“一个可以反复调用的函数”。后面不管是入库、定时任务还是做提醒都建立在fetch_sku返回的这条记录之上。4. 数据落库与定时监控上线4.1 用 SQLAlchemy 把价格记录存进 SQLite自己做个监控数据库选 SQLite 就够了它就是一个本地文件不需要装服务端随拿随用。搭配 SQLAlchemy 的好处是你不用手写原生 SQL定义好表结构之后增删改查都是对象操作后面即使你换到 MySQL、PostgreSQL代码改动也很小。我建的表结构核心字段就这么几个sku_id、name、price、origin_price、stock、captured_at。价格必须记成浮点数时间必须带时分秒这样后面才能画出一条完整的价格走势。# model.py from datetime import datetime from sqlalchemy import create_engine, Column, String, Float, DateTime, Integer from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker Base declarative_base() class PriceRecord(Base): __tablename__ price_records id Column(Integer, primary_keyTrue, autoincrementTrue) sku_id Column(String(64), indexTrue) name Column(String(128)) price Column(Float) origin_price Column(Float, default0) stock Column(Integer, default0) captured_at Column(DateTime, defaultdatetime.now) engine create_engine(sqlite:///hema_prices.db) Base.metadata.create_all(engine) SessionLocal sessionmaker(bindengine)插入记录也非常直白from model import SessionLocal, PriceRecord def save_record(record: dict) - None: db SessionLocal() try: item PriceRecord(**record) db.add(item) db.commit() except Exception: db.rollback() raise finally: db.close()价格记录是只写不改的每次抓取都是一条新记录因此不用纠结“要不要先查一遍再更新”。等到你想看历史最低价、想画折线图的时候直接按sku_id查时间范围内的所有记录就行。4.2 调度策略既要盯得住又要克制点定时任务的方案有很多例如本机 crontab、Windows 计划任务或者 Python 进程内调度。我个人在个人项目里更喜欢 APScheduler因为写起来就是几行代码不用去管系统层面的配置也能在进程里统一处理日志和异常。# schedule.py from apscheduler.schedulers.blocking import BlockingScheduler from monitor import monitor_once def main(): scheduler BlockingScheduler() # 每 30 分钟跑一次随机延时几秒避免请求时间高度规律 scheduler.add_job(monitor_once, interval, minutes30, jitter10) scheduler.start() if __name__ __main__: main()这里要特别说下频率控制。监控的目的是“捕捉降价”不是“给盒马服务器做压力测试”。半小时跑一次、一次只查几个固定的 SKU已经足够覆盖大部分价格变化。你要是改成每分钟拉一次或者把全店商品都拉下来那基本就是在给自己找封号的麻烦请求越规律越容易被识别。jitter10这个参数能让每次执行时间有 10 秒以内的随机偏移算是一个简单又实用的“去规律化”手段。另外我建议在monitor_once里给单次请求设置超时和最多两次重试连续失败三次就跳过本轮并打印告警。监控脚本挂了不可怕可怕的是挂了之后你还不知道价格降了又涨回去你还在傻等。4.3 不花钱的“预警系统”降价提醒怎么发监控数据存下来之后最关键的一步是“降价提醒”。如果每次都要打开数据库查价格那这监控就没什么意义了。我加了一个很朴素的判断逻辑把当前价格和指定时间段内的最低价做对比一旦当前价低于历史最低价的一定比例就触发提醒。def check_low_price(record, threshold_ratio0.9): db SessionLocal() try: # 取这个 SKU 在最近 7 天内的最低价 since datetime.now() - timedelta(days7) rows ( db.query(PriceRecord.price) .filter( PriceRecord.sku_id record[sku_id], PriceRecord.captured_at since, ) .all() ) if not rows: return False min_price min(row[0] for row in rows) # 当前价 历史最低的 90%说明价格进入低位区间 return record[price] min_price * threshold_ratio finally: db.close()触发提醒之后怎么通知自己新手最容易忽略这一步。我个人推荐用企业微信群机器人或者 Server酱这类工具本质就是往一个 Webhook 地址 POST 一段 JSON手机上就能收到推送。如果你只是想本地验证效果先print一段醒目的文字也完全够用等确认整个流程稳定了再接上消息推送。提醒文案别光发一个价格我一般会把商品名、当前价、历史最低价、门店信息都带上方便自己判断要不要冲到 App 里去下单。5. 实操中踩过的坑与排查实录5.1 常见问题速查表这几天跑下来踩坑是少不了的。我把最常见的现象、原因、处理方式整理成一个速查表你要是碰到类似问题直接对着表排查现象可能原因解决办法HTTPS 请求全部乱码证书没安装或没信任按 2.2 节重装证书并开启 SSL ProxyingCharles 里只有 CONNECT没有具体请求SSL Proxying 没开启在Proxy – SSL Proxying Settings添加*:443API 返回未登录/授权失败Cookie 过期重新打开 App 登录重新复制 Cookie请求报 403 或频繁超时请求频率过高被限制拉大请求间隔降低单次请求 SKU 数量价格数字不对动不动差 100 倍价格单位是“分”解析时统一除以 100 再入库某天脚本突然报 Sign 错误App 升级或签名校验收紧重新抓包复制最新的请求头和参数模板手机上部分 App 抓不到包Android 7 证书信任限制考虑换低版本设备或研究系统证书安装方案这里面我最想强调的是“Cookie 过期”这个问题。你可能会遇到一种情况第一天跑得好好的第三天突然全部失败。别慌先别急着改代码打开 Charles 看看 App 现在的请求和你脚本里的 Header 有什么区别。90% 的情况是登录态过期了重新登录一次就能解决。5.2 那些代码之外的忠告最后说几句务实的话。这种抓包 接口复刻的思路不只是盒马能用它适用于绝大多数“看着像黑盒、实际上只是一个 HTTPS 接口”的 App。你已经掌握的是一套方法论换一个目标流程几乎一模一样配好代理、解开 HTTPS、过滤域名、搜索特征字段、复刻请求、落库调度。这套方法论的价值远比某个具体接口的地址大得多。但也正因为这套方法通用你更要知道边界在哪里。我建议你做到下面几点只监控自己账号有权限看的商品不批量拉取全量商品只做个人价格监控不把数据往外卖不要在此基础上做自动下单抢购之类的操作那既违反用户协议也会破坏正常用户的购物体验。技术本身是中性的用它解决自己生活中的小痛点顺手提升一点效率是我觉得最舒服的用法。这几天监控下来我最大的感受是比起“把海鲜价格抓下来”更难的是“坚持每天看一遍结果”。脚本写完之后真正让我受益的是自动化和提醒机制它们把一件需要人盯的小事变成了机器替你盯的系统。最后我等的基围虾也确实降到了心理价位那天晚上我打开手机下了单这个项目对我来说就算圆满收官了。如果你也想监控点什么别急着羡慕别人现成的脚本照着上面的流程自己抓一次包、跑一个定时任务这份“自己亲手搞定”的踏实感才是最有意思的部分。