3. 读取CSV文件:csv.reader逐行读取与分隔符、引号处理
CSV文件读取,说白了就是跟文本格式打交道。Python自带的csv模块,我用了快十年,一直觉得它设计得挺巧妙——既简单,又留足了扩展空间。
咱们先看最基础的操作:用csv.reader逐行读取一个标准CSV文件。你想想看,一个CSV文件本质上就是纯文本,每行是一条记录,每列用逗号隔开。但实际项目里,哪有这么规规矩矩的数据?
3.1 基础读取:csv.reader入门
先来个最简单的例子。假设我们有一个ingredients.csv文件,内容是这样的:
name,quantity,unit,expiry_date
面粉,500,g,2025-06-30
糖,200,g,2025-08-15
鸡蛋,6,个,2025-05-20
黄油,250,g,2025-07-10
用csv.reader读取它,代码非常直接:
import csv
with open('ingredients.csv', 'r', encoding='utf-8') as f:
reader = csv.reader(f)
for row in reader:
print(row)
输出结果:
['name', 'quantity', 'unit', 'expiry_date']
['面粉', '500', 'g', '2025-06-30']
['糖', '200', 'g', '2025-08-15']
['鸡蛋', '6', '个', '2025-05-20']
['黄油', '250', 'g', '2025-07-10']
嗯,这里要注意:csv.reader返回的是一个迭代器,每次迭代返回一个列表。每一行都被拆成了字段列表,所有字段默认都是字符串类型。数字500、200并不会自动转成整数——这个后面做数据校验时会处理。
next(reader)就能拿到表头行,非常方便。
3.2 处理不同分隔符:delimiter参数
实际工作中,CSV文件的分隔符可不一定都是逗号。我在项目中遇到过好几次,客户发来的数据文件用的是制表符(TSV格式),或者分号、竖线。这时候delimiter参数就派上用场了。
| 分隔符类型 | 常见场景 | delimiter参数值 |
|---|---|---|
逗号 , |
标准CSV文件 | delimiter=',' |
制表符 \t |
TSV文件、Excel导出 | delimiter='\t' |
分号 ; |
欧洲地区CSV(Excel默认) | delimiter=';' |
竖线 | |
日志文件、特殊格式 | delimiter='|' |
空格 |
简单文本数据 | delimiter=' ' |
举个例子,如果文件是用制表符分隔的:
import csv
with open('ingredients.tsv', 'r', encoding='utf-8') as f:
reader = csv.reader(f, delimiter='\t')
for row in reader:
print(row)
3.3 引号处理:quoting参数
CSV文件里,如果字段本身包含逗号、换行符或者引号,就需要用引号把整个字段包起来。比如:
name,description,quantity
"面粉,高筋","适合做面包,口感筋道",500
糖,"用于调味",200
这里csv.reader默认就能正确处理引号——它知道引号内的逗号不是分隔符。但有些特殊情况,需要手动控制引号处理行为。
quoting参数有4种模式:
| 常量 | 值 | 含义 |
|---|---|---|
csv.QUOTE_ALL |
1 | 所有字段都加引号 |
csv.QUOTE_MINIMAL |
0 | 只在必要时加引号(默认) |
csv.QUOTE_NONNUMERIC |
2 | 非数字字段加引号 |
csv.QUOTE_NONE |
3 | 不加引号,遇到引号当作普通字符 |
我个人最常用的是QUOTE_MINIMAL,这是默认行为,基本够用。但有一种情况要特别注意:
import csv
# 假设文件里字段包含引号本身
# 比如:他说"你好",500
with open('data.csv', 'r', encoding='utf-8') as f:
reader = csv.reader(f, quoting=csv.QUOTE_NONE, escapechar='\\')
for row in reader:
print(row)
当设置QUOTE_NONE时,如果字段里还有引号,必须配合escapechar使用,否则会报错。这个组合我在处理一些不规范的老系统导出数据时用过几次。
csv.reader会自动剥离包裹字段的引号,你拿到的数据是干净的字段值,不包含外层引号。
3.4 知识结构总览
下面这张图,把CSV读取的核心流程和参数关系梳理清楚了:
3.5 综合示例:处理真实配方数据
把上面这些知识点串起来,写一个能处理多种格式的读取函数。我在做配方数据迁移时,就写过类似的工具:
import csv
def read_csv_flexible(filepath, delimiter=None, encoding='utf-8'):
"""
智能读取CSV文件,自动检测分隔符
"""
# 如果没有指定分隔符,尝试自动检测
if delimiter is None:
with open(filepath, 'r', encoding=encoding) as f:
sample = f.read(1024)
# 统计各种分隔符出现次数
comma_count = sample.count(',')
tab_count = sample.count('\t')
semicolon_count = sample.count(';')
# 选择出现最多的分隔符
counts = {
',': comma_count,
'\t': tab_count,
';': semicolon_count
}
delimiter = max(counts, key=counts.get)
print(f"自动检测到分隔符: {repr(delimiter)}")
# 读取文件
with open(filepath, 'r', encoding=encoding) as f:
reader = csv.reader(f, delimiter=delimiter)
headers = next(reader) # 提取表头
data = []
for row_num, row in enumerate(reader, start=2):
# 跳过空行
if not any(row):
continue
data.append(row)
return headers, data
# 使用示例
headers, rows = read_csv_flexible('recipe_data.csv')
print(f"表头: {headers}")
print(f"数据行数: {len(rows)}")
3.6 常见问题与排查思路
读取CSV时,我遇到过最多的问题就这几类:
- 编码问题:文件是GBK编码,用UTF-8读就乱码。解决办法是先用
chardet库检测编码,或者直接指定encoding='gbk'。 - 空行问题:文件末尾或中间有空行,
csv.reader会返回空列表。加个if not row: continue就能跳过。 - 字段数量不一致:某一行少了一列,读取时不会报错,但后续处理会出问题。我习惯在读完后检查每行的字段数是否等于表头数。
- 引号不匹配:某个字段的引号没闭合,会导致
csv.reader把后续多行当成一个字段。这种情况比较棘手,通常需要手动修复源文件。
嗯,CSV读取这块,说白了就是「打开文件、配置参数、逐行解析」三步走。参数配置是核心——delimiter决定怎么切分字段,quoting决定怎么处理引号。这两个参数搞明白了,市面上99%的CSV文件你都能搞定。