在华为云 AI DevSpace 写一个 CSV 工时汇总脚本
周报里的工时经常来自几份表格:同一个项目出现好几次,项目名末尾还可能多一个空格。每次手动筛选、求和,到了下周又得重复一遍。这类工作适合写成一个小脚本,输入原始 CSV,输出按项目汇总的结果。
华为云 AI DevSpace 提供 AI 对话、命令行和文件管理入口,可以用来编写这类办公工具。下面用三条虚构记录演示,脚本只依赖 Python 标准库。文中的汇总结果和错误处理已在本地 Python 环境验证。
先把输入和输出说清楚
进入 AI DevSpace 时,页面会让用户选择体验版或持久化版。我这次选择的体验版标明“数据不保留”,进入空间后能看到剩余时长。试写脚本时,记得在体验结束前把代码和结果另行保存。
在 AI Shell 里,可以把需求写得具体一些:“读取 project,hours 两列的 UTF-8 CSV,按项目累计工时。项目名去掉首尾空格,每条记录允许 0 到 24 小时、最多两位小数。遇到坏数据说明行号并停止,输出文件已存在时不要覆盖。”这几句话决定了脚本遇到空值、错误数字和重复运行时该怎么处理。
创建 hours.csv,放入下面三条记录。这里的每一行代表一条工时登记,24 小时的限制针对单条记录,同一个项目的累计值可以超过 24 小时。
project,hours
文档整理,1.25
接口联调,2.50
文档整理,0.75
再创建 summarize_hours.py,内容如下。运行前在命令行执行 python3 --version,确认环境里有 Python 3。
import argparse
import csv
from collections import defaultdict
from decimal import Decimal, InvalidOperation
from pathlib import Path
def summarize(source, target):
if source.resolve() == target.resolve():
raise ValueError("输入和输出不能使用同一个文件")
totals = defaultdict(Decimal)
with source.open(encoding="utf-8-sig", newline="") as stream:
reader = csv.DictReader(stream)
if reader.fieldnames != ["project", "hours"]:
raise ValueError("表头必须依次为 project,hours")
for row in reader:
line = reader.line_num
if None in row or any(value is None for value in row.values()):
raise ValueError(f"第 {line} 行:列数不正确")
project = row["project"].strip()
if not project:
raise ValueError(f"第 {line} 行:项目名为空")
try:
hours = Decimal(row["hours"].strip())
except InvalidOperation:
raise ValueError(f"第 {line} 行:工时不是数字") from None
if not hours.is_finite() or not Decimal(0) <= hours <= Decimal(24):
raise ValueError(f"第 {line} 行:工时须为 0 到 24 的有限数值")
if hours != hours.quantize(Decimal("0.01")):
raise ValueError(f"第 {line} 行:工时最多保留两位小数")
totals[project] += hours
with target.open("x", encoding="utf-8-sig", newline="") as stream:
writer = csv.writer(stream)
writer.writerow(["project", "total_hours"])
for project, hours in sorted(totals.items()):
writer.writerow([project, f"{hours:.2f}"])
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="按项目汇总 CSV 工时")
parser.add_argument("source", type=Path)
parser.add_argument("target", type=Path)
args = parser.parse_args()
try:
summarize(args.source, args.target)
except (ValueError, OSError) as error:
parser.exit(1, f"错误:{error}\n")
这段代码先读完并检查所有记录,再创建输出文件。第三行工时填错时,程序会报错退出,目录里不会多出一份只算了前两行的汇总表。输出采用 x 模式创建,重复运行时如果目标文件已经存在,会提示文件存在,原来的结果会保留。
CSV 用 utf-8-sig 编码读取,可以处理带 UTF-8 BOM 的表头;newline=“” 让 csv 模块处理换行。工时从文本直接转换成 Decimal,按十进制累加。表头、列数和项目名也一并检查,避免少了一列后把错误带到合计里。
用几条数据检查脚本
在两个文件所在目录运行:
python3 summarize_hours.py hours.csv summary.csv
summary.csv 的内容如下。输出按项目名排序,工时统一保留两位小数。
project,total_hours
接口联调,2.50
文档整理,2.00
先算得对,再看看填错时会怎样。把其中一条工时改成 abc,换一个尚不存在的输出文件名运行,程序会指出对应行的工时不是数字。填入 -1、25 或 0.001,也会被拒绝。NaN 和 Infinity 虽然能被 Decimal 解析,但不能作为工时参与汇总,因此代码另行检查了数值是否有限。
本地检查覆盖了这六种非法输入,以及正常求和、带 BOM 的输入和输出文件已存在的情况。每种非法输入都在创建输出前停止;重复运行也没有改动已有结果。要调整工时范围,可以修改代码中的 24;如果登记单位改成分钟,就需要同时调整字段名、单位和测试数据。
项目名合并还有一个业务问题:去掉首尾空格后,“接口联调”和“接口联调 ”会归为同一项,但“接口联调”和“接口对接”仍是两项。需要合并别名时,可以先维护一张明确的名称对照表。仅凭名字相似就合并,容易把不同项目的工时算到一起。
脚本跑通后,把 summarize_hours.py、hours.csv 样例和运行命令放在一起保存。下周收到新表格,先检查列名和工时单位,再换一个输出文件名运行;拿到汇总表后抽查一两个项目,确认原始记录与合计能对上。
CSV 字段读取的行为可参阅 Python 官方文档中的 csv 模块章节。
- 点赞
- 收藏
- 关注作者
评论(0)