读一份UTF-8的CSV,解释器直接抛异常,数据一行都没读到:
Traceback (most recent call last):
File "D:\work\read.py", line 4, in <module>
rows = open("data.csv").read().splitlines()
UnicodeDecodeError: 'gbk' codec can't decode byte 0xa4 in position 12: illegal multibyte sequence
0xa4在GBK里是合法首字节,可后面那个字节凑不成完整的双字节汉字,解码器就抛异常了。文件本身是UTF-8,被当成GBK读,中文一多必然炸。
踩过不止一次。
根子在open()的默认编码
open()不写encoding参数时,用的是locale.getpreferredencoding(False)的返回值。中文Windows上这个值是cp936(也就是GBK),Linux上多半是UTF-8。同一段代码换个系统,行为就不一样——脚本在开发机上跑得好好的,丢到Windows的服务器上就崩,多半是这个原因。
import locale, sys print(locale.getpreferredencoding(False)) # 中文Windows输出 cp936 print(sys.getdefaultencoding()) # 这个常年是utf-8,跟文件读写无关
sys.getdefaultencoding()跟文件读写没关系,别拿它当判断依据,见过有人盯着它查半天。
先确认文件到底是什么编码
别猜。记事本另存时选的"UTF-8"在老版本Windows上是带BOM的,Excel导出的CSV常见GBK或者UTF-8带BOM,Linux上生成的日志基本是纯UTF-8。三种情况处理方式不一样。
看文件头几个字节最直观:
with open("data.csv", "rb") as f:
head = f.read(4)
print(head)
# b'\xef\xbb\xbf' -> UTF-8 with BOM
# b'\xff\xfe' -> UTF-16 LE,Excel另存的"Unicode文本"就是它
命令行上也能查(Git Bash、WSL、Linux都行):
file -i data.csv xxd -l 16 data.csv
file命令给出charset=utf-8还是charset=iso-8859-1,一眼清楚。给iso-8859-1基本等于"里面其实是GBK或UTF-8,file没认出来",遇到这种情况继续往下看。
拿不准就上检测库,虽然它不是百分百准,但对付常见中文编码够用:
pip install chardet
python -c "import chardet;print(chardet.detect(open('data.csv','rb').read()))"
# {'encoding': 'UTF-8-SIG', 'confidence': 1.0, 'language': ''}
confidence低于0.7的结果别当真,之前碰到一个几百KB的日志,chardet给出0.5的GBK,实际是UTF-8混了几行乱码。
解决办法:把encoding写死
1、纯UTF-8文件,显式声明:
with open("data.csv", encoding="utf-8") as f:
rows = f.read().splitlines()
2、带BOM的文件用utf-8-sig。用它读纯UTF-8文件也不会出错,所以判断不了有没有BOM时,直接用utf-8-sig更省事:
with open("data.csv", encoding="utf-8-sig") as f:
for line in f:
print(line)
不用utf-8-sig的话,第一行的表头会粘着一个\ufeff,看着像正常列名,拿去做字典key时就对不上了,这个点排查起来很费时间。
3、确认是GBK或GB2312的,写encoding="gbk"。gbk能覆盖gb2312,也能解一部分gb18030,遇到"illegal multibyte sequence"再换成gb18030试试,它是超集。
4、csv模块读文件,除了encoding还要加newline="":
import csv
with open("data.csv", encoding="utf-8-sig", newline="") as f:
reader = csv.DictReader(f)
for row in reader:
print(row["姓名"])
不加newline="",字段里嵌了换行的记录会被切成两条,行数看着多了但数据对不上。
5、pandas读CSV同样要带编码参数,报错也一样是UnicodeDecodeError:
import pandas as pd
df = pd.read_csv("data.csv", encoding="utf-8-sig")
# 少量坏字符不想整批失败时,加 encoding_errors="replace"
errors参数别乱用
errors="ignore"能让报错消失,代价是坏字节被悄悄丢掉。对账单、金额、证件号这种文件千万别用,实测过一次,两万行的对账文件跑完少了三个字符,核对时才发现。真要容错,用errors="replace",至少能看见问号,知道哪儿出过问题。
with open("data.csv", encoding="utf-8", errors="replace") as f:
text = f.read()
想定位到底是哪一段坏了,用二进制读出来自己解,报错里给的是字节位置,直接切:
raw = open("data.csv", "rb").read()
try:
text = raw.decode("utf-8")
except UnicodeDecodeError as e:
print(e.start, e.end, raw[e.start-20:e.end+20])
写文件也是同一回事
open()写文件不写encoding,Windows上就按GBK写出去了,下游拿到的就是乱码。写和读的编码要成对:
with open("out.txt", "w", encoding="utf-8", newline="\n") as f:
f.write("测试")
newline="\n"顺便避免Windows上把\n自动换成\r\n,脚本生成配置、证书这类文件时这一句很有用。
两个能提前暴露隐患的开关
PYTHONUTF8=1或者python -X utf8,能强制走UTF-8模式,临时救急用:
set PYTHONUTF8=1 # cmd export PYTHONUTF8=1 # bash python -X utf8 script.py
python -X warn_default_encoding(环境变量写法PYTHONWARNDEFAULTENCODING=1)会把所有没写encoding的open()以EncodingWarning报出来,改造老项目时开着它跑一遍,漏网的点全出来了。这个开关是Python 3.10加的。
往后看,Python 3.15起UTF-8模式会默认打开,PEP 686写的就是这件事,3.15.0a7在2026年已经放出来了。也就是说现在靠cp936兜着的代码,到了Python 3.15上行为会变:以前能读的GBK文件会开始报错,以前报错的UTF-8文件反而正常。手头有要长期维护的脚本,趁Python 3.11、3.12、3.13、3.14这几个版本还在,把encoding补齐最省事。
补齐的办法很机械:所有open()都带encoding,读用utf-8-sig,写用utf-8。跑一遍warn_default_encoding,报哪儿改哪儿。
数据截至2026年9月。