
大概从三年前开始我一直在用 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:30WorkBuddy 通常会把整段时间算在开始的那天这在项目跨天工时统计上会直接造成误差。第三Duration 字段格式不稳定同一个导出文件里可能是 1:30 也可能是 90m如果你不写兼容逻辑转换程序第一轮就会崩。1.2 dsh 格式解决了什么痛点dsh 是我常用的一种目标格式约定核心思想是把上面这些脏活累活都规范化掉。它并不要求一个全新的文件格式——实际上 dsh 就是一个结构固定的 JSON 或 CSV 文件但内部做了三件关键的事字段名统一成 snake_case所有时间字段都带时区偏移比如2025-05-12T09:00:0008: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解释一下两个核心参数--inputWorkBuddy 导出的原始 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:0008:00, source: report.csv, timezone: Asia/Shanghai, records: [ { work_date: 2025-05-12, start_time: 2025-05-12T09:00:0008:00, end_time: 2025-05-12T10:30:0008: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 字段转换说明Datework_date转为YYYY-MM-DD格式不受时区影响Start Timestart_time与 Date 拼接后转 ISO 8601补时区偏移End Timeend_time同上Durationduration_hours统一转为小时小数类似1.5Projectproject若配置了 project-map则按映射替换Tasktask原样保留去除首尾空格Clientclient原样保留Notesnotes保留但会移除可能破坏结构的换行符Billablebillable转为布尔值true/falseRaterate转为数字空值填 0无split布尔值标记是否由跨天拆分产生无record_key去重哈希这里最需要注意的就是 Duration 字段。WorkBuddy 导出的时长格式我至少见过三种1:30、1.5h、90m。转换器内部会按正则去匹配比如^\d:\d{2}$按时分解释^\d(\.\d)?h$直接转小数小时^\dm$换算成小时数。如果你用的是自己的自定义导出模板务必确认这几种格式是否符合否则时长会被解析成 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 默认的encodingutf-8不会自动去掉 BOM。解决办法是在工具的底层读取逻辑里用utf-8-sigimport pandas as pd df pd.read_csv(report.csv, encodingutf-8-sig)如果你在外部手工处理 CSV也可以在打开文件后先检测前三个字节EF BB BF有就跳过。在工具里面我会建议在配置文档中注明推荐将 CSV 另存为 UTF-8 无 BOM 格式或者交给转换器自动识别。5.2 Duration 字段到底代表多少格式并不统一我遇到过一个客户导出的 CSVDuration 列写的是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然后写进 crontab0 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, encodingutf-8) as f: data json.load(f) summary defaultdict(float) today date.today() week_start today - timedelta(daystoday.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 后续被改坏了、或者发现当时的去重策略有问题我也能随时回退到原始数据重新转换。这个习惯帮我救回了好几次月底对账的错误。