UTF-8 与 GBK 编码详解:从乱码到 Windows 代码页

起因:一次 hermes update 脚本报错,.cmd 文件里全是 '慨澶嶉噸鏀?' 不是内部或外部命令 这样的乱码。排查后发现是 UTF-8 编码的文件被 cmd.exe 按 GBK 解析 导致的。这篇文章把这次踩坑学到的编码知识完整总结下来。


一、先分清两个概念:字符集 vs 编码

很多人把这两个概念混在一起说,其实这是两层:

概念 回答什么问题 例子
字符集 (Charset) “中"这个字排第几号? Unicode 里"中"是U+4E2D;GBK 里它是 0xD6D0
编码 (Encoding) 这个编号怎么存成字节? U+4E2D 存成 3 字节E4 B8 AD(UTF-8)或 2 字节 D6 D0(GBK)

“中"还是那个"中”,只是两套系统给它编了不同的号、用不同的方式存储。

文件里存的永远是字节,读文件时必须知道"按哪套规则解释这些字节”——这就是编码的由来。

我的理解是:字符集就像字典,收录字符并给出一个唯一标志,编码就是把这个标志转换为字节表示


二、GBK:中文世界的老编码

  • 中国国家标准,从 GB2312 扩展而来,是 1990 年代 ~ 2010 年代中文电脑的默认编码
  • 汉字 2 字节,ASCII 字符 1 字节
  • 只覆盖中文 + ASCII(及少量日文假名、朝鲜文等),不认识其他文字
  • 优点:中文省 1/3 空间;是中文 Windows 的原生编码(代码页 CP936)

三、UTF-8:全世界的通用语

  • Unicode 字符集的一种编码实现,目标是把全人类所有文字统一编号
  • 变长编码:ASCII 1 字节、欧洲文字 2 字节、中文 3 字节、生僻字 4 字节
  • 关键设计:完全兼容 ASCII——任何一个 ASCII 文本文件,字节不变就是合法的 UTF-8 文件

这就是"UTF-8 最兼容"的真正含义:它不是"和所有编码兼容",而是它本身就是国际标准,任何现代系统都按它解释

⚠️ 但注意:UTF-8 和 GBK 之间不兼容——同一个中文,UTF-8 写 3 字节,GBK 写 2 字节,用错规则读就是乱码。


四、实测:同一个字,两种编码的字节对比

>>> '中'.encode('utf-8')
b'\xe4\xb8\xad'      # 3 字节
>>> '中'.encode('gbk')
b'\xd6\xd0'          # 2 字节
>>> 'A'.encode('utf-8')
b'A'                  # 1 字节
>>> 'A'.encode('gbk')
b'A'                  # 1 字节(ASCII 两者相同)
字符 Unicode 码点 UTF-8 字节 GBK 字节
U+4E2D E4 B8 AD(3B) D6 D0(2B)
A U+0041 41(1B) 41(1B)

五、乱码是怎么产生的

今天脚本报错的"重放"二字,UTF-8 编码是 6 字节:E9 87 8D E6 94 BE

写入时 (UTF-8):  E9 87 8D E6 94 BE        → 6 字节
读取时 (GBK):    E9 87 | 8D E6 | 94 BE    → 每 2 字节拼成一个"字"
结果:            "閲嶆斁"                 ← 完全不同的字

更糟的是,GBK 按 2 字节分组时,某些字节组合恰好落在 & | < > 等命令分隔符的字节值上,cmd.exe 就把整行当命令执行了——这就是 '慨澶嶉噸鏀?' 不是内部或外部命令 的来源。

乱码的本质是"解释规则用错了"。 同一个文件,用 UTF-8 读是对的,用 GBK 读就是一堆莫名其妙的字。

用 Python 可以完整还原这个过程:

word = '重放'
u8b = word.encode('utf-8')
print(u8b)                        # b'\xe9\x87\x8d\xe6\x94\xbe'
print(u8b.decode('gbk'))          # 閲嶆斁  ← 乱码现场

六、Windows 上的特殊性(重点)

Windows 和 Linux/macOS 最大的不同:Windows 有"ANSI 代码页"这个历史包袱

Windows Linux / macOS
系统默认编码 看区域设置:中文系统 ANSI =GBK (CP936) 一律 UTF-8
cmd.exe / 旧记事本 默认按 ANSI 读写
为什么容易乱码 文件是 UTF-8 但工具按 GBK 读 → 乱码 大家都 UTF-8,不乱

中文 Windows 上的几条铁律

  1. 老工具(cmd.exe、旧记事本、旧软件)默认按 GBK 解释文件——UTF-8 文件没标记的话必乱
  2. UTF-8 BOM 是给 Windows 的"我是 UTF-8"标记——文件开头加 EF BB BF 三个字节,Windows 记事本看到就按 UTF-8 读(但 Linux 工具反而讨厌 BOM)
  3. cmd.exe 解析批处理时不跳过 BOM——第一行 @echo off 会变成 @echo off 直接报错(BOM 不能乱加)
  4. chcp 65001 是 cmd 的"切换语言"命令——放在批处理开头,之后的输出就按 UTF-8 解释

实战:一个能用的 UTF-8 中文批处理

@echo off
chcp 65001 >nul
echo 中文正常显示

要点:无 BOM + CRLF 换行 + 前几行纯 ASCII(@echo off/chcp 65001)+ 中文放在 chcp 之后

为什么这里不用 PowerShell?

同样是编码问题,PowerShell 完胜 cmd:PowerShell 7 原生 UTF-8 无 BOM,5.1 认 UTF-8 BOM,根本没有 cmd 那种 GBK 字节错位解析。cmd 唯一的优势是"双击就能跑、零配置"(PowerShell 默认执行策略禁止运行 .ps1)。成熟做法是 一行 .cmd 启动器 + .ps1 主脚本

@echo off
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0script.ps1"

七、实际选择:什么时候用哪个?

场景 推荐 原因
代码文件、跨平台、发给别人 UTF-8(无 BOM) 现代标准,任何平台无歧义
Windows 批处理.cmd、INI 老配置 GBKUTF-8 + 技巧 cmd 默认 GBK;用 UTF-8 要配合chcp 65001
给 Windows 记事本编辑的中文文档 UTF-8带 BOM 老记事本认 BOM;新版 Win10 已默认 UTF-8 无 BOM
老软件(银行控件、老游戏) GBK 它们只认 ANSI 代码页

八、总结

  1. 字符集管编号,编码管存储——乱码 = 用错误的规则解释字节
  2. GBK 是中文 Windows 的老母语(2 字节/汉字),UTF-8 是全世界通用的新语言(3 字节/汉字)
  3. 乱码的本质是"解释规则用错了",不是文件坏了
  4. 现代开发一律 UTF-8 无 BOM;碰到 cmd / 老软件再单独处理
  5. Windows 上遇到编码问题,优先用 PowerShell 而不是 cmd