我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

Python打印中文乱码排查指南:从编码原理到方案落地

Python打印中文乱码排查指南:从编码原理到方案落地 只要用 python 写过中文输出几乎都会碰到打印中文乱码的问题要么屏幕上吐出一堆“鈥欏瓙”之类的字符要么直接报UnicodeEncodeError代码本身明明只有一行print(你好)。我一开始也以为这是 Python 的“脾气”后来排查多了才明白乱码不是玄学而是字符串在“编码 → 输出 → 终端解码”这条链路上某一层用了不对的规则。这篇文章就围绕“python 打印中文乱码”这个场景把最常见的几种现场拆开讲清楚给出的都是可以直接复制运行的代码。如果你刚被控制台中文输出折磨过或者在 Windows 和服务器之间来回切环境时被编码问题膈应过这篇应该能帮你一次性把思路理顺。1. 乱码不是玄学先搞懂中文到屏幕之间发生了什么1.1 字符、字节、编码三者关系其实很简单计算机底层没有“汉字”这个概念只有 0 和 1 组成的字节。我们写的字符比如“你”是一张“字符画”存储和传输时必须把它翻译成一串字节这个过程叫编码。反过来把字节还原成字符叫解码。字符你能看到的“你”“好”字节二进制数据比如 UTF-8 编码下的b\xe4\xbd\xa0\xe5\xa5\xbd编码规则字符和字节之间的“翻译表”常见有 UTF-8、GBK、GB2312、Latin-1用一个生活化的例子理解把汉字“你好”想成一段法语编码就是翻译规则。UTF-8 是一位法语翻译GBK 是另一位法语翻译同一个意思两个人翻译出来的文字稿可能不一样。如果说话的人用的是 UTF-8 翻译听的人却拿着 GBK 翻译稿去理解最后得到的内容自然就离谱了。1.2 print 到底做了哪些事很多人以为print(你好)就是把内存里的字符串原封不动放到屏幕上其实它至少经过了三步Python 拿到一个str字符串对象里面是 Unicode 码位print调用标准输出流的编码器把字符串编码成字节终端收到字节后再按自己的字符集去解码并显示。只要第 2 步和第 3 步的编码规则不一致就会乱码。如果第 2 步的编码器遇到无法表示的字符比如 GBK 编码器碰到某些 emoji 或生僻字就会直接抛出UnicodeEncodeError程序在编码阶段就崩了。关键结论乱码的本质不是“Python 不会处理中文”而是“写的时候一套规则读的时候另一套规则”。1.3 乱码的几种“长相”和它背后的线索不同形状的乱码往往对应不同原因。我整理了一张速查表排查时可以先对号入座乱码样子最可能的原因???或大量问号编码阶段遇到无法表示的字符被替换成了问号常见于 GBK 编码器遇到生僻字或终端代码页不支持方块、空白编码和解码其实对了但终端或字体缺少对应字形显示不出来浣犲ソ这类“假中文”字符串本来按 UTF-8 编码成字节终端却按 GBK 解码两个规则错位直接报UnicodeEncodeErrorstdout 编码器无法表示目标字符程序在编码阶段直接失败看到“假中文”时不要急着改代码先想一下浣犲ソ其实就是 UTF-8 字节被 GBK 解码后的结果。这个特征非常典型我在 Windows 命令行里遇到最多的就是它。2. 最常见的三个翻车现场2.1 现场一Windows 自带命令行窗口里跑 print这是中文乱码的重灾区。早期 Python 2 时代Windows 的 stdout 默认编码是 GBK/cp936而很多人的代码文件保存成 UTF-8。Python 2 里你好是一个字节串print直接把 UTF-8 字节丢给终端终端却按 GBK 解码于是出现“假中文”。到了 Python 3默认字符类型变成了 Unicode新建式终端里直接print(你好)一般不会报错。但如果你用的是旧版终端、或者标准输出被重定向到管道和文件Python 3 可能回退到 locale 编码在 Windows 上依然是 GBK。如果你打印的是 GBK 无法表示的字符比如某些特殊符号就会触发UnicodeEncodeError。快速验证方法在命令行里跑下面这段代码看sys.stdout.encoding输出的是什么。python -c import sys; print(sys.stdout.encoding)如果输出是cp936或gbk说明当前 stdout 用的就是 GBK。后面第 3 章会讲怎么处理。2.2 现场二源码文件保存编码和解释器对不上有一种乱码和print本身没关系字符串在写代码时就已经“坏了”。Python 3 解释器默认按 UTF-8 读取源码文件。如果你的编辑器右下角显示的是 GBK 编码却又没有在文件头部写编码声明那解释器按 UTF-8 去解一份 GBK 文件轻则字符串内容错乱重则直接SyntaxError。有些开发者习惯在文件第一行写# -*- coding: utf-8 -*-但注意这行声明只在 Python 2 里是必要的Python 3 默认就是 UTF-8。更关键的是声明负责的是“源码文件本身按什么编码解码”它管不到运行时 stdout 的编码。网上很多人以为加了这行声明就能解决 print 乱码其实加错了地方。正确做法是先统一源码文件编码编辑器里把文件另存为 UTF-8 无 BOM 格式检查编辑器右下角的编码状态不要出现“文件声明 utf-8实际保存 gbk”的情况项目里能统一就统一别一个文件 UTF-8、另一个 GBK。2.3 现场三从网页或文件读到了字节直接打印这种乱码常见于爬虫和数据处理脚本。抓网页、读二进制文件、调用接口拿到一段bytes如果直接print(bytes)控制台会把它打印成b\xe4\xbd\xa0\xe5\xa5\xbd看着像乱码其实只是字节的展示形式。如果先做了解码但用错了规则问题更隐蔽。比如某个网站实际用的是 GBK 编码你拿到字节后却按 UTF-8 去decode(utf-8)print 出来就是一堆乱码。这里的问题不在 print而在 decode 那一步。我的排查习惯是先不要猜去看字节本身。输出前用repr(data[:100])看看前后缀再结合页面返回的Content-Type头里的charset字段确认编码。详情在第 3.4 节展开。3. 从根上解决的几个方案代码可直接抄3.1 方案 A把 stdout 的编码强制改成 UTF-8Python 3.7 以上可以直接给标准输出流重新配置编码import sys if hasattr(sys.stdout, reconfigure): sys.stdout.reconfigure(encodingutf-8)为什么是reconfigure因为 Python 3.7 之前不能随便改sys.stdout的编码很多人只能重新包一层TextIOWrapper。在旧版本里可以用这种方式import io import sys sys.stdout io.TextIOWrapper(sys.stdout.buffer, encodingutf-8)这段代码把 stdout 重新用 UTF-8 编码包装print输出的中文就会先编码成 UTF-8 字节。配合终端把代码页也切到 UTF-8屏幕上显示就正常了。需要留意几个点sys.stdout.buffer在标准输出被重定向时一般存在在某些 IDE 或 Jupyter 风格的环境里stdout 可能没有buffer属性这时用hasattr判断更安全如果 stderr 也有中文建议同样处理sys.stderr.reconfigure(encodingutf-8)。3.2 方案 B不用改代码用环境变量如果不想在代码里加这些“环境相关”的东西可以直接设置环境变量。Linux/macOS 的命令行export PYTHONIOENCODINGutf-8 python app.pyWindows 的命令行set PYTHONIOENCODINGutf-8 python app.pyPython 3.7 以后还有一个更强的开关PYTHONUTF81它开启 UTF-8 模式让 Python 在文件读写、标准输入输出等很多地方都把默认编码统一成 UTF-8。两者的差别可以用一个对比说明变量作用范围典型场景PYTHONIOENCODINGutf-8只影响标准输入、输出、错误的编码print 中文乱码、重定向输出乱码PYTHONUTF81影响标准输入输出、文件读写默认编码、很多 stdlib 的默认行为想整个环境统一 UTF-8LC_ALLC.UTF-8系统 locale间接影响 Python 的 locale 编码服务器环境 locale 太老如果只是想让print正常用PYTHONIOENCODING就够了如果你还希望open()不带encoding参数时也默认用 UTF-8就上PYTHONUTF81。注意运行时临时设置的环境变量只对当前进程和它的子进程生效放到容器、定时任务或服务配置里时要把环境变量写进启动参数或服务文件否则下次跑又乱码。3.3 方案 C让 Windows 终端自己“准备好”这一方案专门对付 Windows 命令行窗口。最老套但也有效的办法是把代码页切到 UTF-8chcp 65001在命令提示符里先执行这条再运行 Python 脚本基本能解决一部分“假中文”问题。不过我并不推荐在脚本里通过os.system(chcp 65001)来调代码页因为这会闪一下屏幕而且只对当前终端有效重定向到文件时没什么作用。更省心的方式是直接用新版终端软件很多新式终端默认就是 UTF-8对中文显示友好得多。还有一类情况是编码没问题但屏幕上显示成方块。这通常不是编码错误而是终端字体缺少中文字形。给终端设置一个支持中文的字体比如“微软雅黑”“宋体”或者“中易黑体”方块问题就消失了。我在旧版 Windows 的默认终端里见过很多次这种“其实是字体问题”的假乱码。3.4 方案 D外部数据进门之前先正确解码如果你的乱码来自网页抓取、文件读取、接口返回核心操作不是改 stdout 编码而是把 bytes 正确地转成 str。网页抓取场景import urllib.request url http://example.com resp urllib.request.urlopen(url) data resp.read() # 如果网页返回的 charset 是 gbk就不要按 utf-8 硬解 text data.decode(gbk, errorsreplace) print(text)errorsreplace的意思是解不开的字节用替换字符替代这样程序不会挂方便先定位哪一段内容出了问题。排查阶段用这个参数非常顺手确认一切正常后再改成更严格的模式。文件读取场景with open(notes.txt, encodingutf-8) as f: content f.read() print(content)不要用open(notes.txt)这样把编码交给系统默认值的写法。系统 locale 一变程序表现就变这在跨环境部署时是隐患。显式写encoding是成本最低的确定性做法。至于“实在不知道对方是什么编码”的情况可以先用编码检测库做一个概率判断再人工核对。不过检测库只是根据字节特征猜准确率不是 100%接口能返回charset时优先用接口信息。4. 屏幕显示正常、重定向到文件乱码最容易被忽略的坑4.1 同一个 print换个目的地就翻车有一种情况很迷惑直接在终端里跑printf 中文一切正常一加重定向到文件再用编辑器打开文件就乱码。原因是 Python 对“交互式控制台”和“文件/管道”处理方式不同。在 Windows 控制台里新版 Python 有能力直接用 Unicode 接口写屏幕所以显示正常但 stdout 一旦被重定向到文件Python 就只能按照 locale 编码Windows 上常见是 GBK把字符编码成字节写进文件。于是文件里的内容其实是用 GBK 编码的而你的编辑器如果默认按 UTF-8 打开就会看到乱码。反过来也可能出现“文件正常、终端乱码”但原理一样控制台和文件用了两套不同的编码策略。4.2 别让 print 替你做文件写入一旦要落盘我更推荐“显式写文件”而不是依赖 shell 的重定向with open(result.txt, w, encodingutf-8) as f: f.write(中文结果\n)涉及日志时直接用日志模块的FileHandler并显式指定编码import logging handler logging.FileHandler(app.log, encodingutf-8) logger logging.getLogger(__name__) logger.addHandler(handler) logger.setLevel(logging.INFO) logger.info(中文日志)把“往哪里写”和“用什么编码写”都显式管起来就不会再出现“终端里看着没问题落到文件就乱码”这种玄学。4.3 服务器和定时任务场景locale 影响很大我曾经在 Linux 服务器上跑一个生成中文报告的脚本手动执行没问题放到定时任务里就报UnicodeEncodeError。后来发现是定时任务环境里的LC_ALLC或缺省 locale 导致 Python 默认编码退化成 ASCII。这种场景最稳妥的做法是两个一起用启动命令里带上PYTHONUTF81代码里凡是写文件都显式指定encodingutf-8。export PYTHONUTF81 python generate_report.pywith open(report.md, w, encodingutf-8) as f: f.write(f统计结果{result}\n)只要标准输出和目标文件都明确走 UTF-8就基本不会因为服务器 locale 不同而翻车。5. 一套可以复现的排查流程四步定位乱码5.1 第一步先让 Python 自己“交代”当前编码遇到乱码时先跑一段调试代码把环境信息全打印出来避免瞎猜import locale import sys print(Python 版本:, sys.version) print(默认编码:, sys.getdefaultencoding()) print(stdout 编码:, sys.stdout.encoding if sys.stdout else no stdout) print(locale 编码:, locale.getpreferredencoding(False)) print(字符串类型:, type(你好)) print(真实内容:, ascii(你好))输出里重点看两处stdout 编码决定print用什么规则编码字节locale 编码决定文本文件读写时的默认编码也会影响某些库的默认行为。如果你发现stdout 编码是cp936/gbk而你希望统一到 UTF-8就用第 3.1 节或 3.2 节的方法改。如果stdout 编码是utf-8但屏幕依然乱码问题多半在终端代码页或字体。5.2 第二步确认字符串内容本身是否已经坏了敲下面这行命令看看字符串的 Unicode 码位python -c print(ascii(你好))如果输出是\u4f60\u597d说明字符串内容是对的问题出在输出链路。如果输出的是浣犲ソ之类的内容说明字符串在更早的解码阶段就错了要去检查源码文件编码、读取文件时的encoding参数、或者网络数据的解码方式。这一步的价值在于把问题一分为二字符串坏了还是显示坏了。判断清楚之后处理方向完全不同。5.3 第三步最小化复现逐个变量试不要在大项目里当面调试写一个临时脚本只保留最核心的几行import sys if hasattr(sys.stdout, reconfigure): sys.stdout.reconfigure(encodingutf-8) print(你好中文乱码排查)然后分别在下面几种环境里跑直接在命令行跑用 out.txt重定向后再打开 out.txt换成PYTHONUTF81环境变量跑在 Windows 下先chcp 65001再跑。每次只改一个变量观察结果变化很快就能定位到是哪一层的问题。我排查这类型问题从未超过十分钟靠的就是这个最小复现法。5.4 第四步避开sys.setdefaultencoding这类“邪道”网上还能搜到一些旧答案教人这样做import sys reload(sys) sys.setdefaultencoding(utf-8)这个做法在 Python 2 里就不该乱用Python 3 里连setdefaultencoding都被移除了再这么写纯属走弯路。它表面上解决了输出乱码实际是改变了整个解释器的默认编码容易把其他第三方库的隐式转换也带偏埋下莫名其妙的坑。还有一点要强调给文件头部加# -*- coding: utf-8 -*-不是万能药。它管的是“解释器怎么读源码文件”不管“print 输出时用什么编码”。所以该加声明的地方加声明该配 stdout 编码的地方配 stdout 编码两者不能互相替代。5.5 症状与处理动作速查我把高频场景和处理方式汇总成了一个小表排错时可以直接对照症状可能原因推荐动作Windows 命令行输出“假中文”UTF-8 字节被 GBK 解码改源码文件为 UTF-8终端切代码页或换新式终端print 中文报 UnicodeEncodeErrorstdout 编码器不能表示字符sys.stdout.reconfigure(encodingutf-8)或设PYTHONIOENCODINGutf-8终端正常重定向到文件乱码控制台和文件/管道使用不同编码策略显式写文件并指定encodingutf-8从网页读到的打印乱码decode 用错编码按响应头charset正确解码不确定时用errorsreplace先排查方块或空白字体缺字形不是编码错误给终端设置支持中文的字体服务器定时任务乱码locale 或环境变量不是 UTF-8启动脚本里设置PYTHONUTF81文件写入显式指定编码我现在的个人习惯是程序内部字符串全部用 Unicode不做任何编码转换只有到了文件读写、网络收发、终端输出这些“边界位置”才按需编码和解码。这样写出来的代码换到 Windows、Linux、macOS 上行为都很稳定。实用的小技巧是把上面那段调试函数放进你的代码模板里以后遇到乱码先跑一遍再对症下药比试各种办法快得多。
返回列表