简介:这是一套面向健康数据爱好者、数字取证初学者及Python自动化实践者的小米手环数据提取工具集,解决Zepp Life安卓应用本地数据库无法直接访问、原始健康指标难以结构化导出的痛点。资源共7个文件,含6个Python脚本(覆盖数据库解包、备份提取、压力数据解析、压缩包解压及SFTP远程获取等全流程)和1个带详尽中文注释的DATE_DATA.json5配置文件,便于快速理解字段含义与数据映射逻辑,整体压缩包仅8KB,轻量易部署。已有11103人学习下载,说明其在手环数据自主分析、运动健康研究及移动端取证场景中具备较强实用性。使用者可直接运行脚本批量提取心率、睡眠、压力、步数等多维时序数据,并基于json5格式灵活扩展分析逻辑,无需逆向或Root设备,显著降低健康数据二次利用门槛。
1. 项目缘起:为什么我们需要一个自动化导出工具?
如果你和我一样,是个小米手环的长期用户,大概率会遇到过这样的场景:某天心血来潮,想看看自己过去一年的运动趋势、睡眠质量变化,或者想把手环里积累的宝贵数据导出来做个更深入的分析。这时候,你打开小米运动健康App,翻找半天,发现官方只提供了非常有限的单日或单周数据查看,想要批量导出几个月甚至几年的步数、心率、睡眠记录,几乎是不可能的。要么只能对着手机屏幕一张张截图,要么就彻底放弃。这种“数据被困在App里”的感觉,相信很多注重数据记录的朋友都深有体会。
我最初也是被这个问题困扰的。手环默默记录了海量的个人健康数据,这些数据本应是了解自身状态、优化生活方式的绝佳材料,却因为缺乏便捷的导出通道而变得难以利用。官方的数据生态相对封闭,更侧重于App内的即时呈现和社交功能,对于希望进行长期追踪、跨平台分析或建立个人健康数据库的用户来说,并不友好。于是,自己动手打造一个“小米手环数据自动化导出工具”的想法就诞生了。这个工具的核心目标很明确:安全、稳定、无需人工干预地,将小米运动健康App后端服务器上属于你自己的手环数据,定期、完整地抓取下来,并保存为结构化的、易于分析的格式(如CSV或JSON)。
这不仅仅是一个简单的爬虫脚本。它涉及到对非官方API的反向工程、模拟登录以维持长期会话、处理复杂的数据加密与编码,以及设计一套健壮的异常处理与日志机制,确保在无人值守的情况下也能持续运行数月甚至数年。接下来,我将详细拆解这个工具从构思到实现的完整过程,分享其中遇到的技术挑战、解决方案以及那些官方文档里绝不会写的实操细节。
2. 核心原理拆解:数据从手环到我们手中的旅程
在动手写代码之前,我们必须先搞清楚小米手环的数据究竟是如何流转的。只有理解了这条链路,才能找到合适的“介入点”。整个数据流可以概括为以下几个步骤:
手环(传感器采集) -> 手机蓝牙(同步) -> 小米运动健康App(加密打包) -> 小米云端服务器(存储) -> 我们的导出工具(获取)
我们的工具,其作用点就是在最后一步:与小米云端服务器进行通信,模拟官方App的行为,请求并下载数据。这意味着,我们不需要破解手环本身的蓝牙协议(那复杂得多),也不需要Root手机,只需要在服务器层面与小米的API“对话”即可。
2.1 关键环节:认证与会话维持
这是整个工具最核心也是最脆弱的一环。小米的API并非公开服务,它需要合法的用户身份凭证(通常是手机号+密码或验证码登录后获得的Token)来访问。我们的工具必须能完成登录流程,并妥善管理登录后获得的会话状态(如Cookies、Token)。
注意:这里涉及的用户名和密码是你自己的小米账号。工具的设计原则必须是“本地化”或“自托管”,即认证信息只存在于你运行脚本的设备上,绝不传输到任何第三方服务器。这是数据安全和隐私的底线。
登录成功后,服务器会返回一个或多个Token(例如userId,serviceToken,securityToken)。这些Token具有时效性,短则几小时,长则数天或数周。我们的自动化工具必须能检测Token是否失效,并在失效时自动重新登录。一种常见的策略是:首次运行使用账号密码登录,将获取到的Token持久化保存到本地文件;后续运行时,优先尝试使用保存的Token;如果Token失效(通过请求一个简单的用户信息接口如/user/profile来验证),则触发重新登录流程。
2.2 数据请求与解析
一旦认证通过,我们就可以模拟App请求数据了。这里需要借助一些技术手段来获知API的准确地址、参数格式和请求方法(GET/POST)。
- 抓包分析:这是最直接有效的方法。在电脑上设置代理(如Charles或Fiddler),将手机的Wi-Fi代理指向电脑,然后在小米运动健康App中进行操作(如查看“步数”详情页)。此时,抓包工具会捕获到手机与服务器之间的所有HTTP/HTTPS请求。我们需要从中筛选出那些看起来是请求历史数据的API。这些API的URL通常包含
api-mifit.huami.com或类似域名,路径可能包含/v1/data/、/report/、/list等关键词。 - 参数解读:观察抓包到的请求,你会发现除了Token在请求头(Header)里,请求体(Body)或查询参数(Query)中通常包含时间范围(
startTime,endTime)、数据类型(type)、设备标识(deviceId)等。这些参数需要仔细记录和模仿。 - 响应解析:服务器返回的数据通常是JSON格式,但有时可能会被额外编码或加密。你需要查看返回的原始数据,理解其结构。例如,步数数据可能是一个包含日期和步数字段的数组;睡眠数据可能更复杂,包含浅睡、深睡、清醒阶段的时间片列表。
2.3 数据存储与格式化
获取到原始的JSON数据后,我们需要将其转换为更通用的格式。CSV(逗号分隔值)文件是最佳选择之一,因为它可以被Excel、Google Sheets、Python pandas、R等几乎所有数据分析工具直接打开和处理。
这一步的关键在于设计一个清晰的数据表结构。例如,对于步数数据,可以创建steps.csv,包含列:date(日期),total_steps(总步数),calories(卡路里),distance(距离)。对于睡眠数据,可以创建sleep.csv,包含列:date(日期),start_time(开始时间),end_time(结束时间),deep_sleep_minutes(深睡分钟数),light_sleep_minutes(浅睡分钟数),awake_minutes(清醒分钟数),sleep_score(睡眠分数)。
工具应该能够将每次运行获取的新数据,追加(Append)到已有的CSV文件中,而不是覆盖,这样就能形成一个持续增长的个人健康数据库。
3. 技术选型与实现框架
基于上述原理,我选择使用Python作为开发语言。原因很简单:Python在数据处理、HTTP请求、定时任务等方面有极其丰富的库支持,开发效率高,并且易于部署在各种环境(Windows, macOS, Linux, 甚至树莓派)。
下面是一个简化的核心模块设计:
- 认证模块 (Auth):负责处理登录逻辑,管理Token的获取、刷新、保存和加载。使用
requests库发起HTTP请求,使用json库处理响应。 - 数据获取模块 (Fetcher):负责构造针对不同数据类型(步数、心率、睡眠、体重等)的API请求,发送请求并接收响应。需要根据抓包结果,精确还原请求头和请求体。
- 数据解析与存储模块 (Parser & Storage):负责将API返回的JSON数据解析成结构化的Python对象(如字典列表),然后将其转换为Pandas DataFrame,最后写入CSV文件。使用
pandas库可以极大地简化这个过程。 - 调度与日志模块 (Scheduler & Logger):负责让整个流程定期自动执行(例如每天凌晨2点运行一次)。可以使用操作系统自带的
cron(Linux/macOS)或任务计划程序(Windows),也可以使用Python的schedule或APScheduler库在脚本内部实现。同时,需要一个完善的日志系统,使用Python内置的logging模块,记录每次运行的成功与否、获取的数据量、遇到的错误等,便于后期监控和排错。
一个最基本的项目目录结构可能如下所示:
mi_band_exporter/ ├── config.yaml # 配置文件,存放账号、时间间隔、数据存储路径等 ├── auth.py # 认证模块 ├── fetcher.py # 数据获取模块 ├── parser.py # 数据解析模块 ├── storage.py # 数据存储模块 ├── scheduler.py # 调度模块(如果用内部调度) ├── main.py # 主程序入口 ├── tokens.json # 保存的Token文件(.gitignore忽略) ├── logs/ # 日志目录 │ └── exporter.log └── data/ # 导出的数据目录 ├── steps.csv ├── heart_rate.csv └── sleep.csv4. 实操步骤详解与核心代码片段
接下来,我们深入到代码层面。请注意,以下代码仅为示例和思路演示,因为小米的API接口细节可能随时变更,且涉及隐私不便提供完整可用的密钥。
4.1 环境准备与依赖安装
首先,创建一个干净的Python虚拟环境并安装必要的包。
# 创建并激活虚拟环境(以venv为例) python -m venv venv # Windows: venv\Scripts\activate # macOS/Linux: source venv/bin/activate # 安装核心依赖 pip install requests pandas schedule # requests: 用于网络请求 # pandas: 用于数据处理和CSV读写 # schedule: 用于简单的内部任务调度(可选)4.2 认证模块实现
这是最复杂的一步。我们需要模拟App的登录过程。通过抓包,你可能会发现登录流程涉及多个步骤和重定向。
# auth.py import requests import json import time from typing import Optional, Dict import logging logger = logging.getLogger(__name__) class MiFitAuth: def __init__(self, config_path: str = 'config.yaml'): self.session = requests.Session() self.session.headers.update({ 'User-Agent': 'MiFit/4.6.0 (iPhone; iOS 14.4; Scale/3.00)', # 模拟iOS App 'Content-Type': 'application/x-www-form-urlencoded', }) self.load_config(config_path) self.tokens = self.load_tokens() def load_config(self, path): # 从YAML或JSON配置文件读取账号密码 # 这里简化为字典 self.config = { 'username': '你的小米账号手机号', 'password': '你的密码', 'login_api': 'https://account.xiaomi.com/pass/serviceLoginAuth2' # 示例地址,需抓包确认 } def load_tokens(self) -> Dict: try: with open('tokens.json', 'r') as f: return json.load(f) except FileNotFoundError: return {} def save_tokens(self, tokens: Dict): with open('tokens.json', 'w') as f: json.dump(tokens, f, indent=2) def is_token_valid(self) -> bool: """验证当前Token是否有效""" if not self.tokens.get('user_id') or not self.tokens.get('service_token'): return False # 尝试请求一个需要认证的简单接口,如用户信息 test_url = 'https://api-mifit.huami.com/v1/user/profile' headers = {'X-Token': self.tokens.get('service_token')} try: resp = self.session.get(test_url, headers=headers, timeout=10) return resp.status_code == 200 and json.loads(resp.text).get('code') == 0 except Exception as e: logger.error(f"Token验证失败: {e}") return False def login(self) -> bool: """执行登录流程,获取并保存Token""" logger.info("开始登录流程...") # 注意:以下参数名和值都需要根据实际抓包结果填充,这里仅为示意 login_data = { 'user': self.config['username'], 'hash': self._calculate_password_hash(self.config['password']), # 密码通常不是明文传输,需要计算哈希或加密 'sid': 'mifit', # 服务标识 'callback': 'https://api-mifit...', # 回调地址 '_sign': self._generate_signature(...), # 签名,防止篡改 # ... 其他必要参数 } try: resp = self.session.post(self.config['login_api'], data=login_data) resp_data = json.loads(resp.text) if resp_data.get('code') == 0: # 解析响应,提取关键Token new_tokens = { 'user_id': resp_data['userId'], 'service_token': resp_data['serviceToken'], 'security_token': resp_data.get('securityToken'), 'login_time': int(time.time()) } self.tokens = new_tokens self.save_tokens(new_tokens) logger.info("登录成功,Token已保存。") return True else: logger.error(f"登录失败: {resp_data.get('message')}") return False except Exception as e: logger.error(f"登录请求异常: {e}") return False def _calculate_password_hash(self, password: str) -> str: """模拟App计算密码哈希的方法,具体算法需逆向分析""" # 这是一个复杂且可能变动的点。可能需要使用特定的盐(salt)和哈希算法(如SHA1, MD5)。 # 一种常见方法是:hash = md5(salt + md5(password)) # 此处省略具体实现,需通过逆向工程或动态调试获得。 return "需要根据逆向分析实现的哈希值" def get_auth_headers(self) -> Dict: """获取用于API请求的认证头""" if not self.is_token_valid(): if not self.login(): raise Exception("无法获取有效的认证Token") return { 'X-Token': self.tokens['service_token'], 'X-User-Id': self.tokens['user_id'], # 可能还需要其他Header,如`X-App-Version`, `X-Device-Id`等 }核心难点与心得:登录模块的
_calculate_password_hash和_generate_signature函数是整个项目的“钥匙”。小米App为了安全,会对密码和请求参数进行复杂的加密和签名。破解这个环节通常需要逆向工程安卓或iOS的App安装包,分析其加密逻辑。这对于普通开发者门槛较高。一个更简单但可能不稳定的替代方案是:使用验证码登录流程。有些API接口支持通过短信验证码登录,这种方式无需破解密码加密算法,只需要模拟获取和提交验证码的流程。不过,这需要额外的步骤来处理验证码的接收(例如,在电脑端登录时,手机收到验证码后手动输入,或通过某些自动化方式读取)。
4.3 数据获取模块实现
假设我们已经通过抓包,找到了获取步数历史的API。
# fetcher.py import requests import json import time from datetime import datetime, timedelta from auth import MiFitAuth class DataFetcher: def __init__(self, auth: MiFitAuth): self.auth = auth self.base_url = 'https://api-mifit.huami.com' def fetch_steps(self, date_str: str) -> Optional[Dict]: """获取指定日期的步数数据""" # date_str 格式:'2023-10-27' url = f"{self.base_url}/v1/data/step.json" # 示例路径 headers = self.auth.get_auth_headers() params = { 'queryDate': date_str, 'deviceType': '手环设备类型ID', # 需从抓包或用户信息接口获取 # ... 其他必要参数 } try: resp = requests.get(url, headers=headers, params=params, timeout=30) data = resp.json() if data.get('code') == 0: logger.info(f"成功获取 {date_str} 的步数数据") return data.get('data', {}) else: logger.warning(f"获取 {date_str} 步数数据失败: {data.get('message')}") return None except Exception as e: logger.error(f"请求步数API异常 ({date_str}): {e}") return None def fetch_sleep(self, date_str: str) -> Optional[Dict]: """获取指定日期的睡眠数据""" url = f"{self.base_url}/v1/data/sleep.json" headers = self.auth.get_auth_headers() # 睡眠数据可能需要起始和结束时间戳 date_obj = datetime.strptime(date_str, '%Y-%m-%d') start_time = int(date_obj.timestamp() * 1000) # 转换为毫秒时间戳 end_time = int((date_obj + timedelta(days=1)).timestamp() * 1000) - 1 params = { 'startTime': start_time, 'endTime': end_time, 'deviceType': '手环设备类型ID', } # ... 发送请求并解析响应 pass def fetch_heart_rate(self, date_str: str, sample_interval: str = '1min') -> Optional[Dict]: """获取指定日期的心率数据,sample_interval可以是1min, 5min等""" # 心率数据通常是高频采样,数据量较大,API可能分页或按时间段返回 pass4.4 数据解析与存储模块实现
# storage.py import pandas as pd from datetime import datetime import os class DataStorage: def __init__(self, data_dir: str = './data'): self.data_dir = data_dir os.makedirs(data_dir, exist_ok=True) def save_steps_to_csv(self, steps_data_list: list, date_str: str): """将步数数据列表保存或追加到CSV文件""" if not steps_data_list: return # 将原始数据转换为DataFrame # 假设steps_data_list是 [{'date':'2023-10-27', 'steps': 8523, 'calories': 420}, ...] df_new = pd.DataFrame(steps_data_list) file_path = os.path.join(self.data_dir, 'steps.csv') if os.path.exists(file_path): # 读取现有文件 df_existing = pd.read_csv(file_path) # 合并新旧数据,并去重(基于日期) df_combined = pd.concat([df_existing, df_new]).drop_duplicates(subset=['date'], keep='last').sort_values(by='date') df_combined.to_csv(file_path, index=False) print(f"已更新 steps.csv, 新增 {len(df_new)} 条记录。") else: # 首次创建文件 df_new.to_csv(file_path, index=False) print(f"已创建 steps.csv, 保存 {len(df_new)} 条记录。") def save_sleep_to_csv(self, sleep_data_list: list): """保存睡眠数据,睡眠数据结构可能更复杂""" # 睡眠数据可能包含夜间多次醒来,或者午睡。 # 需要根据API返回的格式,设计合适的DataFrame结构。 # 例如:['date', 'start_time', 'end_time', 'deep_sleep_min', 'light_sleep_min', 'awake_min', 'total_min', 'score'] pass4.5 主程序与调度逻辑
# main.py import schedule import time from datetime import datetime, timedelta from auth import MiFitAuth from fetcher import DataFetcher from storage import DataStorage import logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler('logs/exporter.log'), logging.StreamHandler() ]) logger = logging.getLogger(__name__) def daily_job(): """每天执行的任务""" logger.info("开始每日数据导出任务...") auth = MiFitAuth() fetcher = DataFetcher(auth) storage = DataStorage() # 获取昨天的数据(因为今天的数据可能还不完整) target_date = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d') logger.info(f"目标日期: {target_date}") # 1. 获取步数 steps_data = fetcher.fetch_steps(target_date) if steps_data: # 这里需要根据API实际返回结构,将数据转换成storage需要的列表格式 processed_steps = [{'date': target_date, 'steps': steps_data.get('steps', 0)}] storage.save_steps_to_csv(processed_steps, target_date) # 2. 获取睡眠 # sleep_data = fetcher.fetch_sleep(target_date) # if sleep_data: ... # 3. 获取心率(可选,数据量大) # heart_rate_data = fetcher.fetch_heart_rate(target_date, '5min') # if heart_rate_data: ... logger.info("每日数据导出任务完成。") if __name__ == '__main__': # 方法一:使用schedule库进行内部调度(适合长期运行的脚本) schedule.every().day.at("02:30").do(daily_job) # 每天凌晨2:30运行 logger.info("调度器已启动,将在每天02:30运行任务。按 Ctrl+C 退出。") while True: schedule.run_pending() time.sleep(60) # 每分钟检查一次 # 方法二:直接运行一次(适合配合系统cron使用) # daily_job()5. 部署、监控与长期维护的实战经验
将代码跑起来只是第一步,要让这个工具稳定运行数月甚至数年,还需要考虑很多工程化问题。
5.1 部署环境选择
- 个人电脑:最简单,但电脑需要常年开机,且可能因系统更新、休眠而中断。
- 家庭服务器/NAS:如群晖DSM,可以在Docker容器或任务计划中运行Python脚本,非常稳定。
- 云服务器:最可靠,但需要一定成本。可以选择最低配置的Linux云服务器(如1核1G),使用
systemd或supervisor来管理进程,配合cron定时任务。 - 树莓派:性价比极高的选择,功耗低,可7x24小时运行,是完美的家庭自动化节点。
我个人的选择是部署在家庭NAS的Docker容器里。这样既保证了持续运行,又便于管理(镜像打包、日志挂载、配置持久化)。
5.2 异常处理与健壮性增强
最初的脚本可能一遇到网络波动、API变更或Token失效就会崩溃。我们必须增强其健壮性。
- 重试机制:对于网络请求失败,应该加入指数退避的重试逻辑。
from tenacity import retry, stop_after_attempt, wait_exponential @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10)) def fetch_data_with_retry(url, headers, params): response = requests.get(url, headers=headers, params=params, timeout=45) response.raise_for_status() return response.json() - Token自动刷新:在
DataFetcher的每次请求前,都通过auth.get_auth_headers()来获取Headers,而这个方法内部已经集成了Token有效性检查和自动重新登录。确保认证状态总是最新的。 - 数据完整性校验:在保存数据前,检查获取的数据是否包含必要的字段,日期格式是否正确。对于心率这类大数据,检查数据点数量是否在合理范围内(例如,一天24小时,5分钟间隔应有288个点,如果只拿到10个,可能出错了)。
- 详细的日志记录:日志不仅要记录成功和失败,还要记录关键操作的数据摘要,例如“成功获取2023-10-27步数:8523步”、“登录失败,原因为:密码错误”。这能让你在出现问题时快速定位。
5.3 应对API变更
这是所有依赖非官方接口的工具最大的风险。小米可能随时更新其App和后台API,导致我们的工具失效。
- 监控:在日志中增加对特定错误码的监控。例如,如果连续多次请求都返回
code: 5(假设5代表接口废弃),则触发报警(如发送一封邮件到自己的邮箱)。 - 配置化:将API的URL、参数名等提取到配置文件(如
config.yaml)中,而不是硬编码在代码里。当API变更时,你只需要更新配置文件,而无需修改核心代码逻辑。 - 定期手动检查:每隔一两个月,手动运行一次脚本,或者用抓包工具看看最新的App通信格式是否有变化。
5.4 数据备份与扩展
导出的CSV文件是你的宝贵资产。建议定期(例如每周)将data/目录备份到云盘或其他安全位置。
此外,这个工具的框架是通用的。一旦跑通了步数数据,你可以用同样的模式去扩展支持更多数据类型:
- 心率:通常有详细到每分钟的静息心率和运动心率。
- 睡眠:包含睡眠阶段划分。
- 体重/体脂:如果你有小米体脂秤并关联了同一账号。
- 运动记录:每次跑步、游泳的GPS轨迹和详细数据。
- 压力、血氧等(取决于手环型号)。
每增加一种数据类型,就相当于为你的个人健康数据库增加了一个新的维度。
打造这样一个自动化导出工具的过程,更像是一次有趣的探险。你不仅得到了一个解放双手、掌控自身数据的实用工具,更深入理解了移动应用与云端服务交互的细节。当你能随时用自己熟悉的工具(如Jupyter Notebook, Tableau)分析自己多年的运动睡眠趋势时,那种成就感和对自身生活的洞察,是任何现成App都无法提供的。最重要的是,整个过程都在你自己的控制之下,数据始终留在本地,安全且私密。
本文还有配套的精品资源,点击获取