Python 读写大 CSV 文件实战:csv 模块的引号转义、编码 BOM 与流式处理不爆内存
处理 CSV 看起来是 Python 里最没技术含量的活:open一下按逗号split不就完了?直到你的 CSV 里某个字段本身带逗号(比如地址「北京市, 朝阳区」),或者带换行、带引号,split(',')立刻把一行切成好几段,数据全乱。再或者用 Excel 打开你生成的 CSV,中文全是乱码;又或者读一个 2GB 的文件,一个read()直接把内存吃爆。
这些坑标准库csv模块基本都替你处理了,但你得知道怎么用对。这篇把 CSV 读写里最高频的三类问题——引号转义、编码/BOM、大文件流式处理——一次讲透。
别用 split(‘,’) 手撕 CSV
先看为什么手写解析必翻车。假设有这么一行数据,地址字段里带了逗号:
1,张三,"北京市, 朝阳区",28按 CSV 规范,带逗号的字段要用双引号包起来。你要是line.split(','):
line='1,张三,"北京市, 朝阳区",28'print(line.split(','))# ['1', '张三', '"北京市', ' 朝阳区"', '28'] ← 5 段!地址被切成两半,引号还留着彻底错了。而csv模块知道引号规则:
importcsv,io line='1,张三,"北京市, 朝阳区",28'reader=csv.reader(io.StringIO(line))print(next(reader))# ['1', '张三', '北京市, 朝阳区', '28'] ← 正确,4 段,引号也去掉了CSV 的转义规则比想象中复杂:字段里有逗号、换行、双引号时要用双引号包裹,字段内的双引号要写成两个双引号""。这些规则手写迟早出错,csv模块全处理好了。结论:永远别用字符串切割解析 CSV,用csv模块。
读:用 DictReader 按列名取,别数下标
基础读法是csv.reader,返回每行的列表,靠下标取值:
importcsvwithopen('users.csv',newline='',encoding='utf-8')asf:reader=csv.reader(f)header=next(reader)# 第一行是表头forrowinreader:print(row[1],row[2])# 靠下标 1、2 取,可读性差、易错下标法有个大问题:列一多、或者上游调整了列顺序,row[2]到底是啥全靠数,极易错位。用DictReader按列名取:
withopen('users.csv',newline='',encoding='utf-8')asf:reader=csv.DictReader(f)# 自动把第一行当表头forrowinreader:print(row['name'],row['city'])# 按列名取,清晰且抗列顺序变化注意那个newline=''——这是最容易漏又最坑的参数。打开 CSV 文件给 csv 模块用时,必须传newline=''。因为 csv 模块要自己处理行内的\r\n(字段里可能包含换行),如果不传,Python 的通用换行转换会和 csv 的处理打架,在 Windows 上会每行之间多出一个空行,或者字段内换行被�is错误处理。记死:csv 读写,open一律带newline=''。
写:字段带逗号/换行,交给 writer 自动加引号
写 CSV 同理,别自己拼字符串。csv.writer会自动判断哪些字段需要加引号:
importcsv rows=[['id','name','address'],[1,'张三','北京市, 朝阳区'],# 带逗号[2,'李四','含"引号"的名字'],# 带引号[3,'王五','第一行\n第二行'],# 带换行]withopen('out.csv','w',newline='',encoding='utf-8')asf:writer=csv.writer(f)writer.writerows(rows)生成的文件里,带特殊字符的字段被自动正确转义:
id,name,address 1,张三,"北京市, 朝阳区" 2,李四,"含""引号""的名字" 3,王五,"第一行 第二行"注意第 2 行的引号被转义成了"",第 3 行的字段因为含换行被整体用引号包住——这些都是csv.writer自动做的,手写绝对会漏。
写字典数据用DictWriter,还能顺手用writeheader()写表头:
withopen('out.csv','w',newline='',encoding='utf-8')asf:writer=csv.DictWriter(f,fieldnames=['id','name','address'])writer.writeheader()writer.writerow({'id':1,'name':'张三','address':'北京市, 朝阳区'})DictWriter有个好处:如果 dict 里的 key 和fieldnames对不上(多了个 key),会直接抛ValueError,帮你抓住数据里的脏字段,而不是静默漏写。
编码坑:Excel 打开中文乱码,加 BOM
你用utf-8写的 CSV,自己 Python 读没问题,但同事用Excel(尤其是 Windows 版)双击打开,中文全是乱码。这是因为 Excel 在没有明确编码标记时,默认按系统本地编码(简中环境是 GBK)去猜,把 UTF-8 的中文认成了乱码。
解法是用utf-8-sig编码写——它会在文件开头写一个 BOM(字节顺序标记),Excel 看到 BOM 就知道「这是 UTF-8」,正确显示:
# 给 Excel 用的 CSV,用 utf-8-sig 加 BOMwithopen('for_excel.csv','w',newline='',encoding='utf-8-sig')asf:writer=csv.writer(f)writer.writerow(['姓名','城市'])writer.writerow(['张三','北京'])反过来,读一个带 BOM 的文件时,也用utf-8-sig,它会自动吃掉 BOM。如果你用普通utf-8读带 BOM 的文件,第一个字段名会莫名多个前缀:
# 用普通 utf-8 读带 BOM 的文件:表头第一个 key 会带 withopen('for_excel.csv',encoding='utf-8')asf:reader=csv.DictReader(f)print(reader.fieldnames)# ['姓名', '城市'] ← 第一个 key 脏了!# 正确:用 utf-8-sig 读,自动去 BOMwithopen('for_excel.csv',encoding='utf-8-sig')asf:reader=csv.DictReader(f)print(reader.fieldnames)# ['姓名', '城市']这个前缀极其隐蔽:row['姓名']会 KeyError,但打印出来看着一模一样,排查半天。规则:和 Excel 打交道就用utf-8-sig,读写都用它最省心。
大文件:逐行流式处理,别一次读进内存
处理几百 MB 甚至几 GB 的 CSV,新手常见错误是f.read()或list(reader)把整个文件读进内存,直接 OOM。
好消息是csv.reader本身就是惰性迭代器——你 for 循环它时,它一次只读一行,内存占用和文件大小无关:
importcsv# 正确:逐行迭代,内存恒定,几 GB 也不怕defcount_by_city(path):fromcollectionsimportCounter counter=Counter()withopen(path,newline='',encoding='utf-8')asf:reader=csv.DictReader(f)forrowinreader:# 一次一行,不会把整个文件读进内存counter[row['city']]+=1returncounter反例(别这么写):
withopen('huge.csv',newline='',encoding='utf-8')asf:rows=list(csv.DictReader(f))# ← 整个文件塞进内存,大文件直接 OOMforrowinrows:...只要你不主动list()它、不 append 到一个大列表里,for 循环逐行处理就是流式的。想边读边写(比如清洗大文件),同时开读和写两个流,处理一行写一行:
defclean_csv(src,dst):withopen(src,newline='',encoding='utf-8')asfin,\open(dst,'w',newline='',encoding='utf-8-sig')asfout:reader=csv.DictReader(fin)writer=csv.DictWriter(fout,fieldnames=reader.fieldnames)writer.writeheader()forrowinreader:row['name']=row['name'].strip()# 逐行清洗ifrow['city']:# 过滤掉没城市的writer.writerow(row)# 全程内存只驻留一行,处理 10GB 文件也稳一个隐藏坑:超大字段报 field larger than field limit
如果某个字段特别大(比如一整段 JSON、base64 内容塞进一个单元格),读的时候可能报:
_csv.Error: field larger than field limit (131072)csv 模块默认单字段上限约 128KB。确实有超大字段就调大限制:
importcsv,sys csv.field_size_limit(sys.maxsize)# 放开单字段大小限制但先想清楚:字段真有必要这么大吗?通常这种超大字段是数据设计有问题(该拆表或存文件路径),放开限制是权宜之计。
小结
- 永远用
csv模块,别split(','):带逗号、换行、引号的字段,手写切割必翻车,csv按规范自动处理转义。 - 读用
DictReader按列名取(抗列顺序变化),写用DictWriter+writeheader();open时一律带newline='',否则 Windows 上多空行、字段内换行出错。 - 编码:和 Excel 打交道用
utf-8-sig(写加 BOM 防乱码,读自动去 BOM);用普通utf-8读带 BOM 文件,第一个列名会藏个导致 KeyError。 - 大文件:
csv.reader/DictReader本身是惰性迭代器,for 循环逐行处理内存恒定;别list(reader)或f.read()一次读进内存;边读边写同时开两个流。 - 超大字段报
field larger than field limit时用csv.field_size_limit(sys.maxsize)放开,但先反思字段该不该这么大。
一句话记忆:CSV 三件套记牢——解析交给 csv 模块、open带newline=''、跟 Excel 打交道用utf-8-sig;大文件靠迭代器天然流式,别手贱list()它。