首页
/ Python 标准库 csv 模块权威指南:CSV 读写、方言(Dialect)机制与格式嗅探

Python 标准库 csv 模块权威指南:CSV 读写、方言(Dialect)机制与格式嗅探

2026-09-07 11:44:58作者:秋泉律Samson

本文以 CPython 仓库中 Doc/library/csv.rst 为骨架,结合其纯 Python 封装层 Lib/csv.py 与 C 加速核心 Modules/_csv.c 的源码,系统讲解 csv 模块的读写 API、方言与格式化参数、六个引用常量、Sniffer 自动探测机制以及错误处理实践。读完本文,你将能熟练读写 Excel/UNIX 风格表格数据、注册自定义方言、处理带引号与转义字符的复杂字段,并利用 Sniffer 猜测未知文件的格式。

CSV 格式的由来与 csv 模块的定位

CSV(Comma Separated Values,逗号分隔值)是电子表格与数据库之间最常用的导入导出格式。虽然社区曾尝试用 RFC 4180 对其标准化,但在标准出现之前的多年使用中,不同应用产出的数据在分隔符、引号字符与换行约定上存在大量细微差异,导致"跨来源处理 CSV"成为一件麻烦事。幸运的是,这些差异只集中在分隔符与引用字符层面,整体结构足够相似,因而可以用一个统一模块在幕后隐藏读写细节,这就是 csv 模块的出发点。

csv 模块实现了 PEP 305(CSV File API)所描述的接口,提供以下能力:

  • 让程序员以"Excel 偏好的格式"写入数据、读取 Excel 生成的文件,而无需了解 Excel 的具体 CSV 规则;
  • 描述其他应用所理解的 CSV 格式,或自定义专用 CSV 格式;
  • readerwriter 对象负责处理序列(sequence)形式的行;
  • DictReaderDictWriter 类负责以字典形式读写数据。

在代码结构上,CPython 将 csv 拆成两层:Lib/csv.py_csv 导入 Errorreaderwriter、方言注册等底层构件并实现 Dialect/excel/excel_tab/unix_dialect/DictReader/DictWriter/Sniffer 等类;真正的解析状态机与写出行拼接逻辑位于 Modules/_csv.c。源码注释明确提示"不应直接使用 _csv,请导入 csv.py 模块"。

模块核心函数:从 reader 到方言管理

本节对应原文档 "Module Contents" 一节的函数部分,逐一说明签名、语义与底层实现。

reader(csvfile, /, dialect='excel', **fmtparams)

返回一个处理 csvfile 各行数据的 reader 对象csvfile 必须是"字符串的可迭代对象",最常见的是newline='' 打开的文件对象或 list;若传入文件对象而未指定 newline='',带引号字段内嵌的换行将无法被正确解析,且在 \r\n 行尾平台上写入时还会多出一个 \r——这是因为 csv 模块会自己做(universal)换行处理,详见原文档脚注 [1]。

  • dialect:可选,可以是 Dialect 子类的实例,或 list_dialects() 返回的名字字符串;
  • fmtparams:可选关键字参数,用于覆盖当前方言中的单个格式化参数(完整清单见后文"格式化参数"一节)。

每次读取到的行以字符串列表返回,默认不做任何类型转换;仅当指定 QUOTE_NONNUMERIC 时,未加引号的字段才会被转换为 float

官方示例(文件 eggs.csv 的内容为 Spam Spam Spam Spam Spam |Baked Beans|Spam |Lovely Spam| |Wonderful Spam|):

import csv
with open('eggs.csv', newline='') as csvfile:
    spamreader = csv.reader(csvfile, delimiter=' ', quotechar='|')
    for row in spamreader:
        print(', '.join(row))

writer(csvfile, /, dialect='excel', **fmtparams)

返回一个 writer 对象,负责把用户数据转换为定界字符串并写入 csvfilecsvfile 可以是任何带 write 方法的对象,若为文件对象同样应以 newline='' 打开。

两个关键的数据转换规则(对应源码中 csv_writerow_lock_held 的主循环):

  1. None 被写为空字符串——这是为了对接 DB-API:SQL 的 NULL 值可以直接经 cursor.fetch* 转储到 CSV,无需预处理。不过原文也明确指出这是不可逆的转换;
  2. 所有非字符串数据先经 str() 字符串化再写出。在 Modules/_csv.c 中,非字符串字段正是通过 PyObject_Str(field) 转换后统一进入缓冲区拼接的。

官方写入示例:

import csv
with open('eggs.csv', 'w', newline='') as csvfile:
    spamwriter = csv.writer(csvfile, delimiter=' ',
                            quotechar='|', quoting=csv.QUOTE_MINIMAL)
    spamwriter.writerow(['Spam'] * 5 + ['Baked Beans'])
    spamwriter.writerow(['Spam', 'Lovely Spam', 'Wonderful Spam'])

方言注册表管理:register_dialect / unregister_dialect / get_dialect / list_dialects

这四个函数构成全局方言注册表(在 C 层由 _csvstate->dialects 字典承载):

  • register_dialect(name, /, dialect='excel', **fmtparams):把 dialect 与字符串 name 关联。可以传入 Dialect 子类、fmtparams 关键字参数、或两者兼有(关键字参数优先覆盖方言中的参数);
  • unregister_dialect(name):从注册表删除名为 name 的方言;若 name 未注册则抛出 csv.Error
  • get_dialect(name):返回与 name 关联的方言(返回不可变Dialect),未注册时同样抛出 Error
  • list_dialects():返回所有已注册方言的名字。

模块导入时即在注册表中登记了三个内置方言(见 Lib/csv.py 顶层代码):excelexcel-tabunix

field_size_limit() / field_size_limit(new_limit)

返回解析器允许的最大字段长度;传入 new_limit 时则设置新上限。该上限的实现位于 Modules/_csv.cparse_add_char:当累计字符数 field_len 达到 field_limit 时抛出 "field larger than field limit (%zd)" 错误。字段缓冲区以 4096 字符起步、容量不足时按 2 倍倍增(parse_grow_buff),但单字段总长度受此上限约束。默认值足够处理常规表格,但遇到超长文本字段(如整列嵌入的 JSON)时需按需调大。Lib/test/test_csv.pyTestFieldSizeLimit 用例验证了设置、恢复与非法参数的报错行为。

字典化读写:DictReader 与 DictWriter

原文档将两个字典映射类与 DialectSniffer 一并列为模块类,这里重点展开它们的参数语义。

DictReader(f, fieldnames=None, restkey=None, restval=None, dialect='excel', *args, **kwds)

行为类似普通 reader,但把每行信息映射为以 fieldnames 为键的 dict

  • fieldnames 是序列;省略时取文件首行值作为字段名,且首行会从结果中剔除显式提供时,首行数据会正常出现在结果里。无论字段名如何确定,字典都保持原始顺序。若传入的是迭代器,会被强制转成 list
  • 行字段多于 fieldnames:多余数据放入列表,挂在 restkey 指定的键下(默认 None);
  • 非空行字段少于 fieldnames:缺失值以 restval 填充(默认 None);
  • 其余可选/关键字参数全部透传给底层 reader 实例。

实现上(Lib/csv.py DictReader.__next__),字段名通过惰性属性 fieldnames 在首次访问或读取首条记录时初始化;空白行会被跳过(避免产出一堆 None 的字典);当 lf < lr 时把余下列挂在 restkey 下,lf > lr 时用 restval 补齐。返回行类型在 3.6 为 OrderedDict、3.8 起改为普通 dict

官方示例:

>>> import csv
>>> with open('names.csv', newline='') as csvfile:
...     reader = csv.DictReader(csvfile)
...     for row in reader:
...         print(row['first_name'], row['last_name'])
...
Eric Idle
John Cleese
>>> print(row)
{'first_name': 'John', 'last_name': 'Cleese'}

其中 names.csv 内容为:

first_name,last_name
Eric,Idle
John,Cleese

DictWriter(f, fieldnames, restval='', extrasaction='raise', dialect='excel', *args, **kwds)

行为类似普通 writer,但把字典映射为输出行:

  • fieldnames 必须是键序列,它决定各字典值写入文件的顺序。注意:与 DictReader 不同,DictWriterfieldnames 不可省略
  • restval:当传入字典缺少 fieldnames 中的某个键时,写入该默认值(默认空字符串 '');
  • extrasaction:当传入字典含有 fieldnames 之外的键时的行为——默认 'raise' 抛出 ValueError;设为 'ignore' 则忽略多余键。构造函数的 _dict_to_list 即先检查 rowdict.keys() - self.fieldnames 是否非空,再按 fieldnames 顺序用 rowdict.get(key, self.restval) 生成输出序列;
  • 其余参数透传给底层 writer

官方示例:

import csv

with open('names.csv', 'w', newline='') as csvfile:
    fieldnames = ['first_name', 'last_name']
    writer = csv.DictWriter(csvfile, fieldnames=fieldnames)

    writer.writeheader()
    writer.writerow({'first_name': 'Baked', 'last_name': 'Beans'})
    writer.writerow({'first_name': 'Lovely', 'last_name': 'Spam'})
    writer.writerow({'first_name': 'Wonderful', 'last_name': 'Spam'})

生成 names.csv 内容:

first_name,last_name
Baked,Beans
Lovely,Spam
Wonderful,Spam

内置方言与 Dialect 容器类

Dialect 是一个容器类,其属性描述解析/写入时如何处理双引号、空白、分隔符等。由于缺少严格规范,不同应用产出细节各异的 CSV,Dialect 实例正是用来约束 reader/writer 行为的。所有可用方言名由 list_dialects() 返回,可在 reader/writer 构造时通过 dialect 参数引用,例如:

import csv

with open('students.csv', 'w', newline='') as csvfile:
    writer = csv.writer(csvfile, dialect='unix')

CPython 内置三个方言类(定义与注册均在 Lib/csv.py 顶部,底层属性表见 Modules/_csv.cDialectObj):

方言类 注册名 关键属性 适用场景
excel() 'excel' delimiter=','quotechar='"'doublequote=Trueskipinitialspace=Falselineterminator='\r\n'quoting=QUOTE_MINIMAL Excel 生成的 CSV
excel_tab() 'excel-tab' 继承 excel,仅 delimiter='\t' Excel 生成的 TAB 定界文件
unix_dialect() 'unix'(3.2 新增) delimiter=','quotechar='"'doublequote=Trueskipinitialspace=Falselineterminator='\n'quoting=QUOTE_ALL UNIX 系统生成的 CSV(所有字段都加引号)

从源码结构看,excel_tab 直接继承自 excel 只改写分隔符;unix_dialectexcel 的关键差异在于行终止符为 '\n' 且采用 QUOTE_ALL。另外 Lib/csv.pyDialect 基类还实现了 __replace__copy.replace 协议)与 _validate:前者用于在副本上替换指定格式化参数并重新校验,未知关键字会抛 TypeError;后者把 self 交给 C 层 _Dialect 构造以校验属性取值是否合法。

Sniffer:自动识别 CSV 格式

Sniffer 类用于反推(sniff)一个未知 CSV 文件的格式,提供两个方法与一个属性。

sniff(sample, delimiters=None)

分析给定的 sample 文本,返回反映其格式参数(delimiterquotecharescapechardoublequoteskipinitialspacelineterminator)的 Dialect 子类。可选参数 delimiters 为字符串,限定候选分隔符集合。

原文档强调其判定策略:用所有合理的参数组合去试解析样本,选择把样本切分为"字段数最一致的行"的组合,因此返回的方言与 reader 实际解析结果一致。若没有任何组合匹配(尤其是单列无分隔符的情形),抛出 csv.Error。若多个组合表现同样好(例如 ,; 都能一致地切分每一行),则按 Sniffer.preferred 列出的顺序取优,与各分隔符出现次数无关。

Lib/csv.py 中该算法有更细致的工程化实现,可作为源码级佐证:

  • 先调用 _parses_as_single_column 排除整列加引号的"无分隔符"样本;
  • 对候选分隔符、引号字符("/')与转义字符(\)做笛卡尔积分组,惰性激活(_split_dormant)直到某引号字符真正出现在行首字段处;
  • 增量式窗口试解析(窗口 2000 字符起步、逐步 4 倍放大),配合 _eliminate_worse 淘汰明显劣势的组合,控制整体重解析开销;
  • _modal_share 计算"众数字段数及其占比"作为一致性得分,辅以"带引号解析成功为直接证据"的加分与 preferred 次序的微调;
  • 最后分别用 _detect_doublequote_detect_skipinitialspace_detect_lineterminator 补全其余参数。

从文档注释的版本变更可看到,sniff 的行为基于"试解析"推断,其结果可能与旧版 Python 不同;并且现在能探测 escapecharlineterminator,请求的 delimiters 也不再限于 ASCII 字符。

has_header(sample)

分析样本(假定为 CSV 格式),若首行看起来像列标题则返回 True。判定基于每列的两类关键判据:

  • 第 2 行及以后包含数值
  • 第 2 行及以后包含字符串,且其中至少一个值的长度与对应列首行(假想表头)长度不同。

首行之后最多采样 21 行,若超过半数的列/行组合满足判据则返回 True。原文档特意加注:这只是粗糙启发式,既可能误报也可能漏报。实现(has_header 方法)会先用 sniff(sample) 建 reader,对前 21 行逐列尝试 complex(...) 类型化、失败则退化为字符串长度,最后"投票"决定首行是否为表头。

preferred 属性

平局时优先采用的分隔符列表,按优先顺序排列且可修改,初始值为 [',', '\t', ';', ' ', ':']。注意 Lib/csv.py 中它是在 Sniffer.__init__ 里创建的实例属性。

Sniffer 官方使用示例(读入前 1024 字节探测后回到文件头正式解析):

with open('example.csv', newline='') as csvfile:
    dialect = csv.Sniffer().sniff(csvfile.read(1024))
    csvfile.seek(0)
    reader = csv.reader(csvfile, dialect)
    # ... process CSV file contents here ...

Lib/test/test_csv.pyTestSniffer 系列用例覆盖了 has_header 判真/判假、正则特殊字符作分隔符、21 行采样边界以及各种方言的嗅探正确性。

引用常量 QUOTE_*:控制引号何时生成与识别

模块定义了六个引用常量(C 层 QuoteStyle 枚举同名),分别作用于 writer 的"加引号时机"与 reader 的"引号解读方式":

常量 writer 行为 reader 行为
QUOTE_ALL 所有字段都加引号 按引号规则解析
QUOTE_MINIMAL 仅当字段含 delimiterquotechar'\r''\n'lineterminator 中字符时才加引号;若 doublequote=False 且设置了 escapechar,则用转义而非加引号处理 quotechar 默认解析
QUOTE_NONNUMERIC 所有非数值字段加引号 所有未加引号字段转换为 float
QUOTE_NONE 永不加引号;输出中出现 delimiterquotecharescapechar'\r''\n'lineterminator 字符时以前置 escapechar 转义;未设 escapechar 时遇到需转义字符会抛 Error;可将 quotechar 设为 None 以禁止对其转义 对引号字符不做任何特殊处理
QUOTE_NOTNULL(3.12 新增) None 外的字段都加引号;None 写为空(不加引号)字符串 把空(未加引号)字段解读为 None,其余同 QUOTE_ALL
QUOTE_STRINGS(3.12 新增) 凡是字符串字段总是加引号;None 写为空字符串 把空字符串解读为 None,其余同 QUOTE_NONNUMERIC

与常量直接对应的 C 实现可逐一在 Modules/_csv.c 中找到:writer 侧 csv_writerow_lock_heldswitch (dialect->quoting) 里,QUOTE_NONNUMERIC!PyNumber_Check(field) 判断是否加引号,QUOTE_ALL 恒为 1,QUOTE_STRINGSPyUnicode_Check(field)QUOTE_NOTNULLfield != Py_None;reader 侧 parse_save_field 中,QUOTE_NOTNULL/QUOTE_STRINGS 模式下把空(未加引号)字段替换为 Py_None,而 QUOTE_NONNUMERIC/QUOTE_STRINGS 模式下对非空未加引号字段调用 PyNumber_Float 转成 float。此外 reader 状态机通过 unquoted_field 布尔标记区分字段是否被引号包裹。

需要注意:某些数值类型(如 boolFractionIntEnum)的字符串表示无法转回 float,因而不能在 QUOTE_NONNUMERIC/QUOTE_STRINGS 读取模式下使用,这是原文档明确提醒的陷阱。

模块还定义一个异常:csv.Error,任何函数检测到错误时都会抛出它。

方言与格式化参数(Dialects and Formatting Parameters)

为简化输入输出记录格式的指定,相关格式化参数被归组为"方言"。方言是 Dialect 的子类;创建 reader/writer 时既可用字符串或 Dialect 子类作为 dialect 参数,也可额外或单独传入与下列属性同名的格式化参数来覆盖方言。

属性 默认值 含义与约束
delimiter ',' 单字符字段分隔符
doublequote True 字段内出现 quotechar 时的处理:True 将其双写;Falseescapechar 作前缀。输出时doublequote=False 且未设 escapechar,字段内发现 quotechar 会抛 Error
escapechar None(禁用转义) writer 用它转义需转义字符:QUOTE_NONE 下转义 delimiterquotechar'\r''\n'lineterminator 字符;doublequote=False 时转义 quotechar;还会转义它自身。读取时它使后续字符失去特殊含义。3.10 起 escapechar 本身也会被转义(此前会丢失);3.11 起不允许为空字符串
lineterminator '\r\n' writer 输出行的终止串。注意:reader 硬编码只认 '\r''\n' 为行尾,忽略此参数(未来可能改变)
quotechar '"' 单字符引用符,包裹含 delimiterquotechar、换行字符的字段。可设为 None 以便在 QUOTE_NONE 下不转义 '"'。3.11 起不允许为空字符串
quoting 默认 QUOTE_MINIMAL(若 quotecharNone),否则 QUOTE_NONE 控制 writer 何时生成引号、reader 何时识别引号,取值为上节的 QUOTE_* 常量
skipinitialspace False True 时忽略紧随 delimiter 之后的空格。结合 delimiter=' 'skipinitialspace=True 使用时不允许未加引号的空字段
strict False True 时对坏 CSV 输入抛 Error

此外,方言支持 copy.replaceDialect.__replace__),返回一个替换了指定格式化参数的新方言副本,见 Lib/csv.py 中的实现。

Reader 与 Writer 对象的公开接口

Reader 对象

reader() 返回的对象与 DictReader 实例共享以下公开成员:

  • csvreader.__next__():返回下一行——来自 reader() 则为 list,来自 DictReader 则为 dict,均按当前方言解析。通常用 next(reader) 调用;
  • csvreader.dialect:解析器正在使用的方言(只读描述);
  • csvreader.line_num:从源迭代器读取的行数。注意不等于返回的记录数,因为一条记录可能跨多行(例如带引号字段内嵌换行)。

DictReader 额外提供 fieldnames 属性:若构造时未传,则在首次访问或读取首条记录时初始化(见 DictReader.fieldnames 的惰性属性实现)。

Writer 对象

writer() 返回的对象与 DictWriter 实例共享以下公开成员,row 必须是可迭代对象:

  • csvwriter.writerow(row, /):把 row 按当前方言格式化后写入文件对象,返回底层 write 方法的返回值;3.5 起支持任意可迭代对象;
  • csvwriter.writerows(rows, /):把 rowsrow 的可迭代对象)全部写出;
  • csvwriter.dialect:writer 使用的方言(只读描述)。

DictWriter 额外提供 writeheader()(3.2 新增,3.8 起返回内部 writerow 调用的返回值),按构造时的 fieldnames 写出表头行,其内部实现是把字段名做一次 dict(zip(fieldnames, fieldnames)) 自映射后交给 writerow

writer 侧还有一个值得注意的底层行为:文档提示复数会被写成带括号的形式(如 (1+2j)),这可能给读取 CSV 的其他程序带来兼容问题。此外 C 层在写出空字段记录时若采用 QUOTE_NONE(或 QUOTE_STRINGS/QUOTE_NOTNULL 下的 None 字段)会抛出 "single empty field record must be quoted" 错误,因为这种记录无法被唯一解读。

综合示例:读取、写入、异常捕获与字符串解析

原文档的 Examples 一节给出了 5 类可直接运行的实战片段,本节逐一整理并补充说明。

最简读取:

import csv
with open('some.csv', newline='') as f:
    reader = csv.reader(f)
    for row in reader:
        print(row)

读取替代格式(如 /etc/passwd 这类冒号分隔且无引号语义的文件):

import csv
with open('passwd', newline='') as f:
    reader = csv.reader(f, delimiter=':', quoting=csv.QUOTE_NONE)
    for row in reader:
        print(row)

最简写入:

import csv
with open('some.csv', 'w', newline='') as f:
    writer = csv.writer(f)
    writer.writerows(someiterable)

指定非系统默认编码读写:由于 csv 读写的是文本层,编码由 open() 决定。用 open 读文件时默认按系统默认编码解码(见 locale.getencoding()),如需其他编码请显式传 encoding 参数:

import csv
with open('some.csv', newline='', encoding='utf-8') as f:
    reader = csv.reader(f)
    for row in reader:
        print(row)

写入侧同理,只要在打开输出文件时指定 encoding 即可。

注册并使用新方言(把 /etc/passwd 风格固化成语名):

import csv
csv.register_dialect('unixpwd', delimiter=':', quoting=csv.QUOTE_NONE)
with open('passwd', newline='') as f:
    reader = csv.reader(f, 'unixpwd')

进阶读取——捕获并报告错误(利用 reader.line_num 定位出错行):

import csv, sys
filename = 'some.csv'
with open(filename, newline='') as f:
    reader = csv.reader(f)
    try:
        for row in reader:
            print(row)
    except csv.Error as e:
        sys.exit(f'file {filename}, line {reader.line_num}: {e}')

解析内存字符串:模块虽不直接支持解析字符串,但由于 reader 接受"字符串的可迭代对象",一行即可实现:

import csv
for row in csv.reader(['one,two,three']):
    print(row)

三个易错点小结

综合文档与源码,使用 csv 模块时最容易踩的坑集中在三处:

  1. 必须用 newline='' 打开文件:否则引号字段内嵌换行解析错误,\r\n 平台写入时会多出 \r。由于模块自己做通用换行处理,总是显式传 newline='' 是安全做法;
  2. 读取端忽略 lineterminator:reader 硬编码识别 '\r'/'\n' 为行尾,lineterminator 只影响 writer 输出,二者语义不对称;
  3. 引号与转义的组合限制doublequote=False 且无 escapechar 时字段内出现 quotechar 会抛错;QUOTE_NONE 下遇到需转义字符而未设 escapechar 同样抛错;quotechar/escapechar 在 3.11 起不允许为空字符串;bool/Fraction/IntEnum 不适用于 QUOTE_NONNUMERIC/QUOTE_STRINGS 的读取模式。

如果想深入验证文中所述的各类行为,可以研读模块级回归测试 Lib/test/test_csv.py(含 Sniffer、field_size_limit、各 QUOTE 常量组合的大量用例)以及 C 加速层源码 Modules/_csv.c,两者是官方文档之外最可靠的"事实来源"。

登录后查看全文
热门项目推荐
相关项目推荐