ARTICLE DETAIL

资讯详情

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

Python爬虫实战:从Keep网页端采集运动数据并分析

Python爬虫实战:从Keep网页端采集运动数据并分析 1. 采集前的思路梳理为什么选Web端而不碰App逆向先说清楚我为什么要折腾这个事。Keep的App里其实已经提供了不少统计图表但我想做的分析维度更细——比如把近一年的跑量、骑行、跳绳数据按周聚合看看训练量和配速之间的相关性还要把数据导出到本地做长期留存。官方没有提供完整的数据导出功能App里手动翻记录又不现实所以才想到用Python写一个采集脚本。这里有个关键的路线选择问题Keep的数据入口主要分App接口和Web接口两条路。App端的接口理论上也能抓但实际做起来非常不划算。Keep的App接口有签名校验请求参数需要做设备指纹绑定的加密处理抓包拿到的请求直接重放基本都会失败你要么用桩环境去hook要么去逆向客户端的算法。逆向App这件事技术门槛高不说还涉及反编译、绕过证书校验这些灰色操作不管从合规角度还是投入产出比角度都不推荐普通开发者尝试。Web端的思路就务实多了。Keep的网页版和App共用同一套用户数据你在浏览器里登录了Keep账号浏览器发出的每个请求都带着登录态Cookie服务器就认为这是你本人。用Python模拟浏览器请求本质上就是把你手动在网页上点按钮的过程自动化。这个过程的合规边界相对清楚采集的是你自己账号的数据频率可控不涉及恶意爬取他人信息。我最终的技术选型是这样的请求库用requests轻量直接没有额外的学习成本数据解析用json加pandasJSON转DataFrame再清洗效率高数据存储用CSV加SQLite双轨CSV方便随时打开看SQLite方便做增量更新登录态用Cookie复用不搞自动化登录那一套复杂的图形验证码处理环境方面没什么特殊的Python 3.8以上就行。如果你刚接触Python建议直接用Anaconda或者系统自带的Python解释器把requests、pandas、openpyxl这几个包装齐在命令行里敲pip install requests pandas openpyxl就能搞定不需要额外的IDE配置。这套方案跑通之后你会发现整个流程其实不复杂模拟登录拿数据接口的访问权限循环请求历史记录接口把JSON拆成结构化表格最后落库。每一步都有相对成熟的套路可以抄真正考验耐心的是细节处理。2. 从登录态到数据接口请求伪造的关键细节2.1 用浏览器开发者工具定位数据接口打开Keep网页版登录你的账号进入运动记录页面按F12打开开发者工具切到Network面板。这时候你往下翻列表或者在页面上翻页就能看到一个个带着JSON响应的请求。这些请求里你需要重点关注几个特征接口路径里通常包含api或rest字样响应体是标准的JSON结构请求头里带着Authorization或者一串长的Cookie参数里有分页相关字段拿我用过的接口举例Keep的Web端运动记录接口大致是这样一个模式请求参数里有page、pageSize这类控制分页的字段返回值里包含records数组和total总数。具体路径每个时期可能有调整所以我的建议是不要记死某个接口地址而是要掌握“找接口”的方法——看到返回里带运动类型、时间、时长、卡路里这些字段的请求基本就是你要找的数据源。2.2 Cookie与请求头的组装逻辑定位到接口之后第一步要做的不是写Python代码而是先在浏览器里手动请求几次搞清楚哪些请求头是必不可少的。我的经验是Keep对请求头的校验相对宽松但有几个字段最好带上User-Agent模拟一个正常浏览器的标识不要暴露Python默认的python-requestsReferer带上来源页面避免触发防盗链逻辑Cookie这是核心你的登录凭证就在里面实际操作的时候我习惯先写一段脚本来验证Cookie的有效性。先用requests.Session()建立会话把Cookie塞进请求头然后请求用户信息接口。如果返回200且数据正常说明Cookie没问题如果返回401或跳转到登录页说明登录态过期了需要重新到浏览器复制。import requests session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Referer: https://www.gotokeep.com/, Cookie: 粘贴你从浏览器复制的Cookie, }) # 先请求一个轻量接口验证登录态 resp session.get(用户信息接口地址) print(resp.status_code, resp.text[:500])这一步跑通了整套采集脚本的基础就算打牢了。后面所有请求 只需要在这个session基础上叠加参数就行不用每次重复处理请求头。2.3 首次请求就遇到403一个最常见的坑我第一次写这个脚本的时候在验证接口这一步就被卡了十分钟。cookie明明是刚从浏览器复制的User-Agent也模拟了但请求打过去就是403。后来排查发现问题出在Cookie里有一个字段在读取时被截断了。浏览器的Cookie是一整串以分号分隔的键值对我复制的时候选中了部分内容导致后半段丢失。Keep的鉴权体系里缺了关键的几个字段服务端就会把你当成未登录的匿名请求直接拒绝。所以这里给新手一个建议复制Cookie的时候到开发者工具的Network面板里直接找到请求头里完整的cookie字段拷贝不要在Application面板里一个个字段手动拼接那样最容易出错。另一个403的场景是请求频率过高。Keep的Web端虽然不像App端那么严格但短时间内连续请求几十次还是会被临时限流。这个我在后面第4章展开聊。3. 运动记录解析从JSON到结构化表格3.1 返回数据的字段结构拆解接口请求通了之后返回的JSON结构需要花点心思研究。Keep的运动记录接口返回的数据大致包含这几类字段字段分类字段作用注意事项时间类运动开始时间、结束时间、UTC时间戳注意时区转换Keep返回的多是UTC时间类型类运动类型ID、运动名称同一个typeId在不同时期可能映射不同运动项目指标类时长、卡路里、公里数、平均配速注意单位卡路里可能是kcal配速可能是min/km原始数据心率曲线、每公里配速列表部分字段只有在详情接口里才有我第一次拿到的数据JSON里嵌套了好多层。运动记录列表的每一项里data字段下面又挂了一堆子对象有的子对象还把数值打包到了statistics这种内部结构里。如果直接用json.loads然后一股脑转DataFrame出来的表格会很难看全是NaN。所以解析的第一步不是写代码而是先整理字段映射。我习惯的做法是先用浏览器或者Python把接口返回的JSON打印出来仔细对照Keep网页上显示的内容把“网页上能看到哪些指标”和“JSON里哪些字段对应这些指标”逐一对应上形成一张映射表。比如网页上的“消耗千卡”对应JSON里的某个整数字段记录着以千卡为单位的数值。这一步看起来繁琐但特别值得做。字段对应关系一旦搞反后面所有分析全是错的。Keep的消耗卡路里字段calorie单位是千卡但有些时候接口返回的可能是calories值是字符串类型。这些细节不踩一次坑是真的记不住。3.2 用pandas做数据清洗类型、单位、时区拿到原始的JSON数组之后我通常按照下面这个流程做清洗import pandas as pd records [] # 这个是接口返回的records数组 df pd.DataFrame(records) # 只保留需要的列 keep_cols { startTime: 开始时间, duration: 时长(秒), calorie: 消耗(千卡), distance: 距离(米), typeName: 运动类型, } df df[list(keep_cols.keys())].rename(columnskeep_cols) # 时间戳转成可读时间 df[开始时间] pd.to_datetime(df[开始时间], unitms).dt.tz_localize(UTC).dt.tz_convert(Asia/Shanghai) # 秒转分钟、米转公里 df[时长(分钟)] (df[时长(秒)] / 60).round(1) df[距离(公里)] (df[距离(米)] / 1000).round(2)需要注意几个坑时间戳的处理。Keep返回的开始时间通常是毫秒级时间戳要先转成秒级再传给pd.to_datetime或者直接指定unitms。然后就是时区问题——我采集回来的数据如果不做时区转换直接用pd.to_datetime(df[startTime], unitms)出来的是UTC时间比北京时间慢了8个小时。跨天分析训练量的时候这8个小时的偏差会把一整天的记录算到错误日期里数据直接废掉。数值类型的处理。接口返回的字段不一定都是数字有的可能是字符串。做除法、算均值之前先pd.to_numeric统一类型否则pandas会在某些行上报错或者直接按字符串拼接出天大的错数。缺失值的处理。首次跑步记录里没有心率数据骑行记录里可能没有配速数据代码里凡是涉及这些字段的计算都要先判断是否为空。我的习惯是统一fillna(0)然后在分析的时候单独标记“缺失数据”和“真的是0”的区别避免统计结果失真。3.3 数据落库双轨存储的取舍清洗完的数据我同时存两份一份CSV一份SQLite。CSV的好处是随时可以用Excel打开查看遇到问题能肉眼排查。缺点是重复写入的时候需要处理表头和追加的模式容易写出重复记录。我每次写CSV之前都会先按运动记录的唯一ID去重一下。SQLite的好处是天然支持增量更新。我建了一个workouts表主键是运动记录的ID。每次采集完用INSERT OR REPLACE写进去再次运行的时候已经存在的记录自动覆盖新记录自动插入不需要自己写复杂的去重逻辑。import sqlite3 conn sqlite3.connect(keep_data.db) df.to_sql(workouts, conn, if_existsappend, indexFalse) conn.close()这段看起来没啥技术含量但配合后面要讲的“增量采集”思路你会发现用SQLite做断点续传特别顺手。每次采集不是重新拉全量数据而是从库里取最近一条记录的时间只拉这个时间之后的数据入库的时候靠主键去重反正不会漏也不会重。4. 采集节奏与风控规避别把自己的账号玩进去4.1 请求频率控制与随机延时说到风控这是整个采集项目里最需要“手里有分寸”的部分。Keep的数据是个人运动记录本身没有多大的商业价值但如果你把采集脚本写成毫无节制的循环一两秒钟发几十个请求那跟攻击服务器没什么区别。轻则接口临时返回429或者验证码重则账号被限制登录。我给自己定的节奏是每请求一次time.sleep(1.5)到3秒之间的随机值。拉历史数据可能涉及几十页分页全部跑完也就一分钟左右完全够用。随机延时不是为了慢而慢是为了让请求节奏看起来像真人在操作——真人翻页总要停顿一下看页面内容吧。除了延时我还会在程序里加一个“单次会话最大请求数”的保险。比如一个session会话里最多请求30次就强制休息60秒这个数字是拍脑袋定的但实际用下来很少触发。4.2 触发风控后的表现和解法我自己实际遇到过两次风控症状各不相同。第一次是连续采集多页之后接口突然返回一个HTML页面而不是JSON。打开一看是类似“请完成安全验证”的提示页。解法很简单停掉脚本等个五到十分钟手动打开网页版Keep随便做几个操作比如点一下运动记录页让服务器认为你是一个正常用户然后再跑脚本基本就恢复了。第二次是Cookie被强制下线。我在服务器上跑了一个定时采集的任务Cookie是从本地浏览器复制的第二天任务执行的时候全部返回401。Check了一下才知道Keep的多端登录策略是同一账号在网页端登录后服务端会定期刷新token旧的Cookie过一段时间就失效了。这个问题的解法我放在后面第5章详细说。4.3 增量采集与断点续传增量采集的思路其实特别简单每次只拉上次拉完之后新产生的数据。Keep的运动记录接口一般都有“按时间倒序”的参数设计可以用before或者page实现分页。我的实现方式是先查一下本地数据库里有没有数据如果有取出开始时间最大的一条记录然后把接口的请求时间参数设置为这个时间点只拉这之后的数据。这样做最大的好处不是省时间而是降低请求频率的峰值。如果你每天运动两三次增量采集只需要请求一两次接口就够了根本不会触发风控。反过来每次全量拉数据几千条记录要翻几十页不仅耗时还平白无故给了服务器压力。断点续传的逻辑也不复杂。如果采集跑到第15页的时候超时崩了不要从第1页重来而是记录一下已经入库的最大时间戳从那里接着跑。配合SQLite的主键去重就算中途重复拉了几条最终入库的数据也不会乱。5. 踩坑实录实际采集过程中遇到的三类问题5.1 时间边界凌晨的运动被算到了哪一天这是我在做趋势分析的时候发现的。有一天的数据表里凌晨1点跑完步的记录日期被算成了前一天。排查下来是时区转换写错了。Keep接口返回的startTime是UTC时间戳。我在清洗数据时先转成了北京时间这一步是对的。但问题是我按照“运动日期”做分组的时候用的是转完时区之前的那个UTC日期列导致凌晨0点到8点之间完成的运动因为UTC的日期还没改变被归到了前一天。解决方式是在清洗阶段就固定生成一个“本地日期”字段df[本地日期] df[开始时间].dt.date后面所有按日期的分析都基于这个字段不要中途再去strftime或者取.dt的日期属性那样很容易又绕回UTC的老路上。5.2 Cookie过期与登录态维护定时任务的核心痛点我的定时采集任务跑了两天就翻车了原因就是Cookie失效。浏览器里登录Keep服务端会定期刷新会话旧Cookie的有效期可能只有几个小时到一天。针对这个问题我尝试了几个方案方案一每次采集前手动去浏览器复制Cookie。太麻烦定时任务没法这么干。方案二程序里自动登录走用户名密码加验证码。Keep网页端的登录有行为验证自动化处理成本较高。方案三Cookie失效时发送提醒人工介入重新复制。这个最实在。我最后用的是方案三。脚本里加了异常检测一旦遇到401或登录页跳转就停止采集并给自己发一条通知。因为我是个人项目频率不高人工维护成本可以接受。如果你要做无人值守的长期采集可以考虑方案二但那需要单独处理验证码识别又是一大块工作量。5.3 数据指标的单位换算配速和心率的隐藏陷阱Keep网页上显示的配速是“每公里用时”比如5分30秒/公里。接口返回的数据里这个值可能是秒数330也可能是直接给你一个字符串“530”。我遇到的是前者但看其他人的分享不同接口版本返回格式不一样。最好的做法是不要依赖单位的默认值在解析代码里做明确的换算。秒数转成“分:秒”的字符串很容易但如果要计算平均配速就不能直接对字符串求平均必须先把所有值统一成秒算完再格式化。心率数据也是。Keep的手表或手环记录的心率曲线在详情接口里才有列表接口通常只给你一个平均心率。这个平均值是运动全程的均值把热身和拉伸阶段都算进去了所以你在代码里算“有效训练心率区间”的时候要额外加一个过滤逻辑——只统计运动开始后10分钟到结束前5分钟之间的数据。这个经验是我在对比手环原始数据和Keep平均值时发现的差得还挺多。6. 采集之后能做什么从数据到洞察的延伸6.1 训练负荷趋势分析数据采集下来不能只躺在数据库里吃灰。我最常做的一个分析是“周训练负荷趋势”——把每周的运动总时长、总公里数、总消耗卡路里聚合成一张表再叠加滚动平均线看训练量是在爬坡还是在减载。weekly df.resample(W, on本地日期).agg({ 时长(分钟): sum, 距离(公里): sum, 消耗(千卡): sum, }).reset_index()这张表能直观反映训练曲线。比如我发现自己的跑量连续三周上涨之后第四周总会出现一次状态下滑这其实是身体在发出需要休息的信号。有了数据支撑“今天是跑还是不跑”就不再靠感觉拍脑袋而是看训练曲线是否已经处在高位平台。6.2 运动类型的均衡度检查Keep支持跑步、骑行、游泳、跳绳等多种运动类型。我连续采了一个季度发现一个明显的问题我的跑步记录占了90%以上力量训练和骑行被严重忽视。这个结论从主观感受上其实不太明显但把占比饼图画出来之后一眼就能看出训练结构失衡。这种分析反过来会推着你调整训练计划。我现在会在月初看一次上个月的“运动类型占比表”如果某类运动连续低于10%就主动安排一次。6.3 拓展思路多平台数据合并与长期留存采集Keep的运动数据只是第一步这个脚本的设计模式完全可以复用到其他平台——比如某款跑步软件、某品牌的运动手表后台。不同平台的数据字段大同小异核心都是“时间、类型、时长、距离、消耗”把这些字段统一成一套标准结构就可以做跨平台的长期留档和分析。我现在已经把自己的Keep数据从2018年一直补采到了今天历史记录攒了几千条。这些数据本身不一定能直接指导训练但它让你拥有一份完全属于自己的、可以随时分析的运动档案——这是任何App在用户协议里都没承诺会永久保留给你的东西。最后说一个实用的小技巧数据采集脚本一定要加错误日志。我之前就是没加程序崩了自己都不知道。后来加了一个简单的logging模块每一次请求失败、每一次JSON解析异常都记录到文件排查问题的速度快了几倍。长期跑的采集任务日志就是你的眼睛别省这一步。
返回列表