在 AI 开发者空间实现 CSV 账本汇总工具:整数分、退出码与 50 项测试

举报
yd_225105208 发表于 2026/11/07 12:47:31 2026/11/07
【摘要】 在华为云 AI 开发者空间使用 Python 标准库实现 CSV 账本汇总工具:以 Decimal 校验金额、用整数分精确累计,区分 CSV 记录序号与物理行号,并通过 50 项测试及四个 CLI 场景验证退出码和输出。文中记录真实结果与适用限制。

记账数据导出成 CSV 后,按月份、按分类求和,看起来只需要几行代码。但如果直接把金额读成浮点数、遇到坏数据就跳过,得到的结果可能不适合继续交给报表或其他程序使用。

这次我在华为云 AI 开发者空间的体验环境里,用 Python 标准库做了一个小型 CSV 账本汇总工具。它读取本地模拟账本,检查日期、分类和金额,再输出按分类排序的 JSON。最终在云端环境实际运行了 50 项 unittest 测试,以及正常汇总、月份筛选、非法金额和文件不存在四个 CLI 场景。

整个练习只处理合成数据,没有连接真实账单、部署服务或创建收费云资源。下面的代码与输出来自本次实际生成和运行的项目。

一、先把输入和失败方式说清楚

项目只有五个文件:

decimal-ledger-demo/
├── ledger_summary.py
├── test_ledger_summary.py
├── sample_valid.csv
├── sample_invalid.csv
└── README.md

CSV 必须有 date、category、amount 三列,允许额外的备注列。表头名称去除首尾空白后不能重复,每条数据记录的字段数必须与表头一致。文件使用 UTF-8 编码,也接受 UTF-8 BOM。

本次合法样例包含不同月份、闰日、备注中的逗号与双引号:

date,category,amount,note
2024-01-15,food,12.50,lunch
2024-01-20,transport,5.00,bus fare
2024-02-29,food,0.10,leap day snack
2024-02-29,food,0.20,leap day coffee
2024-03-01,books,999.99,"Python, the good parts"
2024-03-10,food,100.00,"""quoted"" note"

日期要能转换成真实日历日期;分类去除首尾空白后不能为空。金额在这个练习中统一表示正数金额,单笔范围是 0.01 到 999999999.99,最多两位小数。零、负数、科学计数法、NaN、Infinity、带加号和 .50 这样的写法都被拒绝。因此,它目前适合汇总一类正数记录;收入、支出和退款的业务模型需要另外设计。

退出码也是输入输出约定的一部分:

退出码 含义 本次实现的例子
0 成功 正常汇总、只有表头、筛选后没有记录
1 数据记录不合法 无效日期、空分类、非法金额、字段数量不一致
2 文件、编码、结构或命令参数错误 文件不存在、非 UTF-8、缺少必需列、重复表头、CSV 解析异常、错误月份参数

正常结果写到 stdout,错误说明写到 stderr。遇到第一条不合法记录就停止,不输出之前累计出的部分结果。这让调用方能够先看退出码,再决定是否接受 JSON。

二、金额从字符串转换为整数分

我采用的处理顺序是:字符串格式校验、Decimal 解析、范围校验、转换成整数分,最后使用 Python 整数累加。

主程序中的金额处理代码如下:

MAX_AMOUNT = Decimal("999999999.99")
AMOUNT_RE = re.compile(r"^[0-9]+(?:\.[0-9]{1,2})?$")

def parse_amount(raw):
    s = raw.strip()
    if not s:
        raise ValueError("金额为空")
    if not AMOUNT_RE.match(s):
        raise ValueError("不是有效的正数十进制形式(最多2位小数)")
    val = Decimal(s)
    if val <= 0:
        raise ValueError("金额必须为正数(非零)")
    if val > MAX_AMOUNT:
        raise ValueError("金额超过最大值 999999999.99")
    return int(val * 100)

Decimal 直接接收原始字符串,中间不经过 float。通过上述范围校验的单笔金额最多有 11 位有效数字,乘以 100 在本程序默认的 Decimal 精度下足够使用。随后所有分类累计都在整数上进行,0.10 与 0.20 分别变成 10 分和 20 分。

输出时使用除法和余数拼接固定两位小数:

def cents_to_str(cents):
    dollars, remainder = divmod(cents, 100)
    return f"{dollars}.{remainder:02d}"

JSON 里的金额保留为字符串,例如 "0.30"。如果下游再把这个字段转换成二进制浮点数,仍需要自行处理后续计算的精度问题。Python 整数没有固定 32 位或 64 位的溢出边界,但处理能力仍然受内存、运行时间和输出转换限制。

三、CSV 记录序号与物理行号要分开

CSV 的引号字段可以包含换行,一条逻辑记录可能占据文件中的多行。手写 split(',') 或把每个换行都当成记录边界,都会破坏这类输入。

本次实现先以 utf-8-sig 读取整个文件,再交给标准库解析:

with open(filepath, "r", encoding="utf-8-sig") as f:
    text = f.read()

reader = csv.reader(io.StringIO(text), strict=True)
raw_header = next(reader)

# 表头是第 1 条记录,数据记录从第 2 条开始。
for rec_no, row in enumerate(reader, start=2):
    ...

因此,数据错误里的 CSV记录序号 3 指第三条逻辑记录,不能一概说成第三个物理文本行。原始 README 里还有“行号”的简略表述,使用时应以最终代码中的“CSV记录序号”为准。

strict=True 会让解析器对某些不合法 CSV 抛出 csv.Error,程序把这类异常归为退出码 2。不过,它不能代替所有业务校验,50 项测试也没有覆盖全部畸形 CSV 输入。

这里还有一个明确取舍:当前版本使用 f.read(),内存占用会随输入文件大小增长,适合这次小文件练习。它没有实现流式读取,不能据此声称适合大规模账单。默认文本换行转换也意味着它不保证原样保留字段内的 CRLF 字节形式。

四、月份筛选之前先验证每条记录

可选参数 --month YYYY-MM 用来筛选月份。命令入口先检查格式,再通过 date(year, month, 1) 检查日历范围,例如 2024-13 会失败。

数据处理循环先验证日期、分类和金额,再判断是否属于目标月份。这样,即使某条记录位于其他月份,其中的不合法数据也会导致本次导入失败。这是当前实现的规则,不能把它理解为只检查筛选月份内的数据。

匹配的记录按分类累计,最终按分类名称排序输出。没有匹配记录时返回 {},退出码仍为 0;只有表头的文件也按空账本处理。空文件则因为缺少表头而返回 2。

五、实际命令与输出

本次在 AI 开发者空间的 /root/workspace/decimal-ledger-demo 目录执行了这些命令。下面保留实际输出与观察到的退出码。

$ python3 ledger_summary.py sample_valid.csv
{"books": "999.99", "food": "112.80", "transport": "5.00"}
exit=0

$ python3 ledger_summary.py sample_valid.csv --month 2024-02
{"food": "0.30"}
exit=0

$ python3 ledger_summary.py sample_invalid.csv
错误: CSV记录序号 3: 金额 '-5.00' 无效: 不是有效的正数十进制形式(最多2位小数)
exit=1

$ python3 ledger_summary.py /tmp/no_such_file.csv
错误: 文件不存在: /tmp/no_such_file.csv
exit=2

第二个场景使用两笔金额 0.10 和 0.20,结果为 "0.30"。非法样例先有一条有效记录,再出现负数金额;程序报错退出,没有把前一条记录的汇总当作成功结果输出。

六、50 项测试具体证明了什么

最终测试命令为:

python3 -m unittest test_ledger_summary -v

云端环境的实际结果:

Ran 50 tests in 0.974s

OK

测试包含金额格式与边界、整数分转换、0.10 + 0.20、多分类汇总与排序、月份筛选、闰日、重复或缺失表头、字段数量不一致、UTF-8 BOM、引号中的逗号、转义双引号、引号内换行、文件不存在、非法 UTF-8、CLI 退出码、无部分输出和输入内容不被修改。

其中一部分测试直接调用解析函数,另一部分通过 subprocess 启动 CLI,检查退出码、stdout 和 stderr。因此,最终结果覆盖了实际命令调用,而不只是函数内部计算。

0.974s 是这一次云端测试套件的运行时间,不能当作数据处理性能基准。50 项通过也不等于所有边界都已验证:文件权限错误、超大文件、异常 Unicode 月份参数、各种畸形引号输入及 Windows 运行行为,本次都没有完整实测。

七、作为小工具,下一步该改哪里

这个练习让我把金额表示、输入校验和命令行失败语义放在同一条流程里检查。实际使用前,我会先根据账单大小决定是否改成流式读取,再补上对应的读取异常测试;如果要处理收入、支出、退款或多币种,则先明确字段和金额规则,避免直接放开负数后混用不同含义。

当前版本保留首个错误后退出,适合发现问题后逐次修正。如果导入场景需要一次展示全部错误,可以另外增加错误收集模式,同时仍要求存在错误时不输出成功汇总。源码、样例、测试和原始运行结果已经保存,本次记录到的行为都可以据此复查。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。