
你有没有遇到过这种灵异事件一个好好的.txt文件在自己电脑上打开是正常的发给别人对方一打开满屏的“锟斤拷锟斤拷”或者“”或者一串看不懂的符号。这不是文件坏了也不是中毒了。这是编码和解码对不上暗号——计算机世界里最常见、也最让人崩溃的 bug 之一乱码Mojibake。要根治乱码你得先搞懂一个根本问题计算机只会存 0 和 1它是怎么把中这个字存进去、又读出来的第一性原理字符 ≠ 字节先建立一个最关键的认知这也是所有乱码的根源计算机里没有字符只有数字。屏幕上那个中字在计算机眼里不过是一个整数。所谓字符编码Character Encoding干的就是两件相反的事编码Encode把字符 → 变成数字写文件时解码Decode把数字 → 变回字符读文件时。乱码之所以发生九成九是这一个原因写的时候用的编码和读的时候用的解码不是同一套规则。用 A 规则把字变成数字再用 B 规则把数字变回字对不上自然就驴唇不对马嘴了。想彻底搞明白就得从最早的编码说起。ASCII只够美国人用最早计算机只处理英文。1960 年代的ASCII编码用 7 个比特表示一个字符一共能表示2 7 128 2^7 12827128个字符——大小写英文字母、数字、标点、一些控制符。128 个对英文够用了可对中文呢《新华字典》收录的汉字就上万个128 个字符塞牙缝都不够。于是各个国家和地区开始八仙过海各显神通搞出各自的编码中国大陆搞了GB2312后来又扩展出GBKGBK 收录了约 2 万汉字台湾、香港搞了Big5日本搞了Shift-JIS韩国搞了EUC-KR……问题来了这些编码互相不认识。一段用 GBK 存的文本用 Big5 去读出来的就是一堆乱七八糟的字。这就是编码乱世——同一个字节序列在不同编码眼里是不同含义。想象一下A 国人把你好编码成01001100B 国人把这串数字解码出来看到的是 B 国字表里第 76 个字符——可能是龘可能是。数字没变是字典换了。Unicode给全世界每个字符发一个身份证号乱世的解药是大一统。Unicode应运而生它只做一件事给世界上每一个字符分配一个唯一的编号Code Point码点。“中的码点是U4E2D“A的码点是U0041连 emoji 都有码点U1F600。全世界统一用这一张字表”谁跟谁都不会再串味”。但这里有个极其关键、也最容易被误解的点务必记牢Unicode 只负责编号不负责怎么存。码点U4E2D是中的身份证号可这个号码在硬盘上到底占几个字节、怎么排布Unicode 不管。负责存储的是另一套规则——UTF-8、UTF-16、UTF-32。把概念分清楚Unicode 是字符到码点的映射UTF 是码点到字节的映射。一个是字典一个是装订方式。很多人乱码就是因为把这两件事混为一谈以为存成了 Unicode就够了。UTF-8最聪明的一种装订方式UTF-8 是现在互联网事实上的标准。它的设计特别巧妙——变长编码一个字符可能是 1 字节也可能是 2、3、4 字节取决于它的码点大小码点越小越常用的字符用的字节越少。具体规则是这样的字节数码点范围字节格式1 字节U0000 ~ U007FASCII0xxxxxxx2 字节U0080 ~ U07FF110xxxxx 10xxxxxx3 字节U0800 ~ UFFFF含汉字1110xxxx 10xxxxxx 10xxxxxx4 字节U10000 ~ U10FFFF含 emoji11110xxx 10xxxxxx 10xxxxxx 10xxxxxx它有几个精妙之处兼容 ASCII英文ASCII字符在 UTF-8 里还是 1 字节且字节值不变。所以纯英文的 UTF-8 文件和 ASCII 文件完全一样——这保证了老系统不崩。省空间常用字符英文、中文占的字节少只有 emoji、生僻字才用 4 字节。自同步看一个字节的开头就知道它是单字节字符还是多字节字符的第几个字节。哪怕中间丢了一个字节也能很快重新定位不会一路错到底。拿中字算一遍感受下规则中的码点是U4E2D二进制是0100 1110 0010 1101它落在 3 字节区间U0800 ~ UFFFF所以用1110xxxx 10xxxxxx 10xxxxxx的格式把码点的 16 位拆成 466填进三个字节的 x 里得到11100100 10111000 10101101 E4 B8 AD所以中在 UTF-8 里是三个字节E4 B8 AD。如果你拿这三个字节用 GBK 去解码GBK 会按它自己的规则把E4 B8、AD xx解释成别的字——乱码就这么诞生了。锟斤拷到底是怎么来的那满屏的锟斤拷究竟是个啥它的来历特别经典。当一段字节不是合法的 UTF-8时很多程序会用一个替换字符Replacement CharacterUFFFD 来顶替——意思是这有个我不知道的字符。而 UFFFD 这个字符如果用GBK 编码存下来会变成三个字节EF BF BD。当这三个字节又被别人用 GBK 解码时GBK 会把EF BF和BD xx解释成两个汉字——正好就是“锟斤拷”更准确地说锟斤拷是一串EF BF BD反复出现时GBK 解码出来的锟、斤、拷这几个字。所以锟斤拷的本质是**替换字符被错误地二次编码解码**的结果。它不是一个有意义的词而是乱码链层层叠加后的产物。看到它你就该知道有某个环节用 UTF-8 解码了一段不是 UTF-8 的数据。在 Python 里亲眼看看编和解理解了原理用 Python 实操一遍立刻通透s中# 编码字符 - 字节b_utf8s.encode(utf-8)print(UTF-8 编码:,b_utf8.hex())# e4b8ad三个字节# 解码字节 - 字符用对编码正常print(UTF-8 解码:,b_utf8.decode(utf-8))# 中# 用错编码解码立刻乱码try:print(GBK 解码:,b_utf8.decode(gbk))# 报错或出乱码exceptUnicodeDecodeErrorase:print(GBK 解码报错:,e)# 更狠的用 errorsreplace非法字节被替换成 print(容错解码:,b_utf8.decode(ascii,errorsreplace))# 注意最后一行用 ASCII 去解码中的 UTF-8 字节因为 ASCII 不认识三个字节被替换成了三个。一出现就说明这里发生了编码不匹配。你平时看到的乱码绝大多数就是它。结语乱码不可怕可怕的是不知道有编码这回事乱码这事折磨过几乎每一个写过代码的人。但它其实一点不神秘——只要记住一句话你就能看穿九成九的乱码字符要先编码成字节才能存储读回来要解码成字符才能显示编码和解码必须用同一套规则否则就是乱码。这也跟计算机底层一以贯之的逻辑相通所有信息最终都要落到比特上而比特本身没有意义是约定赋予了它意义。负数怎么存靠的是补码这种约定负数凭什么反着存补码——一场让减法从计算机里消失的天才设计字符怎么存靠的是编码这种约定。计算机世界的一切都是用约定给比特赋予意义。下次再看到乱码别慌也别急着删文件。停下来问一句“它被用什么编码、又被用什么解码了” 想通这一层你就从被乱码折磨的人变成了能治乱码的人。字符编码、字节、比特……这些信息如何表示的底层知识是计算机科学的基本功。想把这套数据表示的思维串成体系推荐 B站【408实验室】的《Python 完全自学教程》从 Python 的字符串、字节这些最常用的类型入手把编码这件事彻底弄明白。