Keep运动数据获取实战:从接口分析到数据解析的完整避坑指南
如果你曾经尝试过用Python脚本从Keep这类主流运动App里获取自己的跑步数据,大概率会遇到一些让人头疼的问题。页面突然打不开了,请求总是返回奇奇怪怪的错误码,或者好不容易拿到数据却发现是一堆看不懂的加密字符串。这背后其实是平台为了保护用户数据和服务器资源而设置的各种反爬机制。今天我们就来深入聊聊,如何在不违反平台规则的前提下,稳定、高效地获取Keep的运动数据。
我最初接触这个问题是因为想把自己的跑步记录统一管理起来。Keep用了好几年,里面积累了大量的运动轨迹和心率数据,但平台本身的数据导出功能比较有限。于是我开始研究如何通过技术手段获取这些数据,过程中踩了不少坑,也总结出一些实用的经验。这篇文章不会教你如何暴力破解或者绕过付费限制,而是聚焦在技术层面,分析Keep的数据接口特点,分享一些合规的数据获取思路。
1. 理解Keep的数据接口架构与认证机制
要获取Keep的数据,首先得明白它的接口是怎么工作的。Keep作为一款成熟的移动应用,其后端采用了典型的RESTful API设计,但和很多面向公众的网站不同,它的接口需要经过严格的认证才能访问。
1.1 接口发现与逆向工程
最开始我尝试直接访问Keep的网页版,但很快发现网页版的功能相对有限,很多数据并没有直接暴露出来。这时候就需要用到一些开发者工具来辅助分析。
使用浏览器开发者工具分析网络请求
打开Chrome的开发者工具,切换到Network标签页,然后登录Keep网页版并查看你的运动记录。你会看到大量的XHR请求,这些就是Keep与服务器通信的接口。重点关注那些返回JSON数据的请求,特别是包含运动列表、详情等信息的接口。
# 一个简单的示例,展示如何用Python模拟浏览器的请求
import requests
# 首先需要获取登录后的cookie或token
session = requests.Session()
# 观察浏览器中的请求头,尽量模拟得真实一些
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,en;q=0.8',
'Referer': 'https://www.gotokeep.com/',
'Origin': 'https://www.gotokeep.com',
}
# 尝试访问一个公开的接口看看
try:
response = session.get('https://api.gotokeep.com/some-public-endpoint', headers=headers)
print(f"状态码: {response.status_code}")
print(f"响应头: {response.headers}")
except Exception as e:
print(f"请求失败: {e}")
注意:直接访问Keep的API接口可能会遇到403或401错误,这是因为大多数接口都需要认证信息。在实际操作中,你应该先通过合法的登录流程获取认证凭证。
1.2 认证流程分析
Keep的认证系统相对复杂,它采用了基于token的认证机制。当你用手机号和密码登录时,客户端会向服务器发送认证请求,服务器返回一个access token,后续的所有请求都需要携带这个token。
认证请求的关键参数
通过分析登录过程的网络请求,我发现Keep的登录接口通常需要以下参数:
| 参数名 | 类型 | 说明 |
|---|---|---|
| mobile | string | 手机号码 |
| password | string | 密码(通常是加密后的) |
| deviceId | string | 设备标识 |
| platform | string | 平台类型(iOS/Android/Web) |
| appVersion | string | 应用版本号 |
密码加密方式
一个重要的发现是,Keep不会直接传输明文密码。它会在客户端对密码进行某种加密处理。通过分析JavaScript代码,我找到了加密逻辑:
// 这是Keep Web版中密码加密的简化逻辑
function encryptPassword(password) {
// 实际实现可能更复杂,这里只是示意
const salt = 'keep_salt_value';
return md5(md5(password) + salt);
}
在Python中实现类似的加密:
import hashlib
def encrypt_password(password):
"""模拟Keep的密码加密逻辑"""
# 第一次MD5
first_hash = hashlib.md5(password.encode()).hexdigest()
# 添加盐值后再次MD5
salt = 'keep_salt_value' # 这需要根据实际情况调整
final_hash = hashlib.md5((first_hash + salt).encode()).hexdigest()
return final_hash
1.3 会话保持与Token刷新
成功登录后,服务器会返回一个access token,这个token有一定的有效期。为了维持会话,需要妥善管理这个token。
Token的存储与使用
class KeepSession:
def __init__(self):
self.session = requests.Session()
self.access_token = None
self.refresh_token = None
self.user_id = None
def login(self, mobile, password):
"""模拟登录流程"""
encrypted_pwd = encrypt_password(password)
login_data = {
'mobile': mobile,
'password': encrypted_pwd,
'deviceId': self._generate_device_id(),
'platform': 'web',
'appVersion': '7.0.0'
}
try:
response = self.session.post(
'https://api.gotokeep.com/v1.1/users/login',
json=login_data,
headers=self._get_common_headers()
)
if response.status_code == 200:
data = response.json()
self.access_token = data.get('accessToken')
self.refresh_token = data.get('refreshToken')
self.user_id = data.get('userId')
return True
except Exception as e:
print(f"登录失败: {e}")
return False
def _get_common_headers(self):
"""构造通用的请求头"""
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Content-Type': 'application/json',
'Authorization': f'Bearer {self.access_token}' if self.access_token else '',
}
return headers
def _generate_device_id(self):
"""生成设备ID,保持一致性很重要"""
# 实际应用中应该保存这个设备ID,避免每次登录都生成新的
import uuid
return str(uuid.uuid4())
重要提示:在实际使用中,你应该将token等信息持久化存储(如保存到文件或数据库),避免每次运行脚本都需要重新登录。同时要注意token的刷新机制,当access token过期时,需要使用refresh token获取新的access token。
2. 应对常见的反爬策略与限制
理解了基本的接口架构后,接下来要面对的是各种反爬机制。Keep作为大型平台,在这方面做得相当完善。
2.1 请求频率限制
这是最常见也最基本的反爬措施。Keep的API接口通常会有严格的频率限制,过于频繁的请求会导致IP被暂时封禁。
合理的请求间隔策略
根据我的测试经验,Keep对同一接口的请求限制大约在每分钟30-60次左右。但这并不是固定值,它会根据服务器负载、用户行为模式等因素动态调整。
import time
import random
from datetime import datetime
class RateLimiter:
def __init__(self, base_delay=1.0, jitter=0.3):
"""
base_delay: 基础延迟(秒)
jitter: 随机抖动范围(0-1)
"""
self.base_delay = base_delay
self.jitter = jitter
self.last_request_time = 0
def wait_if_needed(self):
"""如果需要,等待适当的时间"""
current_time = time.time()
elapsed = current_time - self.last_request_time
if elapsed < self.base_delay:
# 添加随机抖动,避免规律性请求
jitter_amount = random.uniform(-self.jitter, self.jitter)
wait_time = self.base_delay - elapsed + jitter_amount
if wait_time > 0:
time.sleep(wait_time)
self.last_request_time = time.time()
def adaptive_delay(self, response):
"""根据服务器响应自适应调整延迟"""
if response.status_code == 429: # Too Many Requests
# 遇到频率限制,加倍延迟
self.base_delay *= 2
print(f"遇到频率限制,延迟增加到 {self.base_delay} 秒")
time.sleep(5) # 额外等待
elif response.status_code == 200:
# 请求成功,逐渐减少延迟(但不能低于最小值)
self.base_delay = max(0.5, self.base_delay * 0.9)
使用示例
limiter = RateLimiter(base_delay=2.0)
def fetch_workout_list(session, page=1, limit=20):
"""获取运动记录列表"""
limiter.wait_if_needed()
params = {
'page': page,
'limit': limit,
'type': 'running' # 可以根据需要调整运动类型
}
response = session.get(
'https://api.gotokeep.com/v1.1/workouts',
params=params,
headers=session._get_common_headers()
)
limiter.adaptive_delay(response)
if response.status_code == 200:
return response.json()
else:
print(f"获取数据失败: {response.status_code}")
return None
2.2 请求头验证
Keep会检查请求头中的多个字段,确保请求来自合法的客户端。
必须包含的请求头字段
通过分析,我发现以下请求头字段对通过验证特别重要:
| 字段名 | 建议值 | 说明 |
|---|---|---|
| User-Agent | 真实的浏览器UA | 不能使用Python默认的UA |
| Accept-Language | zh-CN,zh;q=0.9 | 语言偏好设置 |
| Referer | https://www.gotokeep.com/ | 来源页面 |
| X-Requested-With | XMLHttpRequest | 标识AJAX请求 |
| X-Keep-Device-Id | 设备ID | 保持一致性 |
完整的请求头构造函数
def build_headers(session, additional_headers=None):
"""构造完整的请求头"""
base_headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Accept-Encoding': 'gzip, deflate, br',
'Referer': 'https://www.gotokeep.com/',
'Origin': 'https://www.gotokeep.com',
'Connection': 'keep-alive',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-site',
'Pragma': 'no-cache',
'Cache-Control': 'no-cache',
}
# 添加认证信息
if session.access_token:
base_headers['Authorization'] = f'Bearer {session.access_token}'
# 添加设备信息
if hasattr(session, 'device_id'):
base_headers['X-Keep-Device-Id'] = session.device_id
# 合并额外请求头
if additional_headers:
base_headers.update(additional_headers)
return base_headers
2.3 行为模式检测
更高级的反爬机制会分析用户的行为模式。如果检测到机器人的行为特征(如固定的请求间隔、从不点击页面元素等),即使单个请求看起来正常,也可能被限制。
模拟人类行为模式
import random
import time
class HumanLikeBehavior:
def __init__(self):
self.request_history = []
def random_delay(self, min_seconds=1, max_seconds=3):
"""随机延迟,模拟人类阅读时间"""
delay = random.uniform(min_seconds, max_seconds)
time.sleep(delay)
return delay
def simulate_scroll(self):
"""模拟页面滚动行为(对API请求可能不需要,但保持完整性)"""
# 在实际的浏览器自动化中,这里会执行滚动操作
# 对于纯API调用,可以记录这个行为模式
pass
def get_variable_page_size(self):
"""随机获取每页的数据量,避免固定的分页模式"""
# 常见的选择:10, 20, 30, 50
sizes = [10, 20, 30, 50]
weights = [0.1, 0.4, 0.3, 0.2] # 20条最常用
return random.choices(sizes, weights=weights)[0]
def should_take_break(self, request_count):
"""每处理一定数量的请求后,模拟休息"""
if request_count > 0 and request_count % 50 == 0:
long_break = random.uniform(30, 120) # 30秒到2分钟的休息
print(f"已处理 {request_count} 个请求,休息 {long_break:.1f} 秒")
time.sleep(long_break)
return True
return False
3. 数据解析与处理技巧
成功获取到数据只是第一步,如何正确解析和处理这些数据同样重要。Keep返回的数据结构相对复杂,而且有些字段是加密的。
3.1 理解Keep的数据结构
Keep的运动数据通常包含多个层级的信息。以下是一个典型运动记录的简化结构:
{
"workoutId": "1234567890",
"type": "running",
"startTime": 1625097600000,
"endTime": 1625101200000,
"duration": 3600,
"distance": 10000,
"calorie": 650,
"averagePace": 360,
"averageHeartRate": 145,
"points": "加密的轨迹数据",
"heartRateData": "加密的心率数据",
"elevationData": "加密的海拔数据"
}
关键字段说明
points: 包含GPS轨迹点的加密数据,这是最有价值的部分heartRateData: 心率数据,通常也是加密的elevationData: 海拔变化数据
3.2 解密轨迹数据
Keep的轨迹数据采用了多层加密。根据我的分析,它通常是经过以下处理:
- 使用特定的算法压缩
- 进行Base64编码
- 可能还有额外的混淆处理
解密步骤示例
import base64
import zlib
import json
def decode_keep_data(encrypted_data):
"""
解密Keep的加密数据
实际实现可能需要根据Keep的具体加密方式调整
"""
try:
# 第一步:Base64解码
decoded = base64.b64decode(encrypted_data)
# 第二步:解压缩(可能是gzip或zlib)
try:
# 尝试gzip解压
decompressed = zlib.decompress(decoded, 16 + zlib.MAX_WBITS)
except:
# 尝试zlib解压
decompressed = zlib.decompress(decoded)
# 第三步:解析JSON
data = json.loads(decompressed.decode('utf-8'))
return data
except Exception as e:
print(f"解密失败: {e}")
# 尝试其他可能的解密方式
return try_alternative_decryption(encrypted_data)
def try_alternative_decryption(data):
"""尝试其他解密方法"""
# 根据实际观察,Keep的数据可能以特定前缀开头
if data.startswith('H4sIAAAAAAAA'):
# 这是gzip压缩的Base64数据的常见开头
try:
# 移除可能的额外字符
clean_data = data.strip()
decoded = base64.b64decode(clean_data)
decompressed = zlib.decompress(decoded, 16 + zlib.MAX_WBITS)
retur

1056

被折叠的 条评论
为什么被折叠?



