大概从三年前开始,我一直在用 WorkBuddy 记录工时。自由职业嘛,客户多、项目杂,今天给 A 客户做需求,明天帮 B 客户改个小 bug,没有个正经工具根本算不清每个项目到底搭进去了多少时间。WorkBuddy 用起来确实顺手,启动计时器、按项目切任务、随手写备注,这些操作都很直观。但问题是,每次月底要给财务对账、或者自己做项目复盘的时候,它导出的 CSV 总让我有一种"能用但不太顺手"的感觉——项目名和任务名混在一起、导出文件不带时区信息、跨夜的工时不拆开,直接塞进 Excel 透视表永远对不上数。后来我把 WorkBuddy 的导出数据统一重构成一个规范化的 dsh 结构,才算彻底把这个环节打通了。
这里说的 dsh 不是什么玄乎的新语言,而是一套面向下游分析和报表的标准化工时数据格式。配合一个叫 workbuddy-to-dsh 的小工具,能把 WorkBuddy 导出的原始记录转成干净、按天聚合、带校验信息的结构化数据。这篇教程我会从工具的背景讲起,把安装、转换流程、字段映射规则、我踩过的坑,以及最后怎么把转换做成定时自动化,全部过一遍。如果你也在为工时数据清洗发愁,或者想把 WorkBuddy 的数据接到自己的统计系统里,这篇应该能直接帮你跳过不少弯路。
1. 为什么要给 WorkBuddy 配一个 dsh 转换器
先说结论:WorkBuddy 本身的导出功能并不差,它提供了 CSV 和 Excel 两种格式,也允许你勾选需要导出的字段。问题出在"导出的数据是给人看的,不是给程序用的"。日期是本地时间但不带时区标识,时长字段有时候是 1.5h、有时候是 01:30:00,备注里还藏着逗号导致 CSV 解析错位。这些数据拿来手工看看没问题,可一旦要导入财务系统或者做自动化统计,就处处都是雷。
1.1 WorkBuddy 的导出数据到底长什么样
先给大家看一下 WorkBuddy 报告导出最典型的 CSV 结构。一般你会看到类似下面这样的字段:
Date,Start Time,End Time,Duration,Project,Task,Client,Notes,Billable,Rate 2025-05-12,09:00,10:30,1:30,Website Redesign,Frontend Landing,ACME Corp,Initial design review,Yes,80 2025-05-12,10:30,12:00,1:30,Website Redesign,Gatsby Migration,ACME Corp,migrate blog pages,Yes,80 2025-05-13,09:30,11:00,1:30,Internal Tool,Dashboard API,Internal,fix slow query,No,0 2025-05-13,11:00,11:15,0:15,Internal Tool,Sync Job,Internal,retry mechanism,No,0这是很典型的记录式结构:每条记录一个时间段,包含日期、起止时间、时长、项目、任务、客户、备注、是否计费、费率。单独看某一列都没问题,但放在一起就暴露了几个硬伤:
第一,时区信息缺失。如果你今天在外地出差,或者团队分布在不同的时区,这种"裸时间"在聚合时会产生偏移误差。第二,日期和时间是拆开的,但"跨天记录"并没有拆分。比如说你在 22:00 开始干活,干到第二天凌晨 00:30,WorkBuddy 通常会把整段时间算在开始的那天,这在项目跨天工时统计上会直接造成误差。第三,Duration 字段格式不稳定,同一个导出文件里可能是 "1:30" 也可能是 "90m",如果你不写兼容逻辑,转换程序第一轮就会崩。
1.2 dsh 格式解决了什么痛点
dsh 是我常用的一种"目标格式约定",核心思想是把上面这些脏活累活都规范化掉。它并不要求一个全新的文件格式——实际上 dsh 就是一个结构固定的 JSON 或 CSV 文件,但内部做了三件关键的事:
- 字段名统一成 snake_case:所有时间字段都带时区偏移,比如
"2025-05-12T09:00:00+08:00",不会再有歧义。 - 跨天记录自动拆分:一条 22:00 到次日 00:30 的记录会被拆成两条,分别归到两个日期下,并且标记为
split: true。 - 生成稳定的去重键:用"日期 + 开始时间 + 项目 + 任务 + 备注摘要"算出哈希,避免重复导出时产生重复记录。
这样做的好处是,下游消费方完全不需要了解 WorkBuddy 的各种导出怪癖,只需要面对一套干净、稳定的数据结构。对我个人来说,把数据转成 dsh 之后,我做月度账单、做客户汇总,甚至写个小脚本去生成发票草稿,都变得非常简单。
1.3 哪些人适合用这套方案
如果你符合下面任一情况,workbuddy-to-dsh 这套思路对你大概率有用:
- 自由职业者或独立顾问,需要按客户、按项目梳理工时并生成对账单。
- 小团队里负责统计工时的人,需要拿到规范化的数据再做二次加工。
- 有自建报表或 BI 工具(Power BI、Tableau、Metabase 等)的开发者,希望把 WorkBuddy 数据接入现有仪表板。
反过来,如果你只是偶尔看一眼自己干了多少小时,那确实用不上这个转换工具,WorkBuddy 自带的汇总报表就够了。这个工具的价值在于"让数据变得可编程、可复用"。
2. 先把工具装好:环境要求与安装步骤
这部分没什么黑科技,但安装环境往往是最容易卡住新手的地方。我把完整流程拆开讲。
2.1 运行环境准备
workbuddy-to-dsh 是一个 Python 命令行工具,所以第一步是准备 Python 环境。我建议使用 Python 3.9 及以上版本,因为工具内部依赖了一些较新的类型注解和标准库特性。
- 操作系统上,Windows、macOS、Linux 都能跑。
- 唯一的硬性依赖是 Python 可用,并且
pip能正常安装第三方包。 - 如果你以前没装过 Python,建议直接从官网下载安装包,安装时勾选"Add Python to PATH",这一步很重要,否则后面命令行找不到
python。
另外,强烈建议新建一个虚拟环境来装这个工具,不要直接装到系统 Python 里。原因是这个工具会依赖 pandas、click、python-dateutil 等常见库,如果不同项目对 pandas 版本要求不一样,时间一长就会出现"这个项目要 pandas 1.x,那个项目要 pandas 2.x"的冲突。虚拟环境可以把这个烦恼彻底隔离掉。
2.2 安装 workbuddy-to-dsh
在终端里执行:
# 先创建一个虚拟环境(Windows 示例,macOS/Linux 指令略有不同) python -m venv wb2dsh-env # 激活虚拟环境 # Windows: wb2dsh-env\Scripts\activate # macOS/Linux: source wb2dsh-env/bin/activate # 安装工具 pip install workbuddy-to-dsh安装完成之后,检查一下版本,确保命令可用:
workbuddy-to-dsh --version # 输出类似: workbuddy-to-dsh, version 0.3.1如果你是在公司内网环境,pip 默认源连不上,可以切换清华镜像或者你公司的私有源:
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple workbuddy-to-dsh如果项目托管在 GitHub 且你想装开发版,也可以走源码安装:
git clone https://github.com/your-repo/workbuddy-to-dsh.git cd workbuddy-to-dsh pip install -e .-e表示可编辑安装,改完源码立即生效,适合想自己改映射逻辑的玩家。
2.3 安装过程中可能踩的坑
我见过太多人在第一步就卡住,这里提前说三个高频问题。
第一个,pip版本太老,导致依赖解析失败。如果你看到类似ERROR: Could not find a version that satisfies the requirement,先升级 pip 再重试:
python -m pip install --upgrade pip第二个,Windows 下提示'workbuddy-to-dsh' 不是内部或外部命令。这通常不是没装上,而是 Python 的 Scripts 目录没有加入 PATH。最省事的解法是以后用python -m workbuddy_to_dsh代替直接敲workbuddy-to-dsh,或者手动把 Scripts 目录加进环境变量。
第三个,依赖库安装缓慢或超时。尤其在国内网络中比较常见,解决方案就是换镜像源,还可以加上--timeout 60参数。
3. 从 WorkBuddy 到 dsh:核心转换流程与参数解析
装好工具之后,我们就可以开始真正干活了。我先把从 WorkBuddy 到 dsh 的完整链路走一遍,你照着操作就能跑通。
3.1 第一步:从 WorkBuddy 导出原始工时记录
登录 WorkBuddy 网页端,进入 Reports 页面(对应英文是 Reports / Time Reports)。先设置好时间范围,我一般按月导出,方便和账单周期对齐。然后勾选需要的字段,建议至少勾上 Date、Start Time、End Time、Duration、Project、Task、Client、Notes、Billable、Rate。
导出格式这里,我强烈建议选 CSV 而不是 Excel。原因很简单:CSV 是纯文本格式,所有程序都能稳定读取,也不会有 Excel 多 sheet 带来的麻烦;Excel 文件虽然可视化更好,但在程序化处理时反而更容易出现格式兼容问题。
导出的文件一般叫report.csv,里面就是我在上一节展示的那种记录式数据。
3.2 第二步:执行转换命令
打开终端,进入放置 CSV 的目录,然后执行最基本的转换命令:
workbuddy-to-dsh convert --input report.csv --output march.dsh.json解释一下两个核心参数:
--input:WorkBuddy 导出的原始 CSV 文件路径。--output:转换后的 dsh 文件路径,后缀建议用.json或.csv,工具会根据后缀选择输出格式。
如果什么都不加,工具会使用默认配置。但实际场景中,我几乎总是会额外指定几个参数,尤其是时区,否则转换出来的时间默认是 UTC,而你的 WorkBuddy 记录是北京时间,那就全乱了:
workbuddy-to-dsh convert \ --input report.csv \ --output march.dsh.json \ --timezone Asia/Shanghai \ --source-date-format "%Y-%m-%d" \ --source-time-format "%H:%M"这两个 format 参数是按strftime格式写的。WorkBuddy 导出文件里日期和开始/结束时间通常分开成两列,你必须告诉工具它们的确切格式,它才能正确拼接成 ISO 8601 的时间戳。
另外还有一个非常实用的参数:--project-map,可以传入一个 YAML 文件,把 WorkBuddy 里的项目名映射成你内部统一的项目代号。比如 WorkBuddy 里叫Website Redesign,你内部系统叫web-redesign,就可以映射过去:
workbuddy-to-dsh convert \ --input report.csv \ --output march.dsh.json \ --timezone Asia/Shanghai \ --project-map projects.yaml3.3 第三步:检查 dsh 输出文件
转换完成后,打开march.dsh.json,你会看到类似这样的结构:
{ "generated_at": "2025-06-01T09:00:00+08:00", "source": "report.csv", "timezone": "Asia/Shanghai", "records": [ { "work_date": "2025-05-12", "start_time": "2025-05-12T09:00:00+08:00", "end_time": "2025-05-12T10:30:00+08:00", "duration_hours": 1.5, "project": "Website Redesign", "task": "Frontend Landing", "client": "ACME Corp", "notes": "Initial design review", "billable": true, "rate": 80, "split": false, "record_key": "20250512-0900-Website-Redesign-Frontend-Landing-8f3a2c" } ], "summary": { "total_hours": 4.5, "total_billable_hours": 4.5, "record_count": 3, "split_record_count": 0 } }summary字段是转换器自己算出来的汇总,可以用于快速校验。看到它,你基本可以确认转换过程是正常的,原始记录条数、拆分条数都对得上。
4. 转换规则与字段映射细节
了解了整体流程之后,这一节我们深入到底层:WorkBuddy 原始字段和 dsh 字段是怎么对应的?哪些字段需要手工映射?时区和跨天逻辑到底怎么处理?搞懂这些,你以后再遇到字段老对不上号的问题,基本就能自己排查了。
4.1 默认字段映射表
下表是 workbuddy-to-dsh 的默认映射关系,适用于目前最常见的 WorkBuddy 导出模板:
| WorkBuddy 原始字段 | dsh 字段 | 转换说明 |
|---|---|---|
| Date | work_date | 转为YYYY-MM-DD格式,不受时区影响 |
| Start Time | start_time | 与 Date 拼接后转 ISO 8601,补时区偏�移 |
| End Time | end_time | 同上 |
| Duration | duration_hours | 统一转为小时小数,类似1.5 |
| Project | project | 若配置了 project-map,则按映射替换 |
| Task | task | 原样保留,去除首尾空格 |
| Client | client | 原样保留 |
| Notes | notes | 保留,但会移除可能破坏结构的换行符 |
| Billable | billable | 转为布尔值true/false |
| Rate | rate | 转为数字,空值填 0 |
| (无) | split | 布尔值,标记是否由跨天拆分产生 |
| (无) | record_key | 去重哈希 |
这里最需要注意的就是 Duration 字段。WorkBuddy 导出的时长格式我至少见过三种:1:30、1.5h、90m。转换器内部会按正则去匹配,比如^\d+:\d{2}$按时分解释,^\d+(\.\d+)?h$直接转小数小时,^\d+m$换算成小时数。如果你用的是自己的自定义导出模板,务必确认这几种格式是否符合,否则时长会被解析成 0。
4.2 自定义映射的写法
有时候你导出的 CSV 是自定义字段名,比如把Date改成了Day,把Start Time改成了BeginTime,这时候就需要通过 YAML 配置文件做自定义映射。配置格式如下:
mapping: work_date: Day start_time: BeginTime end_time: EndTime duration_hours: DurationMinutes project: ProjectName task: TaskName client: Customer notes: Comment billable: BillableFlag rate: HourlyRate duration_parsing: default_unit: minutes使用时,把这个配置传给--config参数:
workbuddy-to-dsh convert --input report.csv --output out.dsh.json --config mapping.yaml注意,配置里mapping的键必须是 dsh 字段名,值才是你 CSV 里的原始列名。这个方向搞反了,转换出来全是空值,我一开始就栽过这个跟头。
4.3 时区与日期格式的处理
时区是整个转换里最容易出问题的地方,值得单独拿出来说。WorkBuddy 导出的 Date、Start Time、End Time 都是本地时间,没有任何时区信息。如果你不指定--timezone,工具默认按 UTC 处理。对一个在国内使用的自由职业者来说,这意味着每一条记录的时间都会往回调 8 小时,日期甚至都可能退一天。
正确做法是,在转换命令里指定你的时区:
--timezone Asia/Shanghai然后转换器会把本地时间显式加上+08:00偏移。你可能会问:那我换设备、换地区怎么办?我的建议是,以"你实际产生工时所在的时区"为准。如果你要统一成其他时区,也可以指定--output-timezone UTC,工具会先把本地时间解析成带偏移的时间,再转成目标时区。
日期时间拼接时的另一个坑是:WorkBuddy 的 End Time 可能为24:00或者空。24:00其实代表当天的结束但日期要加一天,比如2025-05-12 24:00应该是2025-05-13 00:00。workbuddy-to-dsh 对这个值做了特殊处理:解析到 24:00 时自动把日期加一天。如果你是手工在处理,也千万别直接strptime,那会直接报错。
4.4 跨天记录的拆分逻辑
跨天拆分是 dsh 格式最被看重的能力之一。举个具体例子:你晚上 22:00 开始处理一个紧急需求,到第二天 00:30 收工。WorkBuddy 原始记录很可能是:
2025-05-12,22:00,00:30,2:30,Operation Fix,Incident,Internal,night hotfix,No,0转换器处理这条记录时,会执行以下步骤:
- 识别出
end_time在日期上小于start_time,判定为跨天。 - 把原时段拆成两段:
- 第一段:
2025-05-12 22:00到2025-05-12 23:59:59,时长为 2 小时。 - 第二段:
2025-05-13 00:00到2025-05-13 00:30,时长为 0.5 小时。
- 第一段:
- 两条记录都保留原项目、任务、备注信息,并分别计算 duration_hours。
- 两条记录的
split字段都标记为true,方便你在下游做筛选。
这样,无论是按日统计还是按项目统计,数据的归属天都正确了。我个人的经验是,在做过这个拆分后,月底生成日报时再也不会出现"某天工时特别少、隔天工时特别多"的假象。
5. 我在实际使用中踩过的坑
工具用得越久,越能发现那些文档里不会写的问题。这一节我把自己真实遇到过的坑按"症状、原因、解法"的方式列出来,希望能帮你省点排查时间。
5.1 CSV 编码导致的乱码,以及一个隐形 BOM 坑
第一次转换我就遇到了失败:读进来的第一列表头叫Date,明显是带了 UTF-8 BOM 的字符串被当成普通文本读了。WorkBuddy 导出的 CSV 很常见的是 UTF-8-BOM 编码,而 Python 默认的encoding="utf-8"不会自动去掉 BOM。
解决办法是在工具的底层读取逻辑里用utf-8-sig:
import pandas as pd df = pd.read_csv("report.csv", encoding="utf-8-sig")如果你在外部手工处理 CSV,也可以在打开文件后先检测前三个字节EF BB BF,有就跳过。在工具里面,我会建议在配置文档中注明:推荐将 CSV 另存为 UTF-8 无 BOM 格式,或者交给转换器自动识别。
5.2 Duration 字段到底代表多少,格式并不统一
我遇到过一个客户导出的 CSV,Duration 列写的是90,既不是1:30也不是1.5h。后来查了 WorkBuddy 的设置才发现,他那个版本把总时长默认设为"分钟"单位。也就是说90代表 90 分钟,而不是 90 小时,也不是 90 秒。
所以转换器提供duration_parsing.default_unit配置项,我建议你在第一次转换前先人工检查 3 到 5 条数据,确认 Duration 的真实含义,再决定配置。另外,如果同一份导出里混了多种格式,比如大部分是1:30,偶尔出现1.5h,工具会自动按格式匹配,但混用本身也说明导出模板被修改过,要多留个心眼。
5.3 重复导出带来的重复记录和去重策略
这是最隐蔽的坑。WorkBuddy 的报告导出不是幂等的,同一个时间范围导出两次,得到的数据在某些情况下会有细微差异——可能是小数点四舍五入不一样,可能是追加了新的备注,甚至可能是字段顺序变了。直接覆盖原 CSV 再重新转换时,如果忽略了这些差异,生成的 dsh 里就会有重复工时,然后账单金额直接翻倍。
workbuddy-to-dsh 生成record_key的算法是这样的:
raw_key = f"{work_date}|{start_time}|{project}|{task}|{notes}" import hashlib record_key = hashlib.md5(raw_key.encode("utf-8")).hexdigest()[:8]转换器会在输出阶段扫描所有record_key,如果发现重复,默认情况下会保留第一条并给后续重复项打上duplicate: true,同时打印警告。我建议你在第一次转换时就开启--strict-dedup,如果有重复记录直接让命令以非零退出码结束,这样你就能及时发现是不是重复导出了。
5.4 项目名带逗号、引号和换行时,CSV 解析容易全线崩盘
WorkBuddy 是一个英文工具,它的 Notes 字段允许任何自由文本,所以备注里很可能出现逗号、双引号,甚至整段换行。如果 CSV 的解析规则不够健壮,就会出现字段错位。举个极端例子:
2025-05-12,11:00,12:00,1:00,Design,Logo Review,ACME,"Need to check the new color palette, especially for dark mode",Yes,100注意这里 Notes 字段内部有换行和逗号,标准 CSV 是允许的,前提是字段必须用双引号包住,内部的双引号还要转义。处理这类数据时,我不会直接用像split(",")这样的简单方法,而是建议用 pandas 或者 Python 内置的csv模块。workbuddy-to-dsh 内部用的就是标准 CSV 解析器,但如果你要自己写脚本,千万别嫌麻烦去手动 split。
同理,在写自定义映射配置时,如果项目名出现在 YAML 文件的 key 或 value 里,也尽量加上引号,防止特殊字符干扰解析。
6. 把转换做成自动化:定时任务与后续分析
跑通一次转换只是开始,真正让这个工具发挥价值的是把它嵌入到你的工作流里,让它每天都自动运行,然后在固定的时间给你推送一份干净的数据。
6.1 定时执行转换
如果你需要每天或每周自动把 WorkBuddy 数据转换成 dsh,可以借助系统自带的任务调度器。
macOS 或 Linux 下,我一般是写一个脚本然后交给 cron。假设脚本路径是/home/me/bin/daily_convert.sh:
#!/bin/bash cd /home/me/workbuddy-to-dsh source wb2dsh-env/bin/activate # 每天凌晨 1 点执行昨天数据的转换 python -m workbuddy_to_dsh convert \ --input latest_export.csv \ --output data/daily_$(date +%Y%m%d).dsh.json \ --timezone Asia/Shanghai \ --strict-dedup然后写进 crontab:
0 1 * * * /home/me/bin/daily_convert.shWindows 下则是用"任务计划程序"创建基本任务,指定每天触发时间,操作设置为"启动程序",程序填虚拟环境里的python.exe,参数填-m workbuddy_to_dsh convert ...。
这里有个非常重要的小建议:不要把定时任务直接指向旧导出的同一个文件。因为 WorkBuddy 不会自动更新本地 CSV,你需要先有一个"下载最新导出并保存成固定文件名"的步骤。可以写一个下载脚本,或者在 WorkBuddy 里设置定期自动发送报告到指定邮箱,再用邮件附带的方式落盘。
6.2 dsh 数据还能怎么用
数据转成 dsh 格式之后,下游能做的分析就非常多了。简单说几种我实际用过的场景。
第一种是接到 Excel 透视表。把 dsh 导出成 CSV,然后直接导入 Excel,插入透视表,按 Client、Project 拖拽,就能快速看到每个客户当月累计工时和账单金额,比在 WorkBuddy 网页端里看报表灵活很多。
第二种是接入 Power BI 或 Tableau。dsh 的 JSON 字段结构稳定,在 Power BI 里直接通过"JSON 连接器"加载,就会自动识别records数组里的字段。之后做时间趋势、项目占比分析基本就是拖拽的事。
第三种是自己写脚本生成周报摘要。下面是一段很简单的 Python 示例,读入 dsh JSON,按项目汇总本周工时:
import json from collections import defaultdict from datetime import date, timedelta with open("march.dsh.json", "r", encoding="utf-8") as f: data = json.load(f) summary = defaultdict(float) today = date.today() week_start = today - timedelta(days=today.weekday()) for record in data["records"]: rd = date.fromisoformat(record["work_date"]) if week_start <= rd <= today: summary[record["project"]] += record["duration_hours"] for project, hours in sorted(summary.items()): print(f"{project}: {hours:.2f}h")这段代码虽然简单,但配合summary字段里的total_hours字段做交叉验证,基本可以保证数据的准确性。
6.3 后续扩展:从转换到小报表
我自己在自动化跑通之后,又往上叠了两层:一层是生成月度账单草稿,另一层是往团队的企业微信群推送每日工时摘要。前者是从 dsh 的billable和rate字段算金额,后者是直接把summary拼进消息文本。大家以后用熟了,完全可以按自己的需求扩展。核心思路是一样的:dsh 是一个中间格式,它负责把 WorkBuddy 的"原生数据"变成"稳定数据",至于这些稳定数据能长出什么,完全取决于你的想象力。不过需要提醒的是,涉及金额相关的自动化一定要保留原始 CSV 备份,避免在连续转换链中出现偏差时没法追溯。
最后再分享一个我个人的习惯:每次转换完之后,我会把原始 CSV 改名归档到archive/目录,文件名带上日期范围。这样就算 dsh 后续被改坏了、或者发现当时的去重策略有问题,我也能随时回退到原始数据重新转换。这个习惯帮我救回了好几次月底对账的错误。