Python 标准库检查 CSV:带 BOM、异常金额和跨行记录
导入一份费用 CSV 时,直接按逗号切分很容易漏掉带引号的字段;直接用浮点数相加,也可能让金额出现难以解释的尾数。本文用 Python 标准库做一个小型检查器:先确认表头,再逐条检查分类和金额,最后汇总有效记录并保留异常位置。示例数据均为自造数据。
输入约定
文件使用 UTF-8 编码,表头至少包含 category 和 amount;可以有其他列。分类不能为空,金额必须是有限的非负数。输入示例:
category,amount
food,12.30
travel,3.50
food,0.20
bad,NaN
missing,
extra,1,x
希望得到 food=12.50、travel=3.50,同时报告 NaN、缺少金额和多余字段。异常行不会参与汇总,但也不会让其他有效行丢失。
完整代码
保存为 csv_check.py,在 Python 3 环境运行,无需安装第三方库:
"""Check a small CSV expense export and total valid rows by category."""
import csv
from decimal import Decimal, InvalidOperation
from pathlib import Path
import sys
def inspect_csv(path):
totals = {}
errors = []
valid = 0
with Path(path).open(encoding="utf-8-sig", newline="") as stream:
reader = csv.DictReader(stream, restkey="__extra__")
headers = reader.fieldnames or []
if len(headers) != len(set(headers)):
raise ValueError("表头存在重复列名")
if "__extra__" in headers:
raise ValueError("表头不能使用保留列名 __extra__")
if not {"category", "amount"}.issubset(headers):
raise ValueError("表头必须包含 category 和 amount")
for row in reader:
line = reader.line_num # 多行记录时是结束的物理行号
category = (row.get("category") or "").strip()
raw_amount = (row.get("amount") or "").strip()
if row.get("__extra__") is not None or not category or not raw_amount:
errors.append((line, "列数不匹配或字段为空"))
continue
try:
amount = Decimal(raw_amount)
except InvalidOperation:
errors.append((line, "金额不是数字"))
continue
if not amount.is_finite() or amount < 0:
errors.append((line, "金额必须是有限的非负数"))
continue
totals[category] = totals.get(category, Decimal("0")) + amount
valid += 1
return valid, errors, totals
if __name__ == "__main__":
valid, errors, totals = inspect_csv(sys.argv[1])
print(f"有效记录:{valid};异常记录:{len(errors)}")
for line, message in errors:
print(f"结束行 {line}: {message}")
for category, total in sorted(totals.items()):
print(f"{category}: {total}")
运行命令:
python csv_check.py sample.csv
对于上面的样例,关键输出如下:
有效记录:3;异常记录:3
结束行 5: 金额必须是有限的非负数
结束行 6: 列数不匹配或字段为空
结束行 7: 列数不匹配或字段为空
food: 12.50
travel: 3.50
这几个细节为什么要处理
带 BOM 的表头。 某些导出文件以 UTF-8 BOM 开头;使用 utf-8-sig 读取可以兼容这类文件,也能读取普通 UTF-8。否则第一个表头可能带有不可见字符,导致 category 匹配失败。
CSV 不是简单的 split。 csv.DictReader 能处理引号中的逗号和换行;open 的 newline=“” 让 CSV 模块自行处理换行。多余字段放进保留键 __extra__;程序也禁止表头使用同名列,避免混淆。缺少必需的分类或金额会报错,其他可选列缺失不作检查。
表头重复会丢信息。 字典无法为同一个键保留两个独立值,所以先检查重复列名,直接拒绝这类表头;缺少必需列也会立即报错。行级错误则收集后继续处理。
Decimal 从字符串构造。 Decimal(raw_amount) 避免先经过二进制浮点数;另外 is_finite() 排除了 NaN 和 Infinity。这个小例子允许零、不同小数位和科学计数法;实际财务系统还需定义币种、精度、舍入规则和金额上限,并根据数据规模配置 Decimal 精度。
行号的含义。 reader.line_num 是该条记录读完时的物理行号。一个带引号字段跨越多行时,它指向结束行,而不是记录的起始行;空白行也会影响物理行号。定位问题时应按文件行号检查。
本地验证与适用范围
用一份临时自造 CSV 验证了 BOM、重复分类求和、NaN、空金额、多余字段和跨行字段:4 条有效记录、3 条异常记录,food 汇总为 12.50;另验证了重复表头、缺少必需列和保留列名会被拒绝。示例只输出汇总和错误位置,不输出完整原始行。
这个脚本适合小型、可信来源的数据预检查,不替代完整的数据导入系统。遇到编码错误、文件不存在或严重 CSV 格式问题时仍需要上层捕获异常;用于生产环境前还应补充字段长度限制、严格格式校验、日志脱敏和针对业务规则的测试。参考资料可查 Python 标准库文档中的 csv 与 decimal 模块。
- 点赞
- 收藏
- 关注作者
评论(0)