1. 从一次 UnicodeDecodeError 说起如果你写过 Python 读文件大概率见过这个报错UnicodeDecodeError: gbk codec cant decode byte 0xad in position 12: illegal multibyte sequence。明明文件用记事本打开一切正常代码一跑就炸。更迷惑的是同一份代码在同事电脑上没事在你这里就报错——因为你们系统默认编码不一样。这个问题的根源是很多人把 Unicode 和 UTF-8 当成同一个东西。实际上它们处在两个不同层面Unicode 是字符集负责给世界上每个字符分配一个唯一编号码点UTF-8 是编码方式负责把这个编号变成计算机能存的字节序列。Python 3 里str是 Unicode 字符串bytes是字节串两者之间的转换必须显式指定编码否则解释器只能猜猜错就报错。这篇内容面向正在被乱码折磨的 Python 开发者尤其是处理中文文件、调第三方 API 返回 JSON、在 Windows 终端打印日志的人。我会从字符集和编码的底层关系讲起给出可以直接复制的open参数、编码声明、错误处理配置最后用一段脚本把str和bytes的转换结果打印出来让你亲眼看到边界在哪里。全程不需要额外装库标准库就够。2. 先把 Unicode 和 UTF-8 的关系理清楚2.1 字符集和编码是两件事打个比方Unicode 像一本字典规定「严」这个字对应编号 U4E25UTF-8 像一套快递打包规则规定这个编号怎么装进字节盒子。字典只有一本打包规则可以有好几套——UTF-8、UTF-16、GBK 都是不同的打包方式。用记事本存一个「严」字选不同编码文件字节完全不同保存方式十六进制字节说明ANSIGBKD1 CFGB2312 编码双字节UnicodeUTF-16 LEFF FE 25 4EFF FE 是 BOM标明小端Unicode big endianFE FF 4E 25FE FF 是 BOM标明大端UTF-8EF BB BF E4 B8 A5EF BB BF 是 BOME4B8A5 是编码注意 UTF-8 那行E4 B8 A5是「严」的 UTF-8 编码前面EF BB BF是 BOM。Python 读带 BOM 的文件时如果用utf-8解码BOM 会变成字符串开头的\ufeff这就是为什么有时候读配置第一行会多出奇怪字符。解决办法是用utf-8-sig。2.2 Python 3 的 str 和 bytes 边界Python 3 把这件事分得很清楚strUnicode 字符串你看到的「严」「hello」都是 str它没有编码概念只有码点。bytes字节串b\xe4\xb8\xa5这种它没有字符概念只有 0-255 的整数。str.encode(utf-8)把字符串按 UTF-8 打包成字节bytes.decode(utf-8)把字节按 UTF-8 拆包成字符串。方向搞反、编码写错都会报错。Python 2 里str本身就是字节所以才有#coding:utf-8那套声明Python 3 源码默认 UTF-8不需要再写编码声明但读写外部文件时仍然要显式指定。3. 接入前的准备用 TaoToken 验证编码处理结果讲编码问题最怕「我以为对了」。我习惯把转换结果丢给模型做一次交叉验证尤其是处理多语言 JSON 的时候。这里用 TaoToken 的模型对话能力来跑验证它兼容常见接口格式改个 base_url 就能用。先到官网了解能力范围https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content然后在控制台创建 API Keyhttps://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content接入文档在这里参数和错误码都列了https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentAPI 端点统一用https://taotoken.net/api如果你只是偶尔验证编码结果用模型对话就够如果要把编码检查嵌进日常开发流程、长期跑脚本可以看 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content4. 可复制的编码配置与脚本4.1 open 参数怎么写读文件时永远显式指定 encoding不要依赖系统默认# 推荐写法明确编码 明确错误处理 with open(data.txt, r, encodingutf-8, errorsstrict) as f: text f.read() # 读带 BOM 的文件Windows 记事本另存的 UTF-8 with open(bom.txt, r, encodingutf-8-sig) as f: text f.read() # 编码不确定时先按字节读再尝试解码 with open(unknown.txt, rb) as f: raw f.read() for enc in (utf-8, gbk, utf-16): try: print(enc, -, raw.decode(enc)[:20]) except UnicodeDecodeError as e: print(enc, 失败:, e)errors参数有三个常用值strict直接抛异常默认ignore丢掉无法解码的字节replace用替换。生产环境读用户上传的文件建议先strict探测失败再降级不要一上来就ignore否则数据静默丢失。4.2 终端输出乱码怎么处理Windows 终端默认可能是 GBKprint中文有时会报UnicodeEncodeError。两种处理方式import sys # 方式一运行时重设标准输出编码Python 3.7 sys.stdout.reconfigure(encodingutf-8) # 方式二写文件时统一用 utf-8避免终端差异 with open(log.txt, w, encodingutf-8) as f: f.write(严\n)如果是在 CI 或容器里跑建议直接设环境变量PYTHONIOENCODINGutf-8比改代码更省事。4.3 JSON 序列化的编码坑json.dumps默认ensure_asciiTrue会把中文转成\u4e25这种转义。想让 JSON 文件里直接显示中文import json data {name: 严, city: 北京} # 默认中文被转义 print(json.dumps(data)) # {name: \u4e25, city: \u5317\u4eac} # 保留中文 print(json.dumps(data, ensure_asciiFalse)) # {name: 严, city: 北京} # 写文件时同时指定编码 with open(out.json, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2)读 JSON 时如果文件带 BOMjson.load会报JSONDecodeError用encodingutf-8-sig打开即可。4.4 一段脚本看清 str/bytes 转换把下面这段存成check_encoding.py直接跑输出会告诉你每个环节的字节和码点# -*- coding: utf-8 -*- import json s 严 print(str 本身:, s, | 码点:, hex(ord(s))) b_utf8 s.encode(utf-8) b_gbk s.encode(gbk) print(UTF-8 字节:, b_utf8.hex( )) print(GBK 字节:, b_gbk.hex( )) # 正确解码 print(UTF-8 解码:, b_utf8.decode(utf-8)) # 错误解码会抛异常这里捕获演示 try: b_utf8.decode(gbk) except UnicodeDecodeError as e: print(用 GBK 解 UTF-8 字节 -, e) # JSON 往返 payload json.dumps({k: s}, ensure_asciiFalse) print(JSON 字符串:, payload) print(JSON 字节:, payload.encode(utf-8).hex( )) print(往返结果:, json.loads(payload)[k] s)实测下来b_utf8.decode(gbk)大概率抛UnicodeDecodeError因为E4 B8 A5按 GBK 拆是两个不完整的多字节序列。这就是乱码和报错的本质字节没变拆包规则错了。5. 验证请求把编码结果交给模型确认脚本跑完后可以把输出贴给模型做一次语义核对尤其是多语言混合的场景。用 curl 发一个请求curl https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: claude-sonnet-4-20250514, messages: [ {role: user, content: 以下 Python 输出中UTF-8 字节 e4 b8 a5 对应的字符是什么GBK 字节 d1 cf 呢请分别说明码点。} ] }成功时返回结构里choices[0].message.content会给出字符和码点解释。如果返回 401检查 Key 是否带上了Bearer前缀返回 404检查路径是不是/api/v1/chat/completions注意/api是端点前缀不要漏。模型对话入口在这里可以直接在网页里试https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content6. 本篇常见错误排查6.1 UnicodeDecodeError: gbk codec cant decode最常见。原因是open没写encodingWindows 默认用 GBK 去解 UTF-8 文件。解决显式写encodingutf-8。如果文件确实是 GBK就写encodinggbk别硬套 UTF-8。6.2 读出来开头多个 \ufeff文件带 UTF-8 BOM。用encodingutf-8-sig打开Python 会自动吃掉 BOM。判断方法raw[:3] b\xef\xbb\xbf。6.3 终端 print 中文报 UnicodeEncodeError标准输出编码不是 UTF-8。加sys.stdout.reconfigure(encodingutf-8)或设PYTHONIOENCODINGutf-8。注意reconfigure在 Python 3.7 才有。6.4 json.load 报 JSONDecodeError 但文件看着正常多半是 BOM 或尾部有多余字符。先raw open(path,rb).read()打印raw[:10]看开头字节再决定用utf-8还是utf-8-sig。6.5 encode 报 surrogates not allowed字符串里有孤立代理项常见于从某些接口拿到的脏数据。用s.encode(utf-8, errorsreplace)先清洗或定位来源修掉。6.6 同一份代码跨平台结果不同Linux/macOS 默认 UTF-8Windows 默认 GBK。所有open都显式写encoding不要依赖默认值这是唯一稳妥的做法。7. 继续深入的方向编码问题排查到最后拼的是对字节的直觉。建议你养成两个习惯读外部文件先rb看前几个字节确认 BOM 和编码特征写文件永远显式encodingutf-8不给自己留坑。把第 4.4 节那段脚本存下来遇到乱码先跑一遍比猜快得多。需要长期把编码检查、JSON 校验这类任务串成自动化流程的话Coding Plan 提供了更稳定的调用额度https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content接入文档里对错误码和参数有完整说明遇到 4xx 先查这里https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentAPI Key 管理页面记得定期轮换https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content