Python CSV 导入:处理 BOM、跨行字段与列数校验
Python CSV 导入:处理 BOM、跨行字段与列数校验
CSV 导入的难点往往藏在很短的样例之外:备注中可以有逗号,字段可以包含换行,从其他工具导出的 UTF-8 文件还可能带 BOM。如果直接逐行 split(‘,’),这些合法内容会被拆成错误的列。本文用 Python 标准库实现一个只接受固定两列表头的读取函数,并用本地样例验证结构边界。
先明确输入约定
示例只接受 UTF-8 编码的文件,可带 BOM;第一条记录必须严格是 name,note,顺序不可交换。每条后续记录必须恰好两列。空字符串是允许的字段值,空白记录会因为列数不足而被拒绝。这里校验的是结构,姓名是否为空、长度是否合法等业务规则需要另外添加。
读取函数
import csv
def load_records(path):
with open(path, encoding="utf-8-sig", newline="") as stream:
reader = csv.reader(stream, strict=True)
if next(reader, None) != ["name", "note"]:
raise ValueError("表头必须为 name,note")
records = []
for record_no, row in enumerate(reader, start=2):
if len(row) != 2:
raise ValueError(f"第 {record_no} 条逻辑记录列数错误")
records.append(dict(zip(["name", "note"], row)))
return records
utf-8-sig 解码会去掉文件开头的 UTF-8 BOM,也能读取没有 BOM 的 UTF-8 文件。它不是通用编码探测器,不能因此承诺兼容 GBK 或任意电子表格导出格式。
newline=“” 把换行处理留给 csv 模块,避免文本层预先把字段中的 CRLF 统一变为 LF。csv.reader 负责理解引号、分隔符和跨行字段,不需要自己按逗号或物理行切分。strict=True 会在解析器认定输入不合法时抛出 csv.Error,但它不是完整的业务校验器,所以代码仍明确检查表头和列数。
先验证列数再 zip 很有必要。zip 遇到较短一方就结束,如果省略检查,多出的列可能被丢弃,少列的记录也可能被悄悄接受。
构造可复现输入
将下面代码接在函数之后运行,只会在临时目录中创建测试文件:
from pathlib import Path
from tempfile import TemporaryDirectory
with TemporaryDirectory() as folder:
path = Path(folder) / "records.csv"
data = 'name,note\r\nAlice,"hello, world"\r\nBob,"line1\r\nline2"\r\n'
path.write_bytes(data.encode("utf-8-sig"))
rows = load_records(path)
assert rows == [
{"name": "Alice", "note": "hello, world"},
{"name": "Bob", "note": "line1\r\nline2"},
]
print("合法样例通过")
本地实际验证中,BOM 没有混入 name 表头,带逗号的备注保持为一列,Bob 的备注保留了内部 CRLF。另测空文件、错误表头、少列和多列四种输入,均被 ValueError 拒绝。这些结果只覆盖所列样例,不代表所有 CSV 方言都能被此函数接受。
记录编号和错误处理的边界
这里的 record_no 是包含表头的逻辑记录编号。跨行字段会占用多条物理行,错误信息不应将该编号称作文件行号。需要排查物理位置时,可以参考 reader.line_num,它表示解析器已经读过的源文件行数,也不等同于逻辑记录起始行。
调用方还应分别处理文件访问错误 OSError、解码错误 UnicodeDecodeError、CSV 解析错误 csv.Error,以及本函数主动抛出的 ValueError。示例返回列表,适合小文件;大文件应设置大小限制或改为分批处理,同时明确中途失败是否允许已有数据落库。把编码、CSV 解析、结构检查和业务规则分开,导入错误才能被准确定位。
参考:Python 标准库 csv 文档、codecs 文档中的 utf-8-sig 编码说明。
- 点赞
- 收藏
- 关注作者
评论(0)