你用本地命令行上的一个工具(内部其实是个 Python 脚本)去读某台服务器上的文本文件。文件里明明是有中文的,可屏幕上显示出来的却是一串 ����,偶尔还夹着 ° 变成的怪符号。第一反应几乎都是:「远端那个文件坏了」,然后 SSH 上去折腾半天,发现文件本身一点问题没有。
现象
- 一个明明有内容的文本文件,
cat出来是����之类的替换字符; - 但同一个文件,直接在服务器上用
more或编辑器打开,中文完全正常; - 换了台机器读同一个文件,有时又好了。
这些现象指向一个反直觉的结论:文件是好的,坏的是「读了之后怎么显示出来」这一段路。
根因
关键在异常发生在输出链路,而不是数据本身。
这个本地工具内部用的是 Python。当它在 Windows 上运行时,sys.stdout 按系统代码页(简体中文下是 GBK)编码。流程是这样的:
- 远端文件是 UTF-8 字节,被完整地读进来;
- Python 内部把它当作 UTF-8 解码,得到正常的字符串;
- 打印时,Python 先把这个字符串按 GBK 编码,再写出去。
如果远端的内容是纯 ASCII,第 3 步没问题。但一旦遇到 GBK 编不出来的字符,或者编码结果和终端期望不一致,屏幕上就出现 ����——这是「编码转换失败」的典型长相。
换句话说,远端传来的字节是对的,是本地 Python 在写 stdout 时用错了编码,把它重新编码弄坏了。
这也解释了为什么「直接上服务器看」是好的:服务器上的工具用 UTF-8 输出,不经过本地这层 GBK 管道,自然正常。
解决
给执行远端命令的这一步强制 UTF-8 输出:
PYTHONIOENCODING=utf-8 python remote_read.py <服务器> /path/to/file
PYTHONIOENCODING=utf-8 会告诉 Python:不管控制台代码页是什么,标准输出一律按 UTF-8 编码。这样第 3 步就用 UTF-8 编码,与远端的数据一致,中文就能正确显示。
如果这个工具是你自己的脚本,也可以在脚本里固定下来,不依赖每次敲环境变量:
import sys
try:
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
except AttributeError:
pass
延伸与预防
这条坑最有价值的不是解法(一行环境变量),而是排查方向。
遇到显示乱码时,先问自己一句:
是数据坏了,还是我这一端的显示管道坏了?
判断方法很简单——换个方式看同一份数据。直接在数据产生地看、用十六进制看、用另一种编码读一遍。如果换个方式就正常,那数据没坏,是管道的问题。
这个「先分清是数据还是管道」的思路,能套用到一大类问题:
- 数据库里存的到底对不对,还是客户端显示不对?
- 接口返回的是不是真的乱码,还是终端渲染的问题?
- 页面上的字显示错了,是后端编错了还是前端解码错了?
很多时候,乱码出现在链路的末端,而根因在链路的某一环用错了编码。 别一看到乱码就本能地怀疑数据源——先在链路上定位是「哪一跳」把它弄坏的,再修那一跳。