Python 读写大 CSV 文件实战:csv 模块的引号转义、编码 BOM 与流式处理不爆内存

📅 发布时间:2026/9/6 13:09:12
Python 读写大 CSV 文件实战:csv 模块的引号转义、编码 BOM 与流式处理不爆内存 Python 读写大 CSV 文件实战:csv 模块的引号转义、编码 BOM 与流式处理不爆内存处理 CSV 看起来是 Python 里最没技术含量的活:open一下按逗号split不就完了?直到你的 CSV 里某个字段本身带逗号(比如地址「北京市, 朝阳区」),或者带换行、带引号,split(,)立刻把一行切成好几段,数据全乱。再或者用 Excel 打开你生成的 CSV,中文全是乱码;又或者读一个 2GB 的文件,一个read()直接把内存吃爆。这些坑标准库csv模块基本都替你处理了,但你得知道怎么用对。这篇把 CSV 读写里最高频的三类问题——引号转义、编码/BOM、大文件流式处理——一次讲透。别用 split(‘,’) 手撕 CSV先看为什么手写解析必翻车。假设有这么一行数据,地址字段里带了逗号:1,张三,北京市, 朝阳区,28按 CSV 规范,带逗号的字段要用双引号包起来。你要是line.split(,):line1,张三,北京市, 朝阳区,28print(line.split(,))# [1, 张三, 北京市, 朝阳区, 28] ← 5 段!地址被切成两半,引号还留着彻底错了。而csv模块知道引号规则:importcsv,io line1,张三,北京市, 朝阳区,28readercsv.reader(io.StringIO(line))print(next(reader))# [1, 张三, 北京市, 朝阳区, 28] ← 正确,4 段,引号也去掉了CSV 的转义规则比想象中复杂:字段里有逗号、换行、双引号时要用双引号包裹,字段内的双引号要写成两个双引号。这些规则手写迟早出错,csv模块全处理好了。结论:永远别用字符串切割解析 CSV,用csv模块。读:用 DictReader 按列名取,别数下标基础读法是csv.reader,返回每行的列表,靠下标取值:importcsvwithopen(users.csv,newline,encodingutf-8)asf:readercsv.reader(f)headernext(reader)# 第一行是表头forrowinreader:print(row[1],row[2])# 靠下标 1、2 取,可读性差、易错下标法有个大问题:列一多、或者上游调整了列顺序,row[2]到底是啥全靠数,极易错位。用DictReader按列名取:withopen(users.csv,newline,encodingutf-8)asf:readercsv.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,encodingutf-8)asf:writercsv.writer(f)writer.writerows(rows)生成的文件里,带特殊字符的字段被自动正确转义:id,name,address 1,张三,北京市, 朝阳区 2,李四,含引号的名字 3,王五,第一行 第二行注意第 2 行的引号被转义成了,第 3 行的字段因为含换行被整体用引号包住——这些都是csv.writer自动做的,手写绝对会漏。写字典数据用DictWriter,还能顺手用writeheader()写表头:withopen(out.csv,w,newline,encodingutf-8)asf:writercsv.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,encodingutf-8-sig)asf:writercsv.writer(f)writer.writerow([姓名,城市])writer.writerow([张三,北京])反过来,读一个带 BOM 的文件时,也用utf-8-sig,它会自动吃掉 BOM。如果你用普通utf-8读带 BOM 的文件,第一个字段名会莫名多个前缀:# 用普通 utf-8 读带 BOM 的文件:表头第一个 key 会带 withopen(for_excel.csv,encodingutf-8)asf:readercsv.DictReader(f)print(reader.fieldnames)# [姓名, 城市] ← 第一个 key 脏了!# 正确:用 utf-8-sig 读,自动去 BOMwithopen(for_excel.csv,encodingutf-8-sig)asf:readercsv.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 counterCounter()withopen(path,newline,encodingutf-8)asf:readercsv.DictReader(f)forrowinreader:# 一次一行,不会把整个文件读进内存counter[row[city]]1returncounter反例(别这么写):withopen(huge.csv,newline,encodingutf-8)asf:rowslist(csv.DictReader(f))# ← 整个文件塞进内存,大文件直接 OOMforrowinrows:...只要你不主动list()它、不 append 到一个大列表里,for 循环逐行处理就是流式的。想边读边写(比如清洗大文件),同时开读和写两个流,处理一行写一行:defclean_csv(src,dst):withopen(src,newline,encodingutf-8)asfin,\open(dst,w,newline,encodingutf-8-sig)asfout:readercsv.DictReader(fin)writercsv.DictWriter(fout,fieldnamesreader.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按列名取(抗列顺序变化),写用DictWriterwriteheader();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()它。