ARTICLE DETAIL

资讯详情

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

训练数据跨平台迁移:WorkBuddy JSON转DSH格式实战指南

训练数据跨平台迁移:WorkBuddy JSON转DSH格式实战指南 折腾训练数据跨平台迁移的时候我发现了workbuddy-to-dsh这个小工具。它专门把iOS端WorkBuddy导出的JSON体能训练记录标准化成DSH数据格式算是解决了我两年多的痛点。这篇东西把我实际跑通的安装步骤、转换参数和踩过的坑都记下来想上手这类数据管线的朋友可以直接照着抄。先说背景。我一直在用WorkBuddy管理每周训练排程界面清爽和Apple Watch配合得也顺。问题是数据都在它自己的生态里导出成JSON之后字段命名、嵌套结构、时间格式都跟其他分析工具对不上。每次想拉个周维度的心率趋势、做个月度训练量对比都得写一堆一次性脚本去清洗跑了这周下周又失效。后来翻到一个叫workbuddy-to-dsh的命令行转换器核心功能就是把WorkBuddy JSON转成DSH这种更通用的训练数据交换格式中间省掉了我手动映射字段的时间。这篇文章就按我实际操作的顺序来从环境准备到转换命令再到常见坑争取你照着做一遍就能跑通。1. 先搞清楚WorkBuddy数据为什么要转成DSH1.1 WorkBuddy导出的JSON差在哪WorkBuddy在iOS端导出训练记录时默认给的是一个JSON数组里面每条是一次训练活动。大致长这样[ { workoutId: 20D5F2A1-8B3C-4D6E-9A77-5F1B3A02C914, workoutType: cycling, startTime: 2024-03-08T07:30:00Z, endTime: 2024-03-08T08:05:00Z, duration: 2100, distance: 32100, avgHeartRate: 142, maxHeartRate: 168, activeEnergy: 425, samples: [ { time: 0, heartRate: 118 }, { time: 300, heartRate: 138 } ] } ]这个结构本身没有问题问题是它太“iOS私有”。字段名是驼峰风格时间用了ISO 8601的UTC格式心率样本是嵌入在活动对象内部的数组。你拿到一个第三方训练分析平台或者自己写的可视化脚本里对方要的可能是snake_case字段可能是时间戳毫秒值也可能把心率序列单独拆成一张表。字段映射说简单也简单但是量一上来就烦了。更要命的是不同版本WorkBuddy导出的结构还不完全一样有的版本多了averagePace有的版本把distance的单位从米换成了公里。你上个星期写的脚本换个iPhone导出就报错。这种脆弱感正是数据管线最忌讳的。1.2 DSH格式到底长什么样DSH格式简单理解就是一套“健身数据统一语言”。它把训练活动整理成一个结构清晰的JSON文档头部放训练者信息和设备信息主体的activities数组里放每一次活动的元数据和指标序列。我实际转换后得到的数据是这样的{ schema: dsh/1.0, generator: workbuddy-to-dsh v1.2.0, subject: { device: Apple Watch, timezone: Asia/Shanghai }, activities: [ { id: 20D5F2A1-8B3C-4D6E-9A77-5F1B3A02C914, type: cycling, start: 2024-03-08T07:30:00Z, end: 2024-03-08T08:05:00Z, durationSeconds: 2100, distanceMeters: 32100, heartRate: { avg: 142, max: 168 }, energy: { activeKcal: 425 }, series: { time: [0, 300, 600], heartRate: [118, 138, 152] } } ] }你可能会问这不就是把字段名换了一下吗有什么了不起其实关键在于两件事。第一DSH对字段做了明确约定距离统一用distanceMeters时长统一用durationSeconds所有序列单独放到series下面。这样不管数据来自WorkBuddy还是其他App落到DSH之后结构都是一样的。第二DSH自带schema版本号工具在解析时可以判断自己支不支持这份数据避免“字段悄悄变了导致脚本静默出错”的情况。用大白话打个比方WorkBuddy导出的JSON就像你家冰箱里的各种食材每样都有自己的包装和标签但你不统一处理就没法做一顿规整的饭。DSH就是一套“后厨备菜标准”切好的菜全部按统一规格摆在框里下一步你想清炒还是炖汤都方便。2. 装好工具准备一份合格的原始数据2.1 安装workbuddy-to-dsh的两种方式这个工具是个Python命令行程序安装方式比较常规。我自己的主力环境是macOS Python 3.10Windows和Linux我也试过基本没遇到兼容障碍。方式一直接从PyPI安装pip install workbuddy-to-dsh安装完成后验证一下workbuddy-to-dsh --version如果能正常打印版本号说明装好了。如果你的环境里同时有多个Python版本建议先用python3 -m pip install --user workbuddy-to-dsh避免把系统Python弄乱。方式二从源码安装。如果PyPI上的版本滞后或者你想用最新特性可以直接拉仓库git clone https://github.com/your-fork/workbuddy-to-dsh.git cd workbuddy-to-dsh python3 -m venv venv source venv/bin/activate pip install -r requirements.txt这里我强烈建议用虚拟环境尤其是你本机还装了其他数据处理库的时候。我最早就是图省事直接pip install到全局结果跟一个旧版pandas撞了依赖转换时动不动就Segmentation Fault折腾半天才定位到是环境问题。2.2 从WorkBuddy导出原始数据的操作细节转换工具只能处理文件所以你得先把WorkBuddy里的训练记录导出成JSON文件。这一步在App里的路径大概是这样打开WorkBuddy进入“设置”或“数据管理”找到“导出数据”或者“Export Data”选择时间范围我一般选全部然后通过系统分享面板存到“文件”App或者AirDrop到电脑上。有几个细节值得注意。第一导出时如果App有选项问你要不要包含心率明细一定要勾上。DSH里series部分的心率序列是后期做强度分析的宝藏丢了之后转换工具只能给你填平均值信息量少一大截。第二导出文件的编码默认是UTF-8如果你的文件是用Windows上的某些文本工具中转过的记得确认编码没有被改成GBK否则后面解析中文标签时会乱码。第三一次导出可能包含几百甚至上千次训练文件体积从几MB到几十MB都正常转换前瞄一眼文件大小如果是个0字节空文件先检查是不是导出没完成。把导出的workout_export.json放在一个专门的目录里比如~/workbuddy_data/后面所有命令都基于这个目录操作路径上能省很多麻烦。3. 转换实操命令、参数与完整样例3.1 基础转换命令怎么跑通进入数据目录执行最基础的转换命令cd ~/workbuddy_data workbuddy-to-dsh convert \ --input workout_export.json \ --output training.dsh \ --timezone Asia/Shanghai输入、输出、时区这三个参数是我每次必带的。--input指定WorkBuddy导出的JSON文件--output指定生成的DSH文件路径--timezone很关键因为WorkBuddy导出的时间戳是UTC如果不在转换时指定本地时区后面你在看板上看到的所有时间都会整体偏移8小时。跑完命令后终端会打印一行汇总信息比如[OK] converted 327 activities, 2 failed, 1 skipped看到这个数字先别急着高兴2 failed要引起重视。我会立刻去看输出目录下生成的conversion_report.log里面会列出失败活动的ID和原因。大部分时候是某些活动缺了必填字段比如一次没有结束时间的“未完成训练”工具默认就会跳过。后面我在问题排查部分会具体讲怎么处理。3.2 高频参数逐个拆解workbuddy-to-dsh的主要参数我整理成一张表都是实际能用上的参数作用我常用的值--input指定WorkBuddy导出的JSON文件路径workout_export.json--output指定生成的DSH文件路径training.dsh--timezone设置活动时间的显示时区Asia/Shanghai--heart-zones计算心率区间分布需要填写最大心率--heart-zones 185--sample-interval对心率序列做降采样单位秒默认是保留全部--sample-interval 5--drop-incomplete跳过字段不完整的活动配合--strict使用转换历史数据时建议开--pretty以缩进格式输出DSH方便人眼阅读调试时开正式备份可不开--schema-version指定输出的DSH schema版本1.0这里重点说说--heart-zones。DSH格式支持存储心率区间分布比如Z1到Z5各占多少秒但它需要你的最大心率作为基准。最大心率的估算公式很多最粗糙的是220减年龄个人实际用下来更推荐用最近一次高强度间歇训练实测到的峰值心率。我设成185是因为我自己的实测值接近这个数不建议盲目套用网上的公式宁可低估一点也别高估否则Z4/Z5区间会失真。--sample-interval也很实用。如果你一天做了一小时骑行WorkBuddy可能每秒钟都记录一个心率点一小时就是3600个点全部塞进DSH会让文件体积爆炸。设成5秒一个点一小时720个点画趋势图完全够用文件体积小了一个量级。3.3 跑一次完整转换看看输出什么样我拿一次实际的骑行训练做演示。WorkBuddy导出的原始片段[ { workoutId: E0A1F953-2B11-4F4B-8BC2-77AD7F5BBC30, workoutType: cycling, startTime: 2024-04-15T09:00:00Z, endTime: 2024-04-15T09:42:00Z, duration: 2520, distance: 18300, avgHeartRate: 151, maxHeartRate: 174, activeEnergy: 386, samples: [ { time: 0, heartRate: 96 }, { time: 60, heartRate: 110 }, { time: 120, heartRate: 134 } ] } ]执行命令workbuddy-to-dsh convert \ --input workout_export.json \ --output training.dsh \ --timezone Asia/Shanghai \ --heart-zones 185 \ --sample-interval 5 \ --pretty转换完成后打开training.dsh你会看到这条活动变成了这样{ activities: [ { id: E0A1F953-2B11-4F4B-8BC2-77AD7F5BBC30, type: cycling, start: 2024-04-15T17:00:0008:00, end: 2024-04-15T17:42:0008:00, durationSeconds: 2520, distanceMeters: 18300, heartRate: { avg: 151, max: 174, zones: { z1: 120, z2: 540, z3: 1260, z4: 510, z5: 90 } }, energy: { activeKcal: 386 }, series: { time: [0, 5, 10], heartRate: [96, 98, 103] } } ] }注意几个变化。第一startTime变成了start且时区从Z显示成了08:00这意味着时间已经转成上海本地时间了。第二duration变成了durationSecondsdistance变成了distanceMeters单位语义更明确。第三多了一个zones字段这是根据你填的最大心率实时算出来的区间分布。第四samples数组被拆成了两个平行的数组time和heartRate而且每5秒一个采样点。这个输出结构就是DSH的核心价值稳定、语义清晰、方便程序处理。4. 转换后的DSH数据如何使用4.1 用Python快速读取和校验DSH本质是JSON所以读取门槛几乎为零。我经常用Python的pandas配合json模块做初步分析几十行代码就能算出一周的训练量概览import json import pandas as pd with open(training.dsh, r, encodingutf-8) as f: data json.load(f) rows [] for act in data[activities]: rows.append({ start: act[start], type: act[type], duration: act[durationSeconds], distance: act[distanceMeters], avg_hr: act[heartRate][avg], kcal: act[energy][activeKcal], z2_time: act[heartRate][zones].get(z2, 0), }) df pd.DataFrame(rows) df[date] pd.to_datetime(df[start]).dt.date weekly df.groupby(date).agg( total_duration(duration, sum), total_distance(distance, sum), avg_hr(avg_hr, mean), active_kcal(kcal, sum) ) print(weekly)这个脚本我基本每周跑一次输出一张周表一眼就能看出训练量有没有异常比如某天距离突然少了或者平均心率莫名拉到170那大概率是记录有问题回头去查原数据。你还可以做个简单的数据完整性校验把DSH里的活动数量跟WorkBuddy导出的数量对一下。正常情况应该一致如果不一致就去查conversion_report.log里被跳过的记录。4.2 接入个人看板或备份到GitDSH是纯文本JSON这意味着它天生适合放进Git仓库做版本管理。我自己的做法是建了一个training-data仓库每次转换完就把training.dsh和conversion_report.log一起推上去。训练数据是长期积累的个人资产本地硬盘万一坏了就全没了Git托管一份多一层保障还能看到每次转换的差异记录。如果你用Grafana或者Superset这类可视化工具DSH也比较好接。它的结构规整不管是直接拿JSON API去供给图表还是先导入数据库再建看板都比从私有JSON格式解析省事。我目前的方案是把DSH用脚本灌进本地SQLite然后在Grafana上配置一个简单的数据源做一个包含周训练时长、周卡路里、心率区间分布的训练看板。核心工作基本就是写一条SQL不用再做复杂的ETL。5. 真实踩坑记录与问题排查速查表5.1 我遇到过的三类典型问题第一类时区偏移。最典型的表现是转换后的DSH里跑步记录显示的时间比真实时间晚了8小时。这个问题的根源很简单WorkBuddy导出的时间是UTC工具默认按UTC输出。解决方式就是转换命令里务必要写--timezone Asia/Shanghai。如果你发现转换后的数据已经晚了8小时也不需要从头跑一遍用脚本把DSH里所有start和end字段统一加8小时就行但我还是建议回炉重新转换因为series里的时间轴也跟着偏了单独改头部字段不彻底。第二类心率序列缺失。有几次我导出跑步数据时没有勾选包含心率明细结果转换完成后series里只有空的time数组和heartRate数组平均值倒是正常。这种数据拿去做心率区间分析基本是废的。解决方法是去WorkBuddy重新导出确认包含心率明细后再转一遍。如果你想抢救旧数据还有个办法从Apple健康App导出同一时间段的心率数据再手动合并进DSH的series但这属于外科手术级别操作非必要不建议折腾。第三类编码问题导致的中文乱码。如果你的训练备注或标题里有中文但导入DSH时变成了一堆\uXXXX或乱码字符先检查环境变量和终端编码。Windows下尤其容易出问题我建议在命令行先执行chcp 65001 set PYTHONUTF81然后再跑转换命令。macOS和Linux一般没有这个困扰。5.2 常见问题速查表现象可能原因解决办法转换的时间整体偏移8小时导出时间戳是UTC没指定时区加--timezone Asia/Shanghai心率区间全为0转换时没指定最大心率加--heart-zones 最大心率值心率序列为空WorkBuddy导出时未包含心率明细重新导出并勾选心率明细文件名或标签中文乱码终端编码不是UTF-8先执行chcp 65001和set PYTHONUTF81转换报告显示大量failed活动缺少必填字段如结束时间使用--drop-incomplete跳过或手工补全大文件转换特别慢心率样本过密每秒一个点加--sample-interval 5降采样输出的DSH文件巨大未降采样且未压缩加--sample-interval或者转换后用gzip压缩命令提示找不到workbuddy-to-dshpip安装到了用户目录Shell没识别执行python3 -m pip install --user workbuddy-to-dsh并检查PATH我在实际使用中最常干的一件事是每次转换完立刻用那个Python小脚本跑一遍汇总大致扫一眼活动数量、总里程、平均心率这几个数字。不是为了看得多细而是给自己一个快速反馈确认这条数据管线没出问题。工具这东西说白了就是个标准动作的固化真正花时间的地方永远是数据本身的整理和校验。workbuddy-to-dsh帮我把最无聊的字段映射和格式转换这部分自动化了剩下的就是我怎么用好这些已经规整好的数据了。后面你如果想把类似的管线扩展到其他App比如把Peloton或者Strava的导出也转成DSH思路是一样的先摸清原格式再定目标schema最后写转换逻辑。有了这套流程换个数据源也就是再写一个适配器的事。
返回列表