Python CSV 导入避坑:BOM、空字段、重复表头与行号定位
导入之前,先定义规则
CSV 不能简单地用 split(",") 拆分:字段本身可能包含逗号、引号和换行。标准库 csv 模块负责解析格式,业务代码负责判断哪些数据可以接受。这两层职责分开,错误信息才容易理解。
下面约定表头必须为 name、age、email,顺序固定;name 和 email 必填,age 可为空,非空时只接受 ASCII 非负整数。示例跳过空记录以及全部字段均为空白的记录。这些都是明确的业务选择,而不是 CSV 格式对所有应用的统一要求。
BOM 只处理文件开头
某些 UTF-8 文件开头带有 BOM。若把它作为普通字符读入,第一个字段名可能变成带有 U+FEFF 的 name,导致表头不匹配。示例仅在整个文本开头移除一次 BOM,不能对每条数据的首列都做 lstrip,以免无意改写数据。
实际从文件读取时,可使用 open(path, encoding="utf-8-sig", newline=""),让解码器处理开头的 UTF-8 BOM。这个设置并不意味着它能自动识别 GBK 等任意编码;来源文件的编码仍须先确认。
空字段、重复表头与数字边界
Bob,,bob@example.com 中的空年龄按本例规则映射为 None,不应一律视为错误。字段数量应先检查,再拆包赋值。拼接多个 CSV 片段后,表头可能在中间重复出现;本例将与原表头完全相同的后续记录拒绝,而不是导入为用户数据。
仅用 str.isdigit() 判断后再调用 int() 也不够稳妥:某些 Unicode 数字字符满足 isdigit(),却不适合这里的十进制转换规则。因此示例同时要求 isascii() 与 isdecimal()。年龄上限、邮箱格式和其他领域规则仍需另行添加。
记录序号不等于物理行号
带引号的字段可以跨越多行,所以 enumerate(reader) 给出的记录序号不一定是文本编辑器中的行号。示例同时报告记录序号和 reader.line_num;后者表示当前记录结束时已读取的物理行数。这样既不会把跨行记录误标为单行,也便于定位问题附近的位置。
可运行示例
import csv
import io
EXPECTED = ["name", "age", "email"]
def read_users(text):
if text.startswith("\ufeff"):
text = text[1:]
reader = csv.reader(io.StringIO(text, newline=""), strict=True)
rows, header = [], None
try:
for record_no, row in enumerate(reader, 1):
where = f"第{record_no}条记录(结束于物理行{reader.line_num})"
if not row or all(not cell.strip() for cell in row):
continue
if header is None:
if row != EXPECTED:
raise ValueError(f"{where}表头不符合预期:{row}")
header = row
continue
if row == header:
raise ValueError(f"{where}疑似重复表头")
if len(row) != len(header):
raise ValueError(f"{where}字段数为{len(row)},应为{len(header)}")
name, age, email = (cell.strip() for cell in row)
if not name or not email:
raise ValueError(f"{where}的 name 或 email 为空")
if age and not (age.isascii() and age.isdecimal()):
raise ValueError(f"{where}的 age 不是 ASCII 非负整数")
rows.append({"name": name, "age": int(age) if age else None,
"email": email})
except csv.Error as exc:
raise ValueError(f"物理行{reader.line_num}附近的 CSV 语法错误:{exc}") from exc
if header is None:
raise ValueError("文件没有有效表头")
return rows
sample = "\ufeffname,age,email\nBob,,bob@example.com\n"
print(read_users(sample))
测试与使用边界
该核心解析函数已用内存数据验证:开头 BOM 与空年龄可正常处理;重复表头、非法年龄、Unicode 上标数字、缺列、未闭合引号会被拒绝;带引号的跨行姓名可正常解析,数据字段内的 BOM 不会被擅自删除。测试使用示例数据,未访问真实用户文件或云资源。
strict=True 有助于报告解析器识别出的格式错误,但不是任意脏数据的自动修复器,也不会替业务判断所有异常。本例把结果收集到列表,适合作为小型导入任务的起点;大文件应改为逐条处理,并结合事务、错误隔离和实际字段约束补充测试。遇到无法可靠判断的编码、分隔符或字段含义,宁可明确拒绝并报告位置,也不要默默猜测后写入数据库。
- 点赞
- 收藏
- 关注作者
评论(0)