本文要点
csv 模块提供了 Sniffer 类,用来推断 CSV 文件的格式
Sniffer 推断出方言后,可以把它传给 csv.reader
如果你已经知道文件用什么分隔,可以直接在 reader 里指定 delimiter,不必先推断

本文目录
先看现象:分隔符不同会让读取结果变成什么样
如果你打开一个 CSV 文件,发现整行内容挤在一列里,或者每行被拆成了数量不对的字段,先别急着改数据。这通常说明文件实际使用的分隔符和你读取时假定的分隔符不一致。
CSV 格式在 RFC 4180 之前已经使用多年,但一直没有严格统一的标准,所以不同应用生成和消费的 CSV 数据常存在细微差异。这些差异包括分隔符和引号字符的不同。csv 模块的设计目标之一,就是让你不必了解某个程序具体用了什么格式,也能读写它的数据。
本文只处理一个具体问题:当你拿到一个分隔符不是逗号的自建 CSV 文件时,怎么识别它用了什么分隔符,以及怎么检查读取结果对不对。下面所有文件都是练习用的自建示例,不涉及真实业务数据。
开始前请先复制一份文件作为练习副本,不要拿重要数据直接做实验。
用 Sniffer 从样本里推断分隔符
csv 模块提供了 Sniffer 类,用来推断 CSV 文件的格式。它的 sniff 方法分析给定的样本,返回一个反映所发现参数的 Dialect 子类。
下面是一个自建示例。假设你有一个用分号分隔的练习文件 exercise/sample_semicolon.csv,内容如下:
name;age
Alice;30
Bob;25用 Sniffer 推断它的方言:
import csv
with open('exercise/sample_semicolon.csv', newline='') as f:
sample = f.read(1024)
dialect = csv.Sniffer().sniff(sample)
print(dialect.delimiter)sniff 的 sample 参数是一段文本样本。如果传入可选的 delimiters 参数,它会被解释为一个包含可能有效分隔符字符的字符串。
如果多个分隔符对样本的拆分效果一样好——比如逗号和分号都能一致地拆分每一行——Sniffer 会优先选择 preferred 属性中列出的分隔符,按该顺序,不管每个分隔符出现了多少次。preferred 的初始值是逗号、制表符、分号、空格、冒号,这个列表可以修改。
这里要提醒一点:sniff 是基于样本的推断,不是对文件格式的权威判定。样本太短或内容太规整时,推断结果可能和实际不符。所以推断出分隔符后,还要做下一步的读取检查。
推断出方言后,怎么用它读取并检查结果
Sniffer 推断出方言后,可以把它传给 csv.reader。注意读取前要把文件位置移回开头,因为 sniff 已经读走了一段样本。
import csv
with open('exercise/sample_semicolon.csv', newline='') as f:
dialect = csv.Sniffer().sniff(f.read(1024))
f.seek(0)
reader = csv.reader(f, dialect)
for row in reader:
print(row)打开文件时要用 newline=''。如果不指定,引号字段里嵌入的换行不会被正确解释,在写入时使用 \r\n 行尾的平台上还会多出一个 \r。csv 模块自己做换行处理,所以指定 newline='' 总是安全的。
读取结果里,每一行返回的是一个字符串列表,不会自动做数据类型转换。所以 age 列读出来是字符串 '30',不是数字 30。
检查读取结果是否正确的办法:看每行的字段数量是否一致,看表头是否单独成行,看原本应该分开的值有没有被拆开或粘在一起。如果字段数量对不上,说明分隔符可能推断错了。
不想用 Sniffer 时,怎么手动指定分隔符
如果你已经知道文件用什么分隔,可以直接在 reader 里指定 delimiter,不必先推断。
import csv
with open('exercise/sample_semicolon.csv', newline='') as f:
reader = csv.reader(f, delimiter=';')
for row in reader:
print(row)delimiter 是一个单字符字符串,用来分隔字段,默认是逗号。除了 delimiter,还可以用其他 fmtparams 关键字参数覆盖当前方言里的单个格式参数。
手动指定的好处是结果可预期,不依赖样本推断。代价是你得先知道文件实际用了什么分隔符。如果你不确定,可以先按上一节用 Sniffer 推断,再把推断结果和手动指定的结果对照。
这里只演示 delimiter 这一个参数。其他格式参数不在本文范围内,需要时请查阅 csv 模块文档的 Dialects and formatting parameters 一节。
用 list_dialects 查看已注册的方言名
除了逐个指定参数,csv 模块还允许用方言名来指定一组格式参数。csv.list_dialects() 返回所有已注册方言的名称。
import csv
print(csv.list_dialects())模块内置注册了几个方言。excel 类定义了 Excel 生成的 CSV 文件的常见属性,注册名是 'excel'。excel_tab 类定义了 Excel 生成的制表符分隔文件的常见属性,注册名是 'excel-tab'。unix_dialect 类定义了 UNIX 系统上生成的 CSV 文件的常见属性,即使用 \n 作为行终止符并给所有字段加引号,注册名是 'unix'。
reader 和 writer 的 dialect 参数可以是 Dialect 子类的实例,也可以是 list_dialects() 返回的字符串之一。所以如果你的文件是制表符分隔的,可以这样读:
import csv
with open('exercise/sample_tab.csv', newline='') as f:
reader = csv.reader(f, dialect='excel-tab')
for row in reader:
print(row)用方言名的前提是你知道文件对应哪个已注册方言。如果文件来自某个特定程序但不在已注册列表里,还是回到 Sniffer 或手动指定参数。
读取报错时怎么定位到具体行
如果文件内容不符合 CSV 格式,读取时可能抛出 csv.Error。reader 对象的 line_num 属性记录从源迭代器读取的行数,注意它和返回的记录数不是一回事,因为一条记录可能跨多行。
下面是一个自建示例,演示怎么在报错时报告文件名和行号:
import csv, sys
filename = 'exercise/sample_bad.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}')这段代码只读取自建练习文件,不修改任何内容。报错信息里的行号可以帮你回到文件里查看那一行,判断是分隔符问题、引号问题还是其他格式问题。
需要说明的是,strict 参数默认为 False,即不对坏输入抛错。如果你希望遇到坏输入就报错,可以在创建 reader 时把 strict 设为 True。本文不展开 strict 的用法,只说明它的默认行为。
识别方言时的限制和容易踩的坑
第一,Sniffer 的推断是启发式的,不是权威判定。样本太短、内容太规整或分隔符出现次数太少时,推断可能出错。所以推断之后一定要用读取结果做检查。
第二,多个分隔符效果相同时,Sniffer 按 preferred 列表的顺序选择,而不是按出现次数。这意味着如果逗号和分号都能拆分样本,它会选逗号。如果你知道文件实际用分号,最好手动指定 delimiter,不要依赖推断。
第三,reader 被硬编码为识别 \r 或 \n 作为行尾,并忽略 lineterminator。所以 lineterminator 参数对读取没有影响,它只用于 writer 生成输出。
第四,本文只涉及识别方言和读取检查这些基础操作。注册自定义方言、修改 preferred 列表、处理引号字符等更细的配置不在本文范围内。如果你的文件涉及这些情况,请查阅 csv 模块文档的 Dialects and formatting parameters 一节。
最后提醒:所有操作都针对自建练习文件,操作前先备份。本文不涉及删除文件或修改系统设置。
观看知识要点视频 18 秒
18秒无旁白要点导览,文字来自已审核正文;完整步骤、条件及参考资料见下方。
参考资料
以下原始资料提供本文依据;请结合日期、适用条件和产品说明查阅。
请结合原始资料的适用条件阅读。
返回表格与数据 →