ARTICLE DETAIL

资讯详情

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

用Python爬虫采集Keep运动数据:从Cookie到CSV的完整实战

用Python爬虫采集Keep运动数据:从Cookie到CSV的完整实战 玩Keep也有三年多了跑步、骑行、燃脂操、瑜伽课攒了上千条运动记录。问题来了每次想看看这个月跑了多少公里、和去年同期比是进步还是偷懒都得在App里翻开翻去而且App给的统计报表就固定那几张想导个原始数据出来做自己的分析压根儿没有导出入口。于是我就用Python写了个采集脚本把自己名下的Keep运动数据全部拉下来存成CSV表格后面想怎么分析就怎么分析。整个项目做完跑步轨迹、训练时长、卡路里消耗全都变成了我能自由处理的数据还顺手做了个月度里程趋势图。这篇博文就把这个项目的完整思路、核心代码和踩坑记录都拆开讲讲。适合两类人看一是想学Python爬虫但不想拿别人网站练手的朋友采集自己的数据既合规又没心理负担二是Keep重度用户想把自己多年运动数据沉淀下来的。1. 动手之前先理清Keep有哪些数据可以采集1.1 Keep运动数据的类型盘点Keep的数据体系从账号维度看主要分四类。第一类是跑步和骑行记录这是最核心的运动数据。每次户外跑步都会记录距离、时长、平均配速、步频、海拔爬升、路线轨迹等几十个字段我的账号里大概有400多条跑步记录这些数据是分析跑步能力变化的第一手资料。骑行记录结构和跑步基本一致就是字段上多了个踏频和车型。第二类是训练课程记录包括你练过的所有Keep课程比如燃脂操、力量训练、瑜伽、HIIT这些。每条记录会保存课程名称、训练时长、消耗卡路里。这类数据适合分析你的训练偏好回头看看自己到底是在坚持还是三天打鱼两天晒网全在数据里摆着。第三类是体重记录。如果你用过Keep的体重打卡功能那么每天的体重、BMI变化都有记录。这类数据量相对少但如果你想把运动量和体重变化做关联分析它是很重要的对照数据。第四类是活动徽章和等级数据比如连续打卡天数、完成课程数量对应的徽章。这类数据分析价值不高采集下来更多是做个回顾用。从采集优先级来说前两类是重点第三类顺带拿一下第四类看个人兴趣。我的脚本核心就是跑步记录加训练课程记录这两类数据量最大分析价值也最高。1.2 三条技术路线为什么我选了网页接口做Keep数据采集技术路线其实有三条我挨个试过之后选了最省心的一条。第一条路是逆向Keep手机App。App的所有请求都走HTTP没错但客户端做了大量加固抓包时需要处理SSL证书固定请求参数里有加密签名你光是想搞清楚签名怎么生成就得花不少时间。这条路适合做安全研究的人去死磕对于我这种只是想导出自己数据的人来说投入产出比太低了。第二条路是抓包看手机App的请求找到关键接口后用脚本模拟。这条比第一条容易但依然绕不开加密参数问题而且App接口是黑盒哪天Keep升级一下签名算法你的脚本就废了维护成本居高不下。第三条路是我最终采用的直接走Keep网页端的接口。Keep的网页版提供了和App类似的数据接口而网页端请求头相对简单不需要处理App签名只要用Session保持登录态就能顺利拿到数据。为什么选第三条核心就四个字——稳定、好维护。个人做数据采集工具最怕的就是接口一变脚本就挂。网页端接口虽然也会调整但变更频率远低于App端而且网页端本身的设计目标就是供浏览器访问你用脚本模拟浏览器的正常访问行为去拉取自己账号名下的数据这就是一个合规的个人数据导出场景没有太多的灰色空间。提示无论做哪类数据采集都务必遵守法律法规和平台用户协议。只采集自己账号的数据不碰他人数据不做商业用途控制请求频率避免给平台服务器造成压力这是底线。2. 环境准备与登录态获取Cookie是第一步2.1 Python环境与依赖库的安装这个项目的Python依赖非常少核心就三个库。requests负责发HTTP请求是整个采集脚本的发动机。json是Python标准库用来解析接口返回的数据。pandas则是可选库但如果你采集完要接着做数据分析强烈建议装上处理表格数据比纯写循环快得多。如果你电脑上还没有Python环境去官网下载Python 3.10以上版本的安装包就行。安装时最关键的一步是勾选Add Python to PATH这一步不勾的话后面在命令行里敲python会提示找不到命令。装好之后在命令行验证一下python --version看到版本号输出就说明环境没问题。接着安装依赖pip install requests pandas openpyxl国内用户如果遇到下载慢或者超时可以临时指定清华镜像源pip install requests pandas openpyxl -i https://pypi.tuna.tsinghua.edu.cn/simpleopenpyxl是pandas读写Excel时需要的底层依赖虽然我们的主力存储是CSV但数据分析完想导出一份Excel报告的话提前装好没坏处。2.2 登录机制拆解Session与Cookie的关系采集数据的前提是拿到登录态。Keep的网页端登录方式有手机号密码登录和验证码登录理论都可以用脚本实现但实际做的时候你会发现Keep对登录接口做了不小的加固包括密码RSA加密、滑块验证码之类的反自动化机制直接用requests模拟登录走到滑块验证码那一步就卡住了。所以我换了一个思路第一次登录在浏览器里手动完成然后把浏览器里的Cookie复制出来给脚本用。这样既绕过了验证码问题又拿到了稳定的登录态。这里要理解两个核心概念Session和Cookie。Session是requests库里的会话对象你可以把它理解成一张健身房的临时卡。你第一次进去亮了会员码登录之后进出都靠这张卡识别身份不需要每次都验证会员码。Session对象会自动帮你保存服务器返回的Cookie之后每次请求都会自动带上。Cookie则是服务器下发的一段身份凭证里面保存了你的登录标识。浏览器能一直保持登录状态就是因为它把Cookie存在本地了。我们把浏览器里的Cookie复制给脚本相当于把这张临时卡借给脚本用。import requests # 创建一个Session对象 session requests.Session() # 从浏览器复制过来的Cookie字符串 cookie_str 这里粘贴你从浏览器复制的完整Cookie # 把Cookie字符串解析成字典再设置进Session for item in cookie_str.split(;): key, _, value item.strip().partition() session.cookies.set(key, value) # 设置统一的请求头 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, Accept: application/json, text/plain, */*, Referer: https://www.gotokeep.com/, Origin: https://www.gotokeep.com })Cookie的提取方法用Chrome打开Keep网页版登录你的账号按F12打开开发者工具切到Network标签刷新页面后随便点一个接口请求在请求头里找到Cookie字段把整段值复制出来即可。提示Cookie就是你的账号钥匙相当于登录凭证。不要把包含Cookie的代码分享出去用完之后更不要随手贴到公开的地方。保护好自己的账号数据安全比拿到数据本身更重要。3. 核心代码实现从请求接口到数据落盘3.1 先看接口返回什么再写解析逻辑登录态搞定之后接下来就是请求运动记录接口。Keep网页端的跑步记录数据接口我实测下来的路径是GET https://api.gotokeep.com/v1.1/running这个接口支持分页参数用page和pageSize控制。第一次请求时我建议直接把接口返回的原始JSON完整打印出来先看结构再动手写逻辑。这算是我做采集一个挺重要的习惯——先摸清数据的形状再决定怎么解析不要凭感觉写一段解析代码结果字段名对不上。import json url https://api.gotokeep.com/v1.1/running params { page: 1, pageSize: 10 } resp session.get(url, paramsparams) data resp.json() print(json.dumps(data, ensure_asciiFalse, indent2))打印出来的JSON结构里data字段是跑步记录列表每条记录的核心字段包括id是记录唯一标识distance是距离单位是米duration是时长单位是秒averagePace是平均配速startTime是开始时间戳calories是消耗卡路里。注意我说的是实测下来的接口路径和字段名。Keep的接口属于内部接口字段名可能因为接口版本调整发生变化。你在动手的时候第一步永远是先打印原始响应以实际返回的数据为准。3.2 分页采集与异常重试别让脚本跑一半就挂跑步记录可能有几百条一次拿不完必须做分页循环。分页逻辑的核心就三件事判断当前页还有没有数据、每页请求之间加延时、请求失败自动重试。import time def fetch_all_running_records(max_pages50): all_records [] for page in range(1, max_pages 1): try: params {page: page, pageSize: 50} resp session.get(url, paramsparams) resp.raise_for_status() data resp.json() records data.get(data, []) if not records: print(f第{page}页没有数据采集结束共{len(all_records)}条) break all_records.extend(records) print(f第{page}页完成累计{len(all_records)}条) time.sleep(1) except Exception as e: print(f第{page}页失败{e}等待3秒后重试) time.sleep(3) continue return all_records running_records fetch_all_running_records() print(f跑步记录采集完成共{len(running_records)}条)几个细节单独说下。pageSize我设成50因为实测单页超过50条时接口偶尔会报错50是一个稳妥的取值。time.sleep(1)是每次请求之间的间隔这个必须加一个是出于礼貌不给服务器太大压力另一个是防止触发风控导致IP被限流。try-except是爬虫脚本的标准配置网络抖动、接口偶发异常都是家常便饭捕捉到异常后等几秒重试很多时候就恢复正常了。这个分页采集函数的逻辑是通用的后面采集训练课程记录、体重记录只需要替换接口地址和参数名其他部分直接复用。3.3 数据清洗把嵌套JSON变成干净表格拿到原始记录之后不能直接存文件。原始JSON里有大量嵌套的统计对象、坐标数组很多字段对分析没有直接意义。我习惯写一个清洗函数把关键字段抽出来转成统一格式。from datetime import datetime def timestamp_to_str(ts): if not ts: return return datetime.fromtimestamp(ts / 1000).strftime(%Y-%m-%d %H:%M:%S) def clean_running_record(item): return { record_id: item.get(id), distance_km: round(item.get(distance, 0) / 1000, 2), duration_min: round(item.get(duration, 0) / 60, 2), pace: item.get(averagePace, ), start_time: timestamp_to_str(item.get(startTime)), calories: item.get(calories, 0), elevation_m: item.get(totalAscent, 0) }清洗逻辑做了三件事距离从米换算成公里时长从秒换算成分钟时间戳转成可读的2025-01-01 08:30:00格式。海拔爬升从totalAscent字段取出来因为只要分析跑步记录海拔变化是个挺有用的参考维度。看这段代码你会发现所有字段都是用item.get(字段名, 默认值)的方式取值而不是直接item[字段名]。这是一个很重要的防御姿势你的历史运动数据里有的早期记录可能没有海拔字段有的手动输入记录可能缺配速直接下标取值会抛出KeyError让整个脚本崩溃而用get方法配上默认值函数永远能正常工作。3.4 落盘存储CSV、Excel还是SQLite清洗之后的数据就可以存起来了。存储方案我实践下来有三种各自适用不同场景。存储方式适用场景优点缺点CSV日常小规模分析通用性强Excel直接能打开无数据类型约束数据量大时读写慢Excel分享给非技术朋友美观、自带头格式依赖openpyxl写入相对慢SQLite数据量上千条、需要复杂查询支持SQL查询效率高需要DB工具才能直观查看我主力用的是CSV因为采集完必然要接pandas做分析CSV是pandas最顺手的输入格式。写CSV时有一个坑编码必须用utf-8-sig不能用普通的utf-8否则生成的CSV用Excel打开时中文全是乱码。我第一次就被这个坑卡了半小时。import csv fieldnames [record_id, distance_km, duration_min, pace, start_time, calories, elevation_m] with open(keep_running.csv, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnamesfieldnames) writer.writeheader() writer.writerows([clean_running_record(r) for r in running_records]) print(数据已保存到 keep_running.csv)训练课程记录的采集逻辑完全可以照搬这套换接口路径换一个清洗函数跑一遍分页循环存一个新CSV十分钟就能搞定。整套采集流程跑下来跑步记录400多条训练课程记录600多条总共也就花了不到两分钟。4. 运行中遇到的问题与排查经验4.1 Cookie过期了怎么办主动失效检测这个脚本跑了两个月遇到最频繁的问题就是Cookie过期。Keep的登录态大概能维持几周到一个月过期之后脚本再请求数据接口返回的不是400就是401之前的分页循环就会一路报错跑到底白白浪费一大堆请求。后来我加了一个前置检查函数在采集脚本最开始先请求一次个人资料接口用一个比较轻量的请求验证登录态是否还有效。def is_login_valid(): check_url https://api.gotokeep.com/v1.1/users/self try: resp session.get(check_url) if resp.status_code 200: data resp.json() return data.get(code) 200 except Exception: pass return False if not is_login_valid(): print(登录态已失效请重新从浏览器复制Cookie后重试) exit(1)多加这几行代码脚本就从跑一半莫名失败变成了启动时快速体检Cookie失效的问题在源头被拦住省了不少排查时间。顺便说一句我后来把Cookie放到了单独的配置文件里每次更新只需要改配置文件不用动主脚本这个小习惯在维护阶段很值得养成。4.2 请求频率过高被限流延时与重试策略有一次我想做全量数据回补把跑步、训练、体重、徽章好几类数据全部放到一个脚本里连续跑。因为每类数据之间没有加延时跑到一半接口开始大量报错返回的错误信息里明确提示了请求过于频繁。这个问题的解法很简单也很直接全局控制请求频率统一走一个带延时的请求函数。REQUEST_INTERVAL 1.5 def safe_get(url, paramsNone, max_retries3): for attempt in range(max_retries): time.sleep(REQUEST_INTERVAL) resp session.get(url, paramsparams) if resp.status_code 429: wait_time int(resp.headers.get(Retry-After, 60)) print(f触发限流等待{wait_time}秒) time.sleep(wait_time) continue resp.raise_for_status() return resp raise RuntimeError(多次重试仍然失败)这里有一个值得记下来的细节HTTP的429状态码表示请求太多服务器通常会在响应头的Retry-After字段告诉你需要等多少秒。把这个值解析出来用上比固定死等60秒要灵活得多。实测下来跑步记录加训练记录共一千一百多条数据用1.5秒间隔跑完全程大约十五分钟全程没有触发限流属于比较安全的节奏。如果你发现自己的账号已经触发了限流建议停一两天让风控状态冷却下来短时间内高频重试往往会适得其反。4.3 字段缺失导致脚本崩溃健壮性处理上文提到过用item.get()来防御字段缺失这里再展开说一个更细的场景。有一次我发现个别跑步记录的calories字段返回的不是数字而是一个空字符串直接拿去计算就会触发ValueError异常。解决方案是给关键字段再加一道类型转换的保护def to_float(value, default0.0): try: return float(value) except (TypeError, ValueError): return default def clean_running_record(item): return { record_id: str(item.get(id, )), distance_km: round(to_float(item.get(distance)) / 1000, 2), duration_min: round(to_float(item.get(duration)) / 60, 2), pace: item.get(averagePace, ), start_time: timestamp_to_str(item.get(startTime)), calories: to_float(item.get(calories)), elevation_m: to_float(item.get(totalAscent)) }to_float会把None、空字符串、非数字类型统一处理成默认值0.0让清洗函数在任何脏数据面前都不会崩。这个处理思路不只是适用于这个项目做任何爬虫采集或者数据处理任务时类型转换加保护都是让代码更抗造的核心技巧可以顺手沉淀成你自己的工具函数。5. 数据到手之后分析与扩展玩法5.1 用pandas做运动趋势分析数据采集只是手段分析才是目的。跑步记录存成CSV之后我用pandas做了一轮简单的趋势分析效果比App自带的统计报表灵活得多。import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(keep_running.csv) df[start_time] pd.to_datetime(df[start_time]) df[month] df[start_time].dt.to_period(M) # 每月跑步总里程 monthly_distance df.groupby(month)[distance_km].sum() print(monthly_distance) # 绘制柱状图 plt.figure(figsize(12, 5)) monthly_distance.plot(kindbar) plt.title(每月跑步总里程) plt.ylabel(公里) plt.tight_layout() plt.show()这段代码做了一件非常重要的事把时间字符串解析成真正的日期类型然后用dt.to_period(M)提取出月份最后按月份分组聚合出总里程。类似的分析可以延伸到训练时长、卡路里消耗、平均配速等多个维度。我还画过一张配速vs距离的散点图能直观看出自己在短距离和中长距离上的速度差异这些个性化图表是App的固定统计页面给不了的。跑完月度趋势之后你会很直观地看到自己的运动规律哪个月偷懒了哪个月状态好一目了然。看完自己连续三个月的下滑曲线比任何鸡汤都管用。5.2 数据资产化的几个扩展方向采集回来的数据存了CSV后续能做的扩展其实非常多我自己试了三个方向效果都还行。第一个方向是年度运动报告。把一年的数据汇总成一张报告图包括总里程、总时长、最佳配速、最活跃月份、连续打卡天数等年底整理一份发朋友圈仪式感拉满。生成这种图表用matplotlib或者pyecharts都能做代码量不大。第二个方向是训练计划的复盘。结合训练课程记录的课程名称字段可以分析出哪类课程练得最多哪些课程开了头又半途而废。我做了这个分析之后发现自己下载的HIIT课程有三分之一压根没练过下个月的训练计划就该围绕少囤课、多练完来调整。第三个方向是体重和运动量的关联分析。把体重记录和运动量数据放在同一个时间轴上看你可能会发现一些有意思的相关性。我自己做完之后发现月度训练时长和当月体重变化确实存在肉眼可见的负相关这个发现比健身博主的经验更贴合我自己的身体数据。如果还想再进一步可以给这个采集脚本加一个定时任务比如每周日自动运行一次把新增的运动数据追加到CSV里。再配合一个ECharts或者Grafana看板你就有了一套完全归属自己的个人运动数据中台App关停了你都不怕丢数据。从我实际使用这几个月的感受来说采集自己的运动数据技术上确实不算复杂真正有价值的是这个过程让你养成了把数据握在自己手里的习惯。而且这整个项目本身就是一次非常完整的爬虫实战会话管理、请求构造、JSON解析、数据清洗、异常处理、限流规避、定时维护爬虫开发里最常见的知识点全覆盖了。如果你也是Python初学者又恰好是Keep用户拿自己账号练手安全合规还能沉淀一套属于自己的数据底座值得折腾一下。最后再分享一个小技巧采集脚本跑完之后把CSV文件做一次版本备份存到网盘或者本地另一个目录养成这个习惯之后你的运动数据就真正意义上变成自己的资产了。
返回列表