ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Keep运动数据抓取实战:Python爬虫与接口分析全流程

Keep运动数据抓取实战:Python爬虫与接口分析全流程 跑步、骑车、撸铁的朋友们你们有没有遇到过这种情况Keep里记录了几百次运动想做个年度总结App里只能看个大概趋势想把数据导出来自己分析平台又没提供像样的导出功能想按周、按月统计一下某项训练的累计时长翻手机翻到怀疑人生。我自己就是因为这个痛点干脆写了一套Python脚本把Keep的运动数据采集下来落到本地数据库里想怎么分析就怎么分析。这篇文章就把这套方案从抓包、接口分析到代码实现、数据可视化完整拆开讲一遍适合有Python基础语法、想通过真实项目练手爬虫和数据分析的朋友。先说清楚这个项目能做什么只要你登录过Keep就能把个人主页里的运动记录、训练时长、卡路里消耗、心率区间这些数据通过Python程序自动拉取到本地存成SQLite数据库或者CSV文件然后结合pandas和matplotlib做趋势分析、类型分布统计相当于给自己建了一套私人运动数据中台。下面进入正题我把整个实操过程拆成五个部分来讲。1. 项目思路与技术选型为什么选择接口抓取这条路1.1 三种可行方案对比在做这个项目之前我先评估了几条技术路线这里把它们的优劣讲清楚方便你根据自己情况选。第一条路是App抓包加接口调用也就是本文最终采用的方案。通过Charles或Fiddler这类抓包工具在手机上安装证书后代理流量观察Keep App在登录和刷数据时到底请求了哪些接口拿到鉴权Token后直接用Python的requests库模拟请求。这个方案的优势是稳定、轻量接口返回的是干净的JSON数据字段清晰解析起来非常舒服。对爬虫初学者来说还能完整经历一遍抓包、分析请求、构造参数、解析响应的全流程技术含量和学习价值都很高。第二条路是浏览器自动化用Selenium或Playwright操作Keep网页版登录和翻页。这条路的问题是Keep网页版的功能比App少很多很多数据在网页端根本看不了而且浏览器自动化脚本跑起来很笨重还得处理各种反爬验证效率差不少。除非你对接口抓包实在没把握否则不建议优先走这条路。第三条路是手动导出或者手工记录。Keep提供的导出能力非常有限基本只能看个汇总拿不到明细数据。手工往Excel里记就更不现实了我一年跑了三百多次真要手动录还没录完就放弃了。所以最终结论很明确抓包分析Keep接口然后用Python直接调接口取数是性价比最高的方案。整个流程跑通之后每次同步数据只需要跑一遍脚本几十秒就搞定以后再也不用手工整理了。1.2 技术栈选型与准备清单技术选型上我没有用重型框架全部选的是Python生态里的轻量组件Python 3.9及以上建议直接用官方安装包装最新稳定版requests库负责发HTTP请求pandas用来做数据清洗和规整SQLite3Python内置的数据库模块零配置适合个人项目matplotlib做趋势图和分布图抓包工具Charles或Fiddler两者选一个就行我用的是Charles。这里多解释一句为什么选SQLite而不是MySQL或者MongoDB。个人运动数据也就是几千条到几万条的量级SQLite一个单文件就能搞定备份直接拷贝文件查询性能也完全够用。没必要为一个单人项目专门搭数据库服务过度设计是新手最容易犯的毛病。环境方面你需要一台电脑和一部装了Keep的手机电脑和手机连同一个WiFi这样抓包工具才能代理到手机的流量。Windows和macOS都能跑下面的操作步骤我尽量按通用的方式来写。2. 登录认证与Token获取万事开头难2.1 抓包环境配置抓包是整个项目最关键的环节Token能不能拿到接口路径看得清不清楚都取决于这一步。先以Charles为例讲配置流程。电脑上安装并打开Charles默认代理端口是8888。在Charles的菜单里找到Proxy Settings确认HTTP代理是开启状态然后记下电脑的局域网IP地址比如192.168.1.100。接下来在手机端设置WiFi代理手动填上电脑IP和端口8888这时候手机上的流量就会经过Charles。但光这样还不够因为Keep的接口走的是HTTPS加密协议不装证书的话抓到的全是乱码。证书配置的步骤是手机浏览器访问chls.pro/ssl下载并安装Charles的SSL证书。iOS用户注意安装完证书之后还要到设置-通用-关于本机-证书信任设置里把Charles证书的完全信任开关打开这一步特别容易漏漏了之后会一直提示SSL握手失败。Android用户则根据系统版本不同有的需要在设置里允许用户证书对App流量解密。配置完后在Charles里勾选SSL Proxying添加一条规则Host填api.gotokeep.com端口填443这样只解密Keep的流量其他App不受影响。配置好之后打开手机上的Keep App随便浏览一下运动记录页面回到Charles你就能看到一堆发往api.gotokeep.com的HTTPS请求。看到这些绿色小锁图标变成明文内容就说明抓包成功了。2.2 从登录请求里拿到Token抓包拿到流量之后接下来要解决的是鉴权问题。Keep的接口是标准的OAuth2.0风格客户端登录成功后会拿到一个access_token后续所有请求都在HTTP头部带上这个Token格式是Authorization: Bearer 具体的Token字符串。我在实际抓包时发现Keep App启动后会通过一个刷新接口自动续期Token所以抓登录请求的话不一定要真的输入账号密码重新登一次。最简单的做法是在保持登录状态的情况下杀掉App重新打开观察启动时的第一个请求通常响应体里就带着完整的用户信息和新的Token。Token在Charles的请求头里一眼就能看到复制出来备用。Token抓到手之后千万不要直接硬编码写在脚本里。我习惯用config.py或config.json单独存一份方便后续失效时快速替换。之前图省事把Token写在代码里结果每次过期都要翻代码修改后来用配置文件隔离整个过程清爽多了。2.3 构造稳如老狗的请求头把Token配置好之后请求头也不是随便写几个字段就能通的。我踩过更新看到一个一个坑这里把我的经验总结一下。Keep接口对User-Agent有校验如果你直接用requests默认的User-Agent大概率会收到403或者验证码提示。正确的做法是从抓包请求里原样复制手机的User-Agent字符串包含手机型号和App版本号的信息。我用的是iOS设备UA大概长这样Keep/7.x.x (iPhone; iOS 16.x; Scale/3.00)。保持这个UA不变每次请求都用同一个能大幅度降低被风控的概率。除了Authorization和User-Agent还需要带上Accept为application/json以及Content-Type根据请求方法区别设置。整体请求头如下面的代码所示headers { Authorization: Bearer TOKEN, User-Agent: Keep/7.18.0 (iPhone; iOS 16.5; Scale/3.00), Accept: application/json, Accept-Language: zh-CN,zh;q0.9, }这里有个实操心得我在刚开始写脚本时习惯把抓包里看到的每个请求头都原样照搬后来发现很多字段根本不影响请求结果。最简单的原则是核心保留Authorization、User-Agent、Accept三个即可请求体类接口再多加一个Content-Type。header越精简之后维护越省心。3. 核心接口解析与数据采集代码实现3.1 接口速查与返回结构说明Token和请求头准备好之后就能正式调接口了。Keep的接口域名是api.gotokeep.com下面这几个接口是我实测下来最常用的建议你先抓包确认一下你当前App版本的实际路径以你自己的抓包结果为准接口用途请求方法路径说明关键返回字段获取用户信息GET/users/self昵称、头像、性别获取运动统计GET/users/self/stats总运动次数、总时长、总卡路里获取运动记录列表GET/workouts运动记录数组、总数获取运动详情GET/workouts/{id}卡路里、心率、配速、轨迹先拿用户信息接口练手最合适它不需要参数返回结构也简单。我这里用requests写了一个最小请求示例import requests TOKEN 从配置文件读取的Token BASE_URL https://api.gotokeep.com def get_user_info(): url f{BASE_URL}/users/self headers { Authorization: fBearer {TOKEN}, User-Agent: Keep/7.18.0 (iPhone; iOS 16.5; Scale/3.00), Accept: application/json, } resp requests.get(url, headersheaders, timeout10) resp.raise_for_status() data resp.json() print(f用户: {data[name]}, 性别: {data.get(gender, 未知)}) return data3.2 运动记录列表与分页拉取运动记录列表是核心数据源这个接口按分页返回所以需要写一个循环来拉取全量数据。分页参数通常是offset和limitlimit最大可以设置到100但强烈建议不要一上来就拉100条先设置20条把结构看明白再去跑全量。分页拉取的完整代码逻辑如下def fetch_all_workouts(): all_records [] offset 0 page_size 20 while True: url f{BASE_URL}/workouts params {offset: offset, limit: page_size} resp requests.get(url, paramsparams, headersheaders, timeout10) resp.raise_for_status() body resp.json() records body.get(data, {}).get(records, []) if not records: break all_records.extend(records) total body.get(data, {}).get(totalCount, 0) offset len(records) print(f已拉取 {len(all_records)} / {total} 条记录) if len(all_records) total: break time.sleep(0.5) # 每次请求间隔半秒降低服务器压力 return all_records代码里我故意加了一句time.sleep(0.5)这一行非常重要。Keep虽然不会像银行系统那样敏感但短时间高频请求一样会触发限流。我是吃过亏的第一次跑全量同步时没加延时跑了大概两百多条就被服务器返回429了之后端口被封了十几分钟。加上延时之后几千条记录也就多花几分钟稳妥起见非常值得。3.3 运动详情的字段解析与轨迹数据处理运动列表返回的主要是概要信息比如运动类型、开始时间、时长、卡路里。但如果你想看心率和配速的详细数据就需要再调运动详情接口。这个接口的路径是/workouts/{运动ID}把列表里的每条记录ID填进去就行。我实际测试下来不同的运动类型返回的字段差异很大。跑步记录里有平均配速、步频、分段配速力量训练记录里有每组动作的重复次数和重量骑行记录里有爬升高度和最高速度。写解析代码的时候一定不能假设所有记录都长一个样必须用dict.get()的方式做容错取不到的字段给默认值。我刚开始就是没注意这一点程序跑着跑着突然在一条瑜伽记录上抛KeyError排查了半天才发现是这个原因。之后的经验是凡是解析接口返回的嵌套字典一律用get方法加默认值绝不直接用中括号索引访问。运动轨迹数据是另一个容易翻车的地方。有些接口返回的轨迹是一个很长的字符串看起来莫名其妙后来查了资料才知道是Google的Polyline算法压缩编码。如果你想把轨迹画出来需要写一个解码函数。保持这个格式的字符串配合地图API就能在页面上还原运动路线。不过这属于进阶玩法如果只是做数据统计分析暂时可以不用管轨迹字段。3.4 数据清洗入库与增量更新策略数据拉到本地之后不能直接扔进数据库就算完事。Keep返回的时间字段是13位毫秒级时间戳SQLite默认存不了需要先转成可读的日期格式。卡路里和时长有些记录是字符串有些是数字需要统一转成浮点数。还有运动类型Keep返回的是英文代码比如running、cycling建议映射成中文再加一列后边做中文图表时直接能用。下面是我用的清洗和入库片段import sqlite3 import pandas as pd def clean_workouts(records): df pd.DataFrame(records) df[start_time] pd.to_datetime(df[start_time], unitms) df[duration] pd.to_numeric(df[duration], errorscoerce) df[calories] pd.to_numeric(df[calories], errorscoerce) df[workout_date] df[start_time].dt.date return df def save_to_db(df): conn sqlite3.connect(keep_data.db) df.to_sql(workouts, conn, if_existsappend, indexFalse) conn.close()增量更新的策略是这样的因为每条运动记录有一个全局唯一的ID我用这个ID作为数据库里的唯一约束。同步数据时先查询数据库里已有的最大日期只拉取这个日期之后的新记录然后批量入库。如果哪次脚本中断了重新跑的时候也不会产生重复数据因为重复ID入库时会被约束挡住。这个设计看起来简单但实际用下来非常省心我现在每周跑一次同步跑完只需要看一眼日志有没有报错其他都不用管。4. 数据可视化与个人运动报告4.1 运动时长与卡路里趋势分析数据入库之后就到了最有成就感的部分把数据变成图表。我先做的是一张按月统计的运动时长趋势图。思路很简单从SQLite里按月份分组求和再用matplotlib画折线图。pandas的resample功能在这里非常顺手按月的聚合一行代码就完成了。import sqlite3 import pandas as pd import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [PingFang SC, Microsoft YaHei] conn sqlite3.connect(keep_data.db) df pd.read_sql_query(SELECT * FROM workouts, conn) df[start_time] pd.to_datetime(df[start_time]) monthly df.set_index(start_time).resample(M)[duration].sum().dt.total_seconds() / 60 monthly.plot(kindline, figsize(12, 5), markero) plt.title(每月运动时长趋势分钟) plt.ylabel(分钟) plt.tight_layout() plt.savefig(monthly_trend.png)这张图画出来之后我对自己的运动习惯有了很明显的新认识。之前一直觉得自己每个月都挺规律的结果图表出来发现上半年有三个月是明显的低谷回想了一下那段时间确实工作特别忙几乎没怎么系统性锻炼。数据不会骗人有了这张图之后安排训练计划就有的放矢了。4.2 运动类型分布与心率区间统计除了趋势图我还做了两个维度运动类型分布饼图和心率区间柱状图。运动类型分布比较直观把运动类型映射成中文后按计数统计用饼图展示就能看出自己的运动偏好。我自己的数据跑出来跑步占大头力量训练次之瑜伽只占很小的一部分。这个信息对我调整训练结构挺有帮助的比如我原本以为力量训练和跑步各占一半实际一看跑步占了七成后续就刻意增加了力量训练的频率。心率区间分析稍微专业一点。Keep会返回运动过程中的心率区间分布数据通常分五个区间热身、燃脂、有氧、无氧、极限。我按运动类型分组分别统计各区间的占比画成堆叠柱状图。这张图能看到每类运动对心肺的刺激强度。跑步记录的分布以有氧和无氧为主力量训练的数据集中分布这两者差异非常清晰也验证了手环传感器数据的可靠性。如果你愿意花点时间还可以把每月的数据汇总成一张个人运动报告输出成图表文件或者存成HTML页面一个月看一次很有仪式感。5. 常见问题与避坑实录5.1 高频问题排查速查表在做这个项目的过程中我遇到了一堆奇奇怪怪的问题这里整理成一张排查表按照建议优先级排列故障现象可能原因解决办法请求返回401 UnauthorizedToken过期或者被服务器吊销重新抓包获取新的Token更新配置文件请求返回403 ForbiddenUser-Agent不是移动端Keep把UA改成抓包看到的完整手机UA返回429 Too Many Requests请求频率过高触发限流脚本里加time.sleep拉一批休息一会儿部分运动记录字段缺失不同类型运动返回结构不同解析时用get加默认值别直接取索引抓包看到加密乱码证书信任没配置好iOS检查证书完全信任开关Android检查用户证书权限中文乱码或方框字体配置问题matplotlib设置中文字体比如PingFang SC或Microsoft YaHei数据库重复数据脚本中断后重跑用运动ID做唯一约束配合增量更新策略5.2 几个值得单独拎出来说的细节第一个坑是关于运动数据跨端同步的。Keep的App端和网页端数据在正常情况下是同步的但偶尔会出现手机端能看到某条记录、接口却查不到的情况延迟的原因通常是服务端做了异步处理。如果你发现某次同步的数据比App上少了几条别急着怀疑脚本等个几分钟再跑一次就能查到了。第二个坑是时间和时区问题。Keep返回的时间戳是UTC时间如果你直接用Python的fromtimestamp转成当地时间的datetime没问题但如果你在SQLite里直接存时间戳然后分组可能就会出现日期偏移导致某天的运动数据跑到前一天去了。稳妥的做法是在清洗阶段就转换为本地时区的日期保证后续分组统计的口径一致。第三个坑是接口变更。Keep的接口并不是永远不变的我遇到过两次重启手机之后抓包发现原有的接口路径已经失效的情况。解决办法没有捷径就是定期观察抓包工具里的真实请求发现异常时重新分析。所以我的项目里把接口路径都统一放在一个配置区域改起来方便不至于动不动就要翻代码到处找硬编码。第四个坑是关于合规边界。这里要专门强调一下这套方案只适合采集自己账号的数据用于个人学习和分析。不要尝试采集他人数据不要高频请求给服务器制造压力不要将采集的数据用于任何商业用途。我自己的脚本严格限制请求频率每天最多同步一次这个习惯值得保持。5.3 项目扩展方向这个项目做到后期可以扩展的方向非常多。感兴趣的可以往这几个方向再走一步把运动记录同步到云端结合自己的体重和饮食数据做综合健康分析用Flask搭一个本地Web服务浏览器打开就能看个人运动仪表盘接入钉钉或者企业微信的机器人每天定时推送当天的训练提醒和昨日运动总结。我个人最近在尝试把运动数据和睡眠数据合并起来分析看看训练强度对睡眠质量的直接影响了样本数据积累得还不够多等跑一段再分享结果。最后分享一个小技巧如果你担心token过期导致脚本失败可以写一个简单的异常捕捉检测到401错误时自动发送一条通知到手机提醒你重新抓包更新token。这样就不用每次跑到一半才尴尬地发现数据没同步成功。这套方案我前前后后跑了快半年最大的感受就是平台的数据终究是平台的只有把数据抓到本地才能真正按自己的需求去分析和使用。整个项目涉及到的爬虫基础、接口分析、数据清洗、可视化展示每一环都是实际工作中非常常用的技能练一遍下来比看十遍教程都管用。
返回列表