查看: 804|回复: 3

Python大CSV读写实战:csv模块、BOM编码与字段上限

[复制链接]
发表于 7 小时前 | 显示全部楼层 |阅读模式
CSV 看起来是 Python 里最简单的文件格式,但一旦遇到真实数据就会暴露问题:字段里本身带逗号、带换行、Excel 打开中文乱码、几 GB 文件一次读进内存直接 OOM。标准库 csv 模块已经把这些规则都处理好了,真正容易出错的是参数和用法。下面按解析、读取、写入、编码和大文件处理几条线,把常见坑逐个说清。

一、为什么不能用 split(',') 解析 CSV

先看一行典型数据:地址字段里带了逗号。
  1. line = '1,张三,"北京市, 朝阳区",28'
  2. print(line.split(','))
  3. # ['1', '张三', '"北京市', ' 朝阳区"', '28']  ← 5 段,地址被切成两半
复制代码

按 CSV 规范,字段内包含逗号、换行或双引号时,整个字段要用双引号包裹,字段内部的双引号要写成两个双引号。手写切割无法处理这套规则,而 csv 模块知道怎么解析:
  1. import csv, io
  2. line = '1,张三,"北京市, 朝阳区",28'
  3. reader = csv.reader(io.StringIO(line))
  4. print(next(reader))
  5. # ['1', '张三', '北京市, 朝阳区', '28']  ← 正确切成 4 段,引号也去掉了
复制代码

结论很直接:只要处理 CSV,就用 csv 模块,别用字符串 split。

二、读:用 DictReader 按列名取值,别数下标

csv.reader 返回的是每行的列表,靠下标取值:
  1. import csv
  2. with open('users.csv', newline='', encoding='utf-8') as f:
  3.     reader = csv.reader(f)
  4.     header = next(reader)  # 第一行是表头
  5.     for row in reader:
  6.         print(row[1], row[2])  # 靠下标取,可读性差、容易错位
复制代码

列一多,或者上游调整了列顺序,row[2] 到底对应哪一列全靠数,极易出错。更稳的写法是用 DictReader,第一行自动当表头,之后按列名取值:
  1. with open('users.csv', newline='', encoding='utf-8') as f:
  2.     reader = csv.DictReader(f)
  3.     for row in reader:
  4.         print(row['name'], row['city'])  # 按列名取,抗列顺序变化
复制代码

这里最容易漏、也最坑的是 open 里的 newline=''。csv 模块需要自己处理行内的 \r\n,因为字段内容本身可能包含换行;如果不传 newline='',Python 的通用换行转换会和 csv 的处理逻辑冲突,在 Windows 上表现为每行之间多出一个空行,或者字段内换行被错误处理。记死:csv 读写时,open 一律带 newline=''。

三、写:特殊字段交给 writer 自动加引号

写 CSV 同样不要自己拼字符串。csv.writer 会自动判断哪些字段需要加引号:
  1. import csv
  2. rows = [
  3.     ['id', 'name', 'address'],
  4.     [1, '张三', '北京市, 朝阳区'],      # 带逗号
  5.     [2, '李四', '含"引号"的名字'],      # 带双引号
  6.     [3, '王五', '第一行\n第二行'],      # 带换行
  7. ]
  8. with open('out.csv', 'w', newline='', encoding='utf-8') as f:
  9.     writer = csv.writer(f)
  10.     writer.writerows(rows)
复制代码

生成的文件里,带逗号的字段被整体加引号,字段内的双引号被转义成两个双引号,带换行的字段也被引号包住。这些都是 csv.writer 自动完成的,手写拼接基本必然漏掉某一种情况。

如果数据是字典,用 DictWriter,还可以用 writeheader() 顺手写表头:
  1. with open('out.csv', 'w', newline='', encoding='utf-8') as f:
  2.     writer = csv.DictWriter(f, fieldnames=['id', 'name', 'address'])
  3.     writer.writeheader()
  4.     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:
  1. with open('for_excel.csv', 'w', newline='', encoding='utf-8-sig') as f:
  2.     writer = csv.writer(f)
  3.     writer.writerow(['姓名', '城市'])
  4.     writer.writerow(['张三', '北京'])
复制代码

反过来,读带 BOM 的文件时也用 utf-8-sig,它会自动吃掉 BOM。如果用普通 utf-8 去读带 BOM 的文件,第一个字段名前面会多出一个不可见的 BOM 前缀(\ufeff):
  1. with open('for_excel.csv', encoding='utf-8') as f:
  2.     reader = csv.DictReader(f)
  3.     print(reader.fieldnames)  # ['\ufeff姓名', '城市'] ← 第一个 key 脏了
  4. with open('for_excel.csv', encoding='utf-8-sig') as f:
  5.     reader = csv.DictReader(f)
  6.     print(reader.fieldnames)  # ['姓名', '城市']
复制代码

这个前缀非常隐蔽:row['姓名'] 会直接 KeyError,但把两个字符串打印出来看着一模一样,很容易排查半天。规则很简单:和 Excel 交互的文件,读写都用 utf-8-sig 最省心。

五、大文件:靠迭代器流式处理,别一次读进内存

处理几百 MB 甚至几 GB 的 CSV,新手常犯的错误是 f.read() 或 list(reader) 把整个文件读进内存,直接 OOM。其实 csv.reader 本身就是惰性迭代器,for 循环时一次只读一行,内存占用和文件大小无关:
  1. import csv
  2. from collections import Counter
  3. def count_by_city(path):
  4.     counter = Counter()
  5.     with open(path, newline='', encoding='utf-8') as f:
  6.         reader = csv.DictReader(f)
  7.         for row in reader:          # 一次一行,不会整体读入内存
  8.             counter[row['city']] += 1
  9.     return counter
复制代码

下面这种写法就是反例:
  1. with open('huge.csv', newline='', encoding='utf-8') as f:
  2.     rows = list(csv.DictReader(f))   # 整个文件塞进内存,大文件直接 OOM
  3.     for row in rows:
  4.         pass
复制代码

只要不主动 list() 它、不往一个大列表里 append,逐行 for 循环就是流式处理。如果要边读边写,比如清洗大文件,同时打开读流和写流,处理一行写一行即可:
  1. def clean_csv(src, dst):
  2.     with open(src, newline='', encoding='utf-8') as fin, \
  3.          open(dst, 'w', newline='', encoding='utf-8-sig') as fout:
  4.         reader = csv.DictReader(fin)
  5.         writer = csv.DictWriter(fout, fieldnames=reader.fieldnames)
  6.         writer.writeheader()
  7.         for row in reader:
  8.             row['name'] = row['name'].strip()   # 逐行清洗
  9.             if row['city']:                     # 过滤掉没有城市的行
  10.                 writer.writerow(row)
复制代码

这种模式下内存里始终只驻留一行,处理 10GB 文件也稳。

六、超大字段:field larger than field limit

如果某个字段特别大,比如一整段 JSON 或 base64 内容塞进一个单元格,读取时可能报错:
  1. _csv.Error: field larger than field limit (131072)
复制代码

csv 模块默认单字段上限约 131072 字节,也就是约 128KB。确实存在超大字段时,可以调大限制:
  1. import csv, sys
  2. 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) 临时放开,同时回头检查字段设计是否合理。
回复

使用道具 举报

发表于 半小时前 | 显示全部楼层

Re: Python大CSV读写实战:csv模块、BOM编码与字段上限

楼主这篇很实用,尤其 newline='' 和 utf-8-sig 这两个点,真是 CSV 读写里最容易忽略又最容易翻车的地方。我之前用 DictReader 读 Excel 导出的文件,fieldnames 第一个列名前面带 \ufeff,row['姓名'] 一直 KeyError,找了半天才发现是 BOM,后来统一改成 utf-8-sig 就好了。DictWriter 对多余 key 直接报 ValueError 这个也挺好,能早点发现脏数据。大文件部分如果后面继续写,感觉可以重点说说逐行迭代、别用 list(reader) 一次载入,以及字段特别长时 csv.field_size_limit 的问题。期待后续。
回复 支持 反对

使用道具 举报

发表于 半小时前 | 显示全部楼层

Re: Python大CSV读写实战:csv模块、BOM编码与字段上限

这篇总结得很实在。csv 模块真不是可有可无,split(',') 遇到地址、备注里带逗号或换行基本必翻车;DictReader / DictWriter 按列名读写也确实省心,不用再数下标。最容易忘的就是 open 里的 newline='',我有次没加,Windows 上每行后面多一个空行,排查半天才反应过来。utf-8-sig 这个点也很关键,写给 Excel 看用 BOM,读带 BOM 的文件也要用 utf-8-sig,不然第一个字段名前面多个 \ufeff,后面 row['姓名'] 直接 KeyError,特别阴。大文件和字段上限那部分好像还没展开,蹲个后续,尤其想看看流式读写和超长字段怎么处理。
回复 支持 反对

使用道具 举报

发表于 半小时前 | 显示全部楼层

Re: Python大CSV读写实战:csv模块、BOM编码与字段上限

这篇很实用,CSV 的坑基本都点在关键处了。newline='' 确实是很容易漏的一点,Windows 上不加经常出现多空行;utf-8-sig 也是和 Excel 交换数据时最省心的做法,读的时候同样用 utf-8-sig 能避开第一个字段名带 BOM 的诡异问题。DictReader 和 DictWriter 按列名处理,比数下标稳太多,写特殊字符交给 writer 自动加引号,也能避免手工拼接漏转义。大文件如果按流式逐行读、逐行写,不要 list(reader) 或 readlines,内存压力会小很多。标题里的字段上限和最后 KeyError 那段好像还没展开完,期待后续补全,整体很有帮助。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-9-11 19:58 , Processed in 0.023849 second(s), 17 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部