CSV 看起来是 Python 里最简单的文件格式,但一旦遇到真实数据就会暴露问题:字段里本身带逗号、带换行、Excel 打开中文乱码、几 GB 文件一次读进内存直接 OOM。标准库 csv 模块已经把这些规则都处理好了,真正容易出错的是参数和用法。下面按解析、读取、写入、编码和大文件处理几条线,把常见坑逐个说清。
一、为什么不能用 split(',') 解析 CSV
先看一行典型数据:地址字段里带了逗号。
- line = '1,张三,"北京市, 朝阳区",28'
- print(line.split(','))
- # ['1', '张三', '"北京市', ' 朝阳区"', '28'] ← 5 段,地址被切成两半
复制代码
按 CSV 规范,字段内包含逗号、换行或双引号时,整个字段要用双引号包裹,字段内部的双引号要写成两个双引号。手写切割无法处理这套规则,而 csv 模块知道怎么解析:
- import csv, io
- line = '1,张三,"北京市, 朝阳区",28'
- reader = csv.reader(io.StringIO(line))
- print(next(reader))
- # ['1', '张三', '北京市, 朝阳区', '28'] ← 正确切成 4 段,引号也去掉了
复制代码
结论很直接:只要处理 CSV,就用 csv 模块,别用字符串 split。
二、读:用 DictReader 按列名取值,别数下标
csv.reader 返回的是每行的列表,靠下标取值:
- import csv
- with open('users.csv', newline='', encoding='utf-8') as f:
- reader = csv.reader(f)
- header = next(reader) # 第一行是表头
- for row in reader:
- print(row[1], row[2]) # 靠下标取,可读性差、容易错位
复制代码
列一多,或者上游调整了列顺序,row[2] 到底对应哪一列全靠数,极易出错。更稳的写法是用 DictReader,第一行自动当表头,之后按列名取值:
- with open('users.csv', newline='', encoding='utf-8') as f:
- reader = csv.DictReader(f)
- for row in reader:
- print(row['name'], row['city']) # 按列名取,抗列顺序变化
复制代码
这里最容易漏、也最坑的是 open 里的 newline=''。csv 模块需要自己处理行内的 \r\n,因为字段内容本身可能包含换行;如果不传 newline='',Python 的通用换行转换会和 csv 的处理逻辑冲突,在 Windows 上表现为每行之间多出一个空行,或者字段内换行被错误处理。记死:csv 读写时,open 一律带 newline=''。
三、写:特殊字段交给 writer 自动加引号
写 CSV 同样不要自己拼字符串。csv.writer 会自动判断哪些字段需要加引号:
- import csv
- rows = [
- ['id', 'name', 'address'],
- [1, '张三', '北京市, 朝阳区'], # 带逗号
- [2, '李四', '含"引号"的名字'], # 带双引号
- [3, '王五', '第一行\n第二行'], # 带换行
- ]
- with open('out.csv', 'w', newline='', encoding='utf-8') as f:
- writer = csv.writer(f)
- writer.writerows(rows)
复制代码
生成的文件里,带逗号的字段被整体加引号,字段内的双引号被转义成两个双引号,带换行的字段也被引号包住。这些都是 csv.writer 自动完成的,手写拼接基本必然漏掉某一种情况。
如果数据是字典,用 DictWriter,还可以用 writeheader() 顺手写表头:
- with open('out.csv', 'w', newline='', encoding='utf-8') as f:
- writer = csv.DictWriter(f, fieldnames=['id', 'name', 'address'])
- writer.writeheader()
- writer.writerow({'id': 1, 'name': '张三', 'address': '北京市, 朝阳区'})
复制代码
DictWriter 还有一个好处:如果 dict 里出现了 fieldnames 之外的 key,会直接抛 ValueError,帮你抓住脏字段,而不是静默漏写。
四、编码与 BOM:和 Excel 打交道用 utf-8-sig
用 utf-8 写出的 CSV,Python 自己读没问题,但同事用 Windows 版 Excel 双击打开时中文可能全是乱码。原因是 Excel 在没有明确编码标记时会按系统本地编码(简体中文环境通常是 GBK)去猜,把 UTF-8 中文认成了乱码。
解法是写文件时用 utf-8-sig,它会在文件开头写入 BOM,Excel 看到 BOM 就知道这是 UTF-8:
- with open('for_excel.csv', 'w', newline='', encoding='utf-8-sig') as f:
- writer = csv.writer(f)
- writer.writerow(['姓名', '城市'])
- writer.writerow(['张三', '北京'])
复制代码
反过来,读带 BOM 的文件时也用 utf-8-sig,它会自动吃掉 BOM。如果用普通 utf-8 去读带 BOM 的文件,第一个字段名前面会多出一个不可见的 BOM 前缀(\ufeff):
- with open('for_excel.csv', encoding='utf-8') as f:
- reader = csv.DictReader(f)
- print(reader.fieldnames) # ['\ufeff姓名', '城市'] ← 第一个 key 脏了
- with open('for_excel.csv', encoding='utf-8-sig') as f:
- reader = csv.DictReader(f)
- print(reader.fieldnames) # ['姓名', '城市']
复制代码
这个前缀非常隐蔽:row['姓名'] 会直接 KeyError,但把两个字符串打印出来看着一模一样,很容易排查半天。规则很简单:和 Excel 交互的文件,读写都用 utf-8-sig 最省心。
五、大文件:靠迭代器流式处理,别一次读进内存
处理几百 MB 甚至几 GB 的 CSV,新手常犯的错误是 f.read() 或 list(reader) 把整个文件读进内存,直接 OOM。其实 csv.reader 本身就是惰性迭代器,for 循环时一次只读一行,内存占用和文件大小无关:
- import csv
- from collections import Counter
- def count_by_city(path):
- counter = Counter()
- with open(path, newline='', encoding='utf-8') as f:
- reader = csv.DictReader(f)
- for row in reader: # 一次一行,不会整体读入内存
- counter[row['city']] += 1
- return counter
复制代码
下面这种写法就是反例:
- with open('huge.csv', newline='', encoding='utf-8') as f:
- rows = list(csv.DictReader(f)) # 整个文件塞进内存,大文件直接 OOM
- for row in rows:
- pass
复制代码
只要不主动 list() 它、不往一个大列表里 append,逐行 for 循环就是流式处理。如果要边读边写,比如清洗大文件,同时打开读流和写流,处理一行写一行即可:
- def clean_csv(src, dst):
- with open(src, newline='', encoding='utf-8') as fin, \
- open(dst, 'w', newline='', encoding='utf-8-sig') as fout:
- reader = csv.DictReader(fin)
- writer = csv.DictWriter(fout, fieldnames=reader.fieldnames)
- writer.writeheader()
- for row in reader:
- row['name'] = row['name'].strip() # 逐行清洗
- if row['city']: # 过滤掉没有城市的行
- writer.writerow(row)
复制代码
这种模式下内存里始终只驻留一行,处理 10GB 文件也稳。
六、超大字段:field larger than field limit
如果某个字段特别大,比如一整段 JSON 或 base64 内容塞进一个单元格,读取时可能报错:
- _csv.Error: field larger than field limit (131072)
复制代码
csv 模块默认单字段上限约 131072 字节,也就是约 128KB。确实存在超大字段时,可以调大限制:
- import csv, sys
- csv.field_size_limit(sys.maxsize) # 放开单字段大小限制
复制代码
但更值得先想一步:字段真有必要这么大吗?通常出现超大字段说明数据设计有问题,该拆表或者改成存文件路径,放开限制只是权宜之计。
七、小结
把上面的内容压缩成一条记忆线:解析永远交给 csv 模块,别 split(',');读用 DictReader 按列名取,写用 writer 或 DictWriter;open 一律带 newline='';和 Excel 打交道用 utf-8-sig,写时加 BOM 防乱码,读时自动去 BOM;大文件依靠 reader 的惰性迭代逐行处理,别 list() 或 read() 整体读入;遇到 field larger than field limit 再用 csv.field_size_limit(sys.maxsize) 临时放开,同时回头检查字段设计是否合理。 |