处理 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 模块知道引号规则:
import csv, io line = '1,张三,"北京市, 朝阳区",28' reader = csv.reader(io.stringio(line)) print(next(reader)) # ['1', '张三', '北京市, 朝阳区', '28'] ← 正确,4 段,引号也去掉了
csv 的转义规则比想象中复杂:字段里有逗号、换行、双引号时要用双引号包裹,字段内的双引号要写成两个双引号 ""。这些规则手写迟早出错,csv 模块全处理好了。结论:永远别用字符串切割解析 csv,用 csv 模块。
读:用 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]) # 靠下标 1、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']) # 按列名取,清晰且抗列顺序变化
注意那个 newline=''——这是最容易漏又最坑的参数。打开 csv 文件给 csv 模块用时,必须传 newline=''。因为 csv 模块要自己处理行内的 \r\n(字段里可能包含换行),如果不传,python 的通用换行转换会和 csv 的处理打架,在 windows 上会每行之间多出一个空行,或者字段内换行被�is错误处理。记死: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)
生成的文件里,带特殊字符的字段被自动正确转义:
id,name,address 1,张三,"北京市, 朝阳区" 2,李四,"含""引号""的名字" 3,王五,"第一行 第二行"
注意第 2 行的引号被转义成了 "",第 3 行的字段因为含换行被整体用引号包住——这些都是 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 里的 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 加 bom
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 的文件,第一个字段名会莫名多个 前缀:
# 用普通 utf-8 读带 bom 的文件:表头第一个 key 会带
with open('for_excel.csv', encoding='utf-8') as f:
reader = csv.dictreader(f)
print(reader.fieldnames) # ['姓名', '城市'] ← 第一个 key 脏了!
# 正确:用 utf-8-sig 读,自动去 bom
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
# 正确:逐行迭代,内存恒定,几 gb 也不怕
def count_by_city(path):
from collections import counter
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:
...
只要你不主动 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 模块默认单字段上限约 128kb。确实有超大字段就调大限制:
import csv, 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;大文件靠迭代器天然流式,别手jian list() 它。
以上就是python读写大csv文件的实战指南的详细内容,更多关于python读写大csv文件的资料请关注代码网其它相关文章!
发表评论