Python CSV 导入避坑:BOM、空字段、重复表头与行号定位

举报
yd_226410095 发表于 2026/11/03 06:09:15 2026/11/03
【摘要】 用 Python 标准库构建一个可定位错误的 CSV 导入示例,区分 UTF-8 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 有助于报告解析器识别出的格式错误,但不是任意脏数据的自动修复器,也不会替业务判断所有异常。本例把结果收集到列表,适合作为小型导入任务的起点;大文件应改为逐条处理,并结合事务、错误隔离和实际字段约束补充测试。遇到无法可靠判断的编码、分隔符或字段含义,宁可明确拒绝并报告位置,也不要默默猜测后写入数据库。

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

评论(0)

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

全部回复

上滑加载中

设置昵称

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

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

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