☰
从JSON到DSH:WorkBuddy数据转换的Python自动化实践
2026/10/24 8:30:47 网站建设 项目流程

做任务管理的人常有这种经历:录入的时候很爽,等要把数据交给另一套系统时,发现格式完全对不上。我手上的 WorkBuddy 数据就是这个状态。WorkBuddy 是本地优先的待办记录工具,导出的 JSON 保留完整字段,但下游看板流水线认的是 DSH(Delimited Structured Header)格式——一种按块组织的键值文本协议。为了不再每次手动复制几百行,我写了 workbuddy-to-dsh,一条命令把 WorkBuddy 的 JSON 转换成 DSH,同时处理日期格式化、字段映射、空值过滤和增量导出。这篇文章适合三类人:日常用任务管理工具但需要对接自动化的开发者,维护数据流水线的运维同学,以及正在学 Python 脚本、想找一个真实练手项目的初学者。下面我从为什么要写这个工具开始,把配置、命令、踩过的坑一次说清。

1. 为什么要写这样一个转换工具

1.1 任务记录与脚本格式之间的落差

WorkBuddy 的数据不是一张干净的 CSV 表格,而是一个嵌套 JSON。比如一条任务里面可能有标题、状态、优先级、截止时间、标签数组、备注、指派对象、创建时间、修改时间,这些字段是给人类阅读和编辑器用的。下游的 DSH 解析器不会去理解任务的业务字段,它只按固定语法把文本拆成块,凡是符合key: value的行都当作属性处理。要让两端对话,本质上要做一次“嵌套对象转扁平文本”的适配。

这种适配看起来不难,难在稳定。手工转换时,你可能今天把时间写成2026-03-31 18:00,明天写成2026/03/31 18:00,后天带上了GMT。下游解析器一旦遇到不认识的日期,整条任务可能被丢弃,而不是报错提醒你。类似问题还有很多,空值到底输出还是不输出,数组到底用逗号还是分号,任务标题里的冒号会不会把字段行搞乱。这些零散细节在数据结构只有几条时无所谓,当记录数量涨到几百条、而且要每天自动跑时,就必须用一套固定规则来兜底。

1.2 手工转换的痛点到底在哪

我最早也试过人工维护中间文件。做法是打开 WorkBuddy 导出 JSON,筛选需要的字段,再粘贴到 DSH 模板里。前五十条还好,越到后面越容易出问题。第一是复制错位,看错一行导致整条任务用了别人的截止时间;第二是格式不统一,标签在 JSON 里是数组,在 DSH 里可能需要逗号连接,手工容易漏;第三是增量处理非常痛苦,每次同步都要猜哪些任务改过,只能把整个 DSH 文件推倒重来;第四是人工操作不可审计,出了问题说不清是数据源的问题还是转换过程的问题。

这些痛点总结成一句话:任务数量小的时候,人工是成本最低的方案;一旦超过某个量级,人工反而变成最贵的方案。所以我决定写一个命令行转换器,把“数据源解析、字段映射、文本生成、增量判断”这些环节固定下来。人工只需要在开始时维护好映射配置,之后每次跑命令都能得到一致的结果。

1.3 为什么选命令行而不是图形界面

这里有个很容易被忽略的选型理由。图形界面适合交互式修改单条记录,但自动化任务、定时同步、版本管理都需要“无人工参与”的执行方式。命令行工具天然能进流水线,能加参数,能写进计划任务,输出还能直接重定向到文件。更重要的是,命令行工具的所有规则都写在配置和源码里,换一个人换一台机器,跑出来的结果是一样的。

另外,命令行工具方便写测试。我把转换规则拆成纯函数,给定输入 JSON,断言输出的 DSH 文本,跑一轮测试用例大概几十毫秒。这个优势在后续修改字段映射时特别明显:改坏一个规则,测试立刻亮红,而不是等到同步任务时被看板消费者发现。对很多人来说,脚本只要“能跑”就算完,但工具要长期维护,测试和可重复性比第一次跑通更重要。

2. 先看懂两端的数据格式

2.1 WorkBuddy 导出 JSON 长什么样

WorkBuddy 的导出文件我习惯放在~/.workbuddy/export.json。结构很简单,最外层一个版本号,然后是一个 entries 数组:

{ "version": 1, "entries": [ { "id": "wb_001", "title": "整理季度复盘材料", "status": "completed", "priority": 1, "due": "2026-03-31 18:00", "project": "analysis", "labels": ["review", "ppt", "report"], "notes": "把各类指标导成图表,核对上季度数字。", "assignee": "zhangsan", "createdAt": "2026-03-01T10:00:00Z", "updatedAt": "2026-03-29T11:30:00Z" } ] }

可以看到,due是本地时间字符串,没有时区信息;labels是数组;priority是数字而不是人类可读的 P0/P1;status是completed/in_progress/todo这类工具内部枚举。这些细节决定了转换脚本不能只做字符串拼接,必须针对每种类型写处理规则。还有一个容易忽略的地方是assignee字段可能不存在,有些任务没指派负责人,脚本需要补一个默认值,而不是输出assignee:这种空字段。

2.2 DSH 格式的约定

DSH 不是通用标准,我在文章里指的是团队内部使用的一种文本协议:以#开头是注释,以@task id=xxx开头是一个任务块的开始,块内每行都是key: value,块与块之间用空行分隔。完整例子如下:

# Generated by workbuddy-to-dsh 1.2 # source: ~/.workbuddy/export.json @task id=wb_001 title: 整理季度复盘材料 status: done priority: P1 due: 2026-03-31T18:00:00+08:00 owner: zhangsan labels: review,ppt,report desc: 把各类指标导成图表,核对上季度数字。

选这种文本格式有一个好处:它对人类可读,对机器也可解析。中间任何一步出问题,可以直接打开文件看是哪一行语法错误,不必借助复杂调试工具。解析器拿到文件后,按空行和@task切块,再按冒号分隔属性和值,逻辑非常简单。文本格式也天然适合 diff,转完的文件放进版本库,每次改动都能看出是谁、改了什么。

2.3 字段映射不是乱写的

转换工具的核心是映射关系。由于 DSH 需要的字段名跟 WorkBuddy 不一定一致,比如 WorkBuddy 叫assignee,DSH 叫owner,所以必须显式声明。我的做法是维护一个 YAML 配置文件,每个目标字段写清楚来源字段和处理函数。映射表大致如下:

源字段DSH 字段处理方式
idid直接复制
titletitle去掉首尾空白
statusstatus枚举映射:completed→done
prioritypriority数字 1/2/3 映射为 P1/P2/P3
duedue本地时间转 ISO 8601 + 时区
projectproject直接复制
labelslabels数组转逗号分隔
assigneeowner缺失时填 unassigned
notesdesc多行内容转义后输出

映射写清楚还有一个附加好处:下游 DSH 如果增加新字段,不需要改 Python 代码,只要在 YAML 里加一行映射,工具就能按新字段输出。这也是我坚持把映射放进配置文件而不是写死在代码里的原因。写死在代码里维护成本高,改一个字段名就要重新发脚本,还会出现不同机器上脚本版本不一样的问题。

3. 安装与第一跑通:3 个命令完成一次转换

3.1 环境准备

工具本身用 Python 3 实现,我建议至少 3.9 以上,因为用了标准库里的时区处理能力,太低版本会遇到时区解析问题。唯一额外的第三方依赖是 PyYAML,用来读取映射配置。安装命令:

pip install pyyaml

如果你不想装任何第三方库,也可以把映射配置改成 JSON 文件,但我还是推荐 YAML,因为带注释,方便在映射表里写清楚每个字段为什么这样处理。配置文件建议和脚本一起放进同一个项目目录,并用版本库管理,这样之后改映射规则能留下历史记录。

3.2 下载脚本和基本用法

把workbuddy_to_dsh.py保存到项目目录,先不要直接生成文件,用 dry-run 检查输出是否符合预期:

python workbuddy_to_dsh.py export \ --input ~/.workbuddy/export.json \ --mapping mapping.yml \ --output tasks.dsh \ --dry-run

--dry-run会在终端打印转换后的 DSH 内容,但不写文件。我建议第一次接触这个工具的读者先跑这一步,确认字段名和顺序没有明显问题,再进入正式转换。确认无误后,去掉--dry-run执行:

python workbuddy_to_dsh.py export \ --input ~/.workbuddy/export.json \ --mapping mapping.yml \ --output tasks.dsh

如果之前已经跑过一次,想只导出最近改动的任务,加上--since参数:

python workbuddy_to_dsh.py export \ --input ~/.workbuddy/export.json \ --mapping mapping.yml \ --output tasks.dsh \ --since state.txt

3.3 参数速查

使用过程中最常用的参数我整理成了一张表:

参数作用默认值
--inputWorkBuddy 导出 JSON 路径无,必填
--mappingYAML 映射配置路径mapping.yml
--output输出的 DSH 文件路径无,必填
--dry-run只打印不写文件关闭
--since增量导出的状态文件路径无,表示全量
--project只导出指定项目不筛选
--encoding输出文件的编码utf-8
--verbose打印每条任务的处理日志关闭

--project这个参数很实用。比如导出文件里混了五六个项目,但当前流水线只需要处理其中一个,不用改映射配置,直接按项目筛掉其他任务。--verbose则用来排查问题,打开后每条任务如果出现字段缺失、日期格式无法识别,都会打印一行 warning,定位速度会快很多。

3.4 第一次转换的输出与验证

执行正式转换后,可以用两行命令快速验证结果是否完整:

grep -c '^@task ' tasks.dsh grep -c '^[a-z].*: ' tasks.dsh

第一行统计任务块数量,应该和 WorkBuddy 里导出的任务总数一致。第二行统计字段行数量,如果数量明显偏少,说明部分任务字段被过滤掉了,需要回去看映射配置。更稳妥的验证方式是把输出文件存一份快照,后续每次转换都跟快照做 diff:

diff <(tail -n +2 old_snapshot.dsh) tasks.dsh

因为 DSH 文件开头带生成时间,比较时跳过前两行注释。如果 diff 结果里只有预期改动的任务,说明转换过程没有引入意外变化。这一步做好之后,再让下游看板接入 DSH 文件,心里就有底了。

4. 核心转换规则是怎么实现的

4.1 日期与时区处理

日期是最容易翻车的地方。WorkBuddy 里的due经常是"2026-03-31 18:00"这种本地字符串,不带时区。DSH 解析器希望拿到带时区的 ISO 8601 字符串,这样不同机器上读到的时间是同一时刻。我的处理函数如下:

from datetime import datetime, timezone, timedelta DEFAULT_TZ = timezone(timedelta(hours=8)) def to_iso8601(value, tz=DEFAULT_TZ): if value is None or str(value).strip() == "": return None raw = str(value).strip() # WorkBuddy 常见格式:YYYY-MM-DD HH:MM if "T" not in raw and raw.count(":") == 1: dt = datetime.strptime(raw, "%Y-%m-%d %H:%M") return dt.replace(tzinfo=tz).isoformat() # 已经是 ISO 格式,尝试保留 if raw.endswith("Z"): dt = datetime.fromisoformat(raw.replace("Z", "+00:00")) return dt.astimezone(tz).isoformat() dt = datetime.fromisoformat(raw) if dt.tzinfo is None: dt = dt.replace(tzinfo=tz) return dt.isoformat()

实现要点是“没有时区就补默认时区,有 UTC 就转成默认时区”。为什么不能直接用系统本地时区?因为同一份导出文件,在部署机器和本机转换,系统时区可能不同,产出结果就会不一样。配置里显式写默认时区,能保证任何环境跑出来的时间一致。这对后续定时任务特别重要,否则周一早上同步的数据可能出现“时差怎么看都差八小时”的问题。

4.2 嵌套对象与数组的序列化

WorkBuddy 的数据除了扁平字段,还有数组和少量嵌套对象。数组我默认转成逗号分隔,例如labels: review,ppt,report;嵌套对象则按层级太深的字段用点号展平。展平逻辑很简单:

def flatten(data, prefix=""): for key, value in data.items(): full_key = f"{prefix}.{key}" if prefix else key if isinstance(value, dict): yield from flatten(value, full_key) elif isinstance(value, list): yield full_key, ",".join(str(item) for item in value) else: yield full_key, value

需要特别小心的是数组元素本身带逗号的情况。比如标签["A, B", "C"],直接拼接会变成A, B,C,下游解析时很难还原。我在工具里会对逗号和反斜杠做转义,数组元素里的逗号写成\,,解析端再反转义回来。DSH 这种文本格式本身就是约定轻量协议,协议里必须明确规定特殊字符怎么处理,否则转换工具做得再漂亮也会在下游出错。

4.3 空值和缺失字段的兜底策略

WorkBuddy 导出 JSON 里不是每条任务都有全部字段。有的没指派负责人,有的备注为空,有的due可能没填。工具默认行为是:字段值为 None、空字符串、空数组时,直接跳过这个字段不输出。因为大多数 DSH 解析器读到due:后面没内容会当成格式错误,不如不输出,让下游使用默认值。

如果某个字段来源不存在,但映射配置里写了默认值,则会输出默认值。比如owner的默认值我设成unassigned,这样看板上至少能看到一条任务没有负责人,而不是缺失属性。另一个容易踩坑的点是project字段,有些旧任务没有项目归属,直接跳过会导致看板筛选少统计;我的做法是在映射里给 project 也设默认值,保证任务永远不会从看板视图里“消失”。

4.4 状态枚举和字段改名

不同工具的状态词不一致。WorkBuddy 内部可能是completed,DSH 约定用done;in_progress要转成doing。这些枚举映射写死在代码里不够灵活,我放在 YAML 配置的 enum 段里:

enum: status: completed: done in_progress: doing todo: todo

当工具遇到未在枚举表里出现的值,比如status: canceled,会先输出 warning,再原样写到 DSH。这里的原则是“不要静默吞掉不认识的值”,否则下游把一条 canceled 任务当 done 处理,远比多一条 warning 危险。如果你不希望原样输出,可以在配置里加strict_enum: true,让工具直接失败,强制要求你补充枚举。

4.5 增量导出是怎么做到的

全量导出逻辑简单,但任务多之后每次都生成整份 DSH 文件,下游解析时间变长,diff 里也全是噪声。工具用--since参数支持增量导出:读一个状态文件,文件里保存上一次执行的最大updatedAt时间;本次只输出updatedAt大于该时间的任务,并用这一批生成 DSH。处理完成后把新的最大时间写回状态文件。

状态文件内容很简单,只有一行:

last_run=2026-03-30T09:12:34Z

增量导出要特别说明一点:它只能感知新增和修改,感知不到删除。如果某条任务从 WorkBuddy 里被删除,导出 JSON 里不会再有它,工具也不会主动去下游把对应任务块删掉。我的处理办法是每周安排一次全量导出,跟存量 DSH 做 diff,人工确认哪些任务块需要清理。增量负责日常快,全量负责周期纠偏,两者配合才是最稳的状态。

5. 一个完整实操案例:任务看板同步

5.1 需求设定

某个团队的内部看板消费一种 DSH 文件,文件里每个@task块对应一块卡片。团队每人会在 WorkBuddy 里维护自己的待办,星期一早上看板需要把最新任务同步过去,供周会使用。任务量大约一千条,手工处理不现实。需求其实很简单:每周自动从 WorkBuddy 导出所有未完成任务,转成 DSH,并保证时间、负责人、标签、优先级都准确。

5.2 配置文件和命令

我按看板需要的最小字段集写了映射配置,没有把 WorkBuddy 的每个字段都塞进去,只保留了 id、title、status、priority、due、owner、labels、desc 这八个。因为看板不需要createdAt和updatedAt,但增量导出依赖updatedAt,所以工具内部会读它,却不会把它写进 DSH。命令如下:

python workbuddy_to_dsh.py export \ --input ~/.workbuddy/export.json \ --mapping configs/board.yml \ --output /data/board/feed/tasks.dsh \ --since /data/board/feed/state.txt \ --project analysis

--project analysis让工具只导出 analysis 这个项目,其他项目不进看板。如果以后新增项目,只需要在计划任务表再加一条命令,不影响现有逻辑。

5.3 流水线脚本里的调用

直接把命令放进计划任务虽然可行,但不够健壮。我通常包一层 shell 脚本:

#!/usr/bin/env bash set -euo pipefail export LANG=C.UTF-8 PYTHON=/usr/bin/python3 PROJECT_DIR=/opt/wb2dsh $PYTHON "$PROJECT_DIR/workbuddy_to_dsh.py" export \ --input "$HOME/.workbuddy/export.json" \ --mapping "$PROJECT_DIR/configs/board.yml" \ --output /data/board/feed/tasks.dsh \ --since /data/board/feed/state.txt \ --project analysis if [ $? -ne 0 ]; then echo "workbuddy-to-dsh export failed" exit 1 fi

这里有两个经验。第一是set -euo pipefail,脚本里一旦有命令失败就立刻退出,避免生成半截文件骗过下游。第二是在计划任务环境里显式指定 Python 绝对路径,因为计划任务的 PATH 很干净,直接写python很可能找不到命令。我最初就栽在这上面,脚本本地跑没问题,进计划任务后一直报 command not found。

5.4 实际效果与约束

这套方案跑起来后,一千多条任务从读取到生成 DSH 文件,实测不到两秒,大部分时间花在 JSON 解析和 DSH 写入上。每周一自动同步几十条增量,基本是秒级完成。这个案例也让我重新理解了“约束”这件事:DSH 是文本协议,只适合传结构化文本,不适合传附件和图片。任务备注里如果贴了截图,转换成 DSH 时只会把图片路径写进 desc,原始图片还得靠其他方式同步。当初设计映射时就把附件字段排除在外,避免让文本协议背不动的包袱。

6. 常见问题排查实录

6.1 中文乱码和编码问题

WorkBuddy 导出 JSON 通常是 UTF-8,但很多使用系统默认编码的桌面程序读文本文件时默认按本地编码解码,打开的 DSH 文件全变乱码。我的默认编码是utf-8,这符合 Linux 和现代工具习惯。如果下游确实需要兼容旧桌面程序,可以指定--encoding utf-8-sig,输出文件会带 BOM,表格类程序就能正确识别。但要注意 BOM 会污染 Unix 端解析,所以只有确认下游需要时才用它。排查乱码时先看文件头几个字节,再用文件信息命令确认编码,别一上来就怀疑代码。

6.2 日期格式解析失败

WorkBuddy 不同版本导出的日期格式不固定。我见过2026-03-31 18:00、2026/03/31 18:00、2026-03-31三种,甚至还有相对时间“今天下午”。相对时间是转换器最难处理的,需要业务规则才知道“今天”到底是哪天。我的建议是在工具里只支持明确的绝对时间格式,遇到无法解析的值打 warning 并跳过该字段,同时提醒用户在 WorkBuddy 里把时间改标准。绝对不要在脚本里用一堆正则猜日期,猜错的代价比不输出更大。

6.3 输出文件换行符不一致

在 Windows 上写文本文件默认会得到\r\n,而 DSH 解析器按\n切行,最后一行可能出现一个\r残留在字段值里。解决方法很简单:open 输出文件时显式指定newline="\n"。Python 的 open 函数支持这个参数,确保无论在哪台机器运行,生成的 DSH 都是统一的 LF 换行。如果已经生成了错误换行的文件,可以用转换命令修正,但最关键的是在代码里一次根治。

6.4 字段值里带冒号和反斜杠

DSH 用冒号分隔键值,如果标题或备注里出现:,解析器会误判。我的工具默认对所有输出值做转义:反斜杠转成\\,冒号转成\:,换行转成\n。这样 DSH 文件里每一行仍然只有一个键值分隔冒号。下面的代码是整个工具里最简单但最重要的一段:

def escape_value(value): value = str(value) value = value.replace("\\", "\\\\") value = value.replace(":", "\\:") value = value.replace("\n", "\\n") return value.strip()

有人会觉得转义后文本不直观,但它保证了协议稳定。下游解析时先按未被转义的冒号切分,再反转义还原,这个约定要写进 DSH 协议文档,两端都要遵守。

6.5 任务数太大时内存和性能问题

WorkBuddy 单文件通常几千条任务,json.load直接读入内存不存在问题。但如果未来扩展到几万条,建议改用流式解析。我的脚本目前保留json.load的简单实现,因为数据结构是整体数组,流式解析需要把文件拆成 JSONL 或换协议,成本和收益不成比例。更值得关注的是 DSH 写出性能,频繁字符串拼接在大数据量下很慢,我在实现里用列表收集行再用"\n".join输出,实测比一行行写文件快不少。

7. 一些值得单独说的经验

7.1 测试先行,别等接下游才试

转换工具最容易陷入“看起来能跑就行”的陷阱。我建议在开发阶段就准备一个包含各种边界情况的 fixture:空字段任务、无标签任务、带特殊字符标题、不同日期格式、超长备注,然后用一小段测试断言输出 DSH 的每一行。我用的测试框架很轻量,本质就是一组断言。这样后续改映射规则时,回路非常短,几分钟就能确认没有破坏已有规则。没有测试,你只能依赖下游系统反馈,出问题时定位链路很长。

7.2 把转换结果纳入版本控制

DSH 是文本格式,天然适合放进版本库。每次转换如果产生增量,我习惯把结果作为一个文件提交,提交说明写“sync from WorkBuddy”。这样做有两个好处:一是任何一次 DSH 内容变化都能追溯;二是如果下游看板显示错误,能快速对比之前和之后的 DSH,判断问题是出在数据源还是映射配置。如果把转换结果排除在版本控制之外,你会失去很多排查线索。

7.3 只映射下游真正需要的字段

刚开始写映射配置时,我倾向于把 WorkBuddy 所有字段都转进 DSH,觉得这样可以“保留所有信息”。实际运行一段时间后,发现字段太多让 DSH 文件很难读,而且下游根本不消费大部分字段,白白占用解析时间。后来我把映射精简到下游确认需要的最小集,文件瘦身一半,排查问题时一眼能看出来哪几行是有效数据。处理 JSON 原始数据时,信息冗余没关系;生成给下游消费的 DSH 时,按需产出才是合理的。

7.4 在自动转换和消费之间留人工检查窗口

完全自动化能省时间,但不能完全自动消灭错误。我的最终方案是:平时用增量导出让流程自动跑,每周一全量导出后保留一个检查文件,由团队里一个人花五分钟扫一眼,确认没有异常的日期和重复的 task id,再替换线上文件。别看这五分钟很原始,它过滤掉的问题比任何智能检测都多。任务记录是人写的,人会在状态枚举上拼错,会在日期上填错,这类问题是纯格式转换无法纠正的,必须留一道人工兜底。

我在实际使用中体会最深的一句话是:格式转换不是字符串替换,而是一套完整的数据契约。上游 JSON 的字段哪天变了,下游 DSH 的协议哪天改了,工具都应该在第一时间暴露出来,而不是悄悄吞掉错误。workbuddy-to-dsh 让我的任务数据在 WorkBuddy 与看板之间流动起来,如果你想把自己的某个记录工具也接到某种格式上,这套“配置映射 + 增量导出 + 测试兜底”的思路可以直接抄过去。先把一条任务转通,再慢慢增加字段,比一开始就追求覆盖所有情况要稳得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询