Python CSV 处理:正确应对逗号、换行与 UTF-8 BOM

举报
yd_70403235 发表于 2026/10/27 12:00:20 2026/10/27
【摘要】 CSV 不是简单的按逗号分割。本文用 Python 标准库 csv 演示可靠读写,验证逗号、双引号、字段内换行、BOM 和错误列数,并区分编码、空值及电子表格类型推断问题。

CSV 不是按逗号切开的文本

从数据库导出报表、接收供应商清单,或者把接口数据交给运营人员时,CSV 是常见的交换格式。但它的简单外观容易产生误导:对每行调用 split(‘,’),遇到字段内部的逗号就会错列;先用 splitlines() 拆行,遇到带换行的备注又会把一条记录拆成两条。

正确做法是让 CSV 解析器处理引号和记录边界,再在业务层验证字段。本文用 Python 标准库 csv 演示一次完整的写入与读取,并说明编码、空值和电子表格打开时需要区分的问题。示例是本地功能验证,不涉及外部数据或性能基准。

一、先明确三个约定

第一,编码不是 CSV 格式本身规定的。数据双方应约定 UTF-8 等编码,不能因为文件后缀是 .csv 就假定编码正确。

第二,分隔符也需要约定。本文使用逗号;如果上游使用分号,应显式指定 delimiter=‘;’,不要靠替换文本转换格式。

第三,列名和空值语义属于业务协议。CSV 不会自动保留整数、日期和空值类型,读取到的普通字段通常是字符串。写出 None 时,csv.writer 会将它转换成空字符串,这种转换不可逆;若需要区分“缺失”与“空字符串”,应另行约定表示方法或选择其他格式。

二、最小可运行的往返测试

以下示例可以直接作为 Python 3 脚本运行。内存文本流用于避免创建临时业务文件,文件读写的对应方式见下一节。

import csv
import io


FIELDS = ["id", "name", "note"]


def encode_csv(rows):
    stream = io.StringIO(newline="")
    writer = csv.DictWriter(stream, fieldnames=FIELDS)
    writer.writeheader()
    writer.writerows(rows)
    return stream.getvalue()


def decode_csv(text):
    stream = io.StringIO(text, newline="")
    reader = csv.DictReader(stream)
    if reader.fieldnames != FIELDS:
        raise ValueError("unexpected CSV header")
    result = []
    for record_number, row in enumerate(reader, start=1):
        if None in row or any(value is None for value in row.values()):
            raise ValueError(f"wrong column count at record {record_number}")
        result.append(row)
    return result


def main():
    rows = [
        {"id": "001", "name": "Alice, Bob", "note": 'say "hello"'},
        {"id": "002", "name": "Carol", "note": "line one\nline two"},
        {"id": "003", "name": "", "note": ""},
    ]
    text = encode_csv(rows)
    assert decode_csv(text) == rows
    print("quoted_fields_round_trip:", True)

    data_with_bom = text.encode("utf-8-sig")
    restored = data_with_bom.decode("utf-8-sig")
    assert decode_csv(restored) == rows
    print("utf8_bom_round_trip:", True)

    for bad in ["id,name,note\r\n1,A\r\n",
                "id,name,note\r\n1,A,B,extra\r\n"]:
        try:
            decode_csv(bad)
        except ValueError:
            pass
        else:
            raise AssertionError("invalid column count was accepted")
    print("wrong_column_count_rejected:", True)


if __name__ == "__main__":
    main()

运行时,三个检查项都应输出 True。第一项同时覆盖含逗号、双引号、字段内换行和空字符串的数据;第二项验证带 UTF-8 BOM 的字节可以正常恢复;第三项验证少列和多列记录不会被静默接受。

id 使用字符串,是为了保留 001 这样的前导零。程序若主动调用 int(),前导零就会消失;这不是 CSV 解析器丢失数据。

三、真实文件为什么使用 newline=‘’

实际写入时,可以使用如下方式:

with open("report.csv", "w", encoding="utf-8", newline="") as file:
    writer = csv.DictWriter(file, fieldnames=FIELDS)
    writer.writeheader()
    writer.writerows(rows)

with open("report.csv", "r", encoding="utf-8-sig", newline="") as file:
    reader = csv.DictReader(file)
    for row in reader:
        print(row["id"], row["name"])

此片段中的 FIELDS 和 rows 沿用前文定义。newline=‘’ 将换行处理交给 csv 模块,避免文本层换行转换与 CSV 自身的换行处理叠加,在 Windows 等环境尤其值得注意。不要为了消除所谓“多余换行”,在生成完成后对整个文件做字符串替换;那可能改变字段中的真实换行。

读取采用 utf-8-sig,可以消费开头的 UTF-8 BOM;没有 BOM 的普通 UTF-8 文件也能读取。如果上游带 BOM 却用普通 utf-8 解码,第一列名可能带有不可见的 U+FEFF,导致 row[‘id’] 查找失败。utf-8-sig 不是通用编码检测器,不能据此读取任意 GBK 文件。

四、列数检查和格式严格性不是一回事

DictReader 对少列记录默认补 None,多列记录默认把多出来的字段放到键 None 下。因此示例显式检查这两种情况,而空字符串仍被允许。

示例中的 record_number 是数据记录序号,不是文件物理行号:一条带换行的记录可能跨越多行。需要定位源文件时可结合 reader.line_num,但它表示解析器已读取的物理行数,也不总是记录起始行。

这段代码只演示表头与列数验证,不是任意畸形输入的完整校验器。对不可信输入,还应根据业务设置文件大小、字段长度、记录数量限制,以及必要的数值和日期校验。若需要严格的引号语法处理,可考虑 strict=True 并处理 csv.Error;它仍不替代业务字段验证。

五、能解析不等于电子表格打开后完全一样

CSV 引号用于语法转义,不是数据类型声明。即使正确写出 001,电子表格直接打开时仍可能推断为数字;如果需要保留文本类型,应使用导入向导指定列类型,或选择能明确设置单元格类型的工作簿格式。

同样,来自不可信用户的字段若以等号等特殊字符开头,电子表格软件可能将它解释为公式。给字段加 CSV 双引号不等于消除了公式注入风险。面向电子表格的导出应制定单独的文本化策略,并在目标软件上验证;不要无说明地修改机器间交换的原始值。

总结起来,CSV 的可靠处理有两层:语法层让 csv 模块负责分隔符、引号和换行;协议层明确编码、列结构、空值与类型。把两层分清,才能避免“文件打开没报错,但数据已经变了”的隐蔽问题。

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

评论(0)

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

全部回复

上滑加载中

设置昵称

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

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

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