美文网首页程序员
编码与乱码——追根究底

编码与乱码——追根究底

作者: Henry606 | 来源:发表于2018-07-13 21:15 被阅读0次

乱码问题是不但是新手程序员之痛,也常常让许多资深 coder 束手无策。最近在社区接连收到关于乱码问题的求助,五花八门,我觉得是时候深入讨论一下这个问题了。本文试图让读者深入理解编码的概念以及乱码的产生的原理,以至于今后再遇到乱码问题,能够独立分析、解决。
由于 Sublime Text 是笔者最青睐的编辑器,因此文中的所有截图和实验均以 Sublime Text 为例,其他编辑器或 IDE 在原理上是类似的。

一、什么是编码?

什么是编码?这要从「文件」的概念说起。根据呈现形式,文件可分为两种类型:「文本文件」和「二进制文件」。

二者的区别非常明显,文本文件中保存的是各种字符,包括英文字母如 abc、汉字如 你好、日文如 こんにちは 等;而二进制文件中保存的则是 0101 等二进制数值。如果你用 Sublime Text 分别打开文本文件和二进制文件,那么它们呈现的样子大致如下:

文本文件与二进制文件

注:我们习惯采用十六进制的方式简化二进制数据的显示,这样对人类用户稍微友好一些,避免了过长的 0-1 串使得人们眼花缭乱。

为什么会产生这两种类型的文件呢?一个非常直接的原因是,文本文件主要是给人类用户看的,例如我们常使用的 txt、markdown 文件,各种代码文件如 .cpp.java.py.js 等,以及各种配置文件如 .ini.json 等;而二进制文件则是给操作系统或应用程序看的,如 .exe 交给 Windows 系统执行、Word 文档交给 Office Word 软件打开、.class 文件交给 java 虚拟机执行,许多应用程序都会设计自己专用的二进制文件格式。

尽管我们把文件分为文本文件和二进制文件两种类型,但从计算机硬件层面上来看,它只能存储 0101 这样的二进制数据,不可能直接存储 abc 这样的字符。那么该如何解释文本文件的存在呢?

事实上,从存储方式上来看,文件确实只有一种类型,那就是二进制文件。至于文本文件,它只是二进制文件的一种特殊情况。在计算机最初发明的时候,确实只有二进制文件,那时的人们通过「打孔的纸带」作为存储程序的载体,而纸带上小孔的有无就代表二进制的 1 和 0。那时候的计算机根本没有字符的概念,更不要说文本文件。

后来,人们为了方便就制定了一套规则,规定二进制数值 01100001 代表字符 a01100010 代表字符 b、……、01111010 代表字符 z。于是,最早的编码「ASCII 编码」就产生了。现在,如果我在一个文件中写入二进制数据 011000010110001001100011,从表面上看,它就是一个常规的二进制文件,没有任何特殊之处,但如果我用 ASCII 编码的规则去解释它,就会看到一串字符 abc。这时候,我们就可以认为这个文件是文本文件。

从上面的描述中,你应该已经发现:

  • 所谓的「编码」就是一种规则,它规定了二进制数值与字符之间的映射关系
  • 所谓的「文本文件」就是一种二进制文件,只不过能用某种编码解释得通

说回到 ASCII 编码,它使用 8 个二进制位——也就是 1 个字节来映射一个字符,这意味着它最多只能映射 2^8=256 个字符。256 个字符对于纯英文来说已经足够了,但世界上的语言太多了,要囊括英文、德文、法文、中文、日文、韩文、阿拉伯文、希伯来文等所有语言文字,至少需要十几万的字符量。随着各种文字不断被引入计算机,字符编码的长度也不断扩张,从 1 个字节逐渐增加到 2 个、3 个、4 个字节。同时,各个组织、各个国家都在制定自己的编码体系,形成了错综复杂的编码“方言”。最终,到了 1994 年,人们终于制定出了一套统一的、无所不包的编码——Unicode 编码,成为编码界的“世界语”,因此也被称为万国码。

Unicode 编码使用 4 个字节来保存字符映射关系,因此共支持 2^(4*8)=4294967296 个字符,远远超出了地球上所有文字的总量。这彻底解决了字符数量不够用的担忧,但也带来了存储空间的浪费:即使仅仅保存一个简单的英文字母 a,Unicode 编码也需要 4 个字节,但事实上只需要 1 个字节(ASCII 编码)。如果一个文本文件中绝大部分字符都是英文字母,那么 Unicode 就浪费了 75% 的存储空间。鉴于上述问题,人们又制定了一系列“改良版”的 Unicode 编码,包括 UTF-8、UTF-16、UTF-32 等,它们同样能够编码所有已知的字符,但占用更少的空间。

以 UTF-8 为例,对于常见的英文字符,它采用 1 个字节编码,常见的中文、日文等字符采用 2 个字节,不常见的中文字符等采用 3 到 4 个字节,对于极不常见的字符,它会采用 6 个字节进行编码。因此,在通常情况下,UTF-8 编码要比 Unicode 编码节省超过一半的空间。UTF-8 编码无所不包、节省空间,且具有良好的跨平台性,因此推荐一切文本文件都使用 UTF-8 编码。目前,主流的文本编辑器都把 UTF-8 作为默认编码方式。

最后解释一下所谓的「ANSI 编码」。ANSI 编码常被称为标准编码,但它并不是指某种明确的编码方式。为了更容易地理解 ANSI 编码,我们不妨把它与「官方语言」的概念做类比。正如中国的官方语言是汉语,日本的官方语言是日语一样,中文 Windows 系统的 ANSI 编码为 GBK 编码,而日文 Windows 系统的 ANSI 编码为 Shift_JIS 编码。正如「官方语言」不是某种语言,「ANSI 编码」也不是某种编码,它是另一个维度的概念,与国家和地区有关,不同国家和地区的 ANSI 编码是不兼容的。可想而知,如果都采用 ANSI 编码,那么不同国家的开发者在互相交换代码时将非常糟糕。因此,不推荐以 ANSI 作为 coding 编码。

二、什么是乱码?

什么是乱码?用某种编码方式去解读一个文件,得到了无意义的字符,这就是乱码。打个通俗的比方:我写了一段英文,你非要把它当作拼音来读,那么得到的解释就是无意义的,就相当于乱码;反过来,我写了一段拼音,你非要用英语的语法去解释它,也是解释不通的。

举几个实际的例子:

  • 用 UTF-8 编码打开一个二进制文件会出现乱码:
用 UTF-8 编码打开一个二进制文件
  • 用 UTF-8 编码打开一个 GBK 编码的文本文件会出现乱码:
用 UTF-8 编码打开一个 GBK 编码的文本文件
  • 用 UTF-8 编码打开一个 UTF-8 编码的文本文件不会乱码:
用 UTF-8 编码打开一个 UTF-8 编码的文本文件

综上,乱码的根源就是编码与解码用的不是同一套规则。 但不管文件是否乱码,它里面保存的二进制数据总是不变的。通常情况下,乱码并不是文件本身有问题,而是打开方式(解码方式)不正确

三、编程中出现乱码的原因与类型

我们在日常使用文本编辑器、IDE、命令行等编写和执行程序的过程中,常常会遇到乱码现象,而出现乱码的原因是多种多样的。这里试图从根源上理解乱码,并将其归类。

一般,我们编写和执行程序的流程如下:

  1. 编写代码并保存;
  2. 调用编译器编译代码,并执行程序;
  3. 查看输出结果。

在这短短的三步操作中,隐含着两次编码和解码过程,也就是下图中的过程 1 和过程 2:

代码编写和执行过程中的编码和解码

在过程 1 和过程 2 中,任意一个过程两端的编码方式都必须一致,否则就会出现乱码。其中,对于「代码文件的编码」以及「展示器的编码」,我们可以在编辑器和控制台中进行设置。最不可控的是编译器的输入编码和输出编码,常见编译器/解释器的默认输入输出编码如下表所示:

编译器/解释器 默认输入编码 默认输出编码 设置输入编码 设置输出编码
python UTF-8 ANSI # coding=xxx 环境变量 PYTHONIOENCODING
gcc/g++ UTF-8 UTF-8 未知 未知
javac ANSI ANSI -encoding 参数 未知
matlab ANSI ANSI 修改配置文件 未知

注:该结果是笔者在自己的 Windows 10 家庭中文版上测试得到的,不同的平台可能有差异。


接下来,我们将以 Sublime Text 执行一段 Python 脚本为例来展示这 2 种乱码,通过设置编译器输入编码、输出编码、展示器编码来探究乱码产生的不同原因。

这段 Python 脚本非常简单,只有一句话:print('你好'),以 UTF-8 编码保存。正常执行的结果如下:

正常无乱码

从上上图中不难看出,过程 1 和过程 2 均能导致乱码,其组合可形成如下三种乱码类型:

类型 1:过程 1 乱码

我们在 Python 脚本头部添加一行 # -*- coding: gbk -*-,即把 Python 解释器的输入编码指定为 GBK,但脚本的编码保持 UTF-8 不变。执行结果将发生乱码,如下:

乱码类型 1

从这里我们也可以看出,Python 解释器的默认输入编码为 UTF-8。

类型 2:过程 2 乱码。

这里又分为两种情况,一是编译器的输出编码错误;二是展示器的输入编码错误:

2-1. 编译器输出编码不当。

打开 Python.sublime-build 文件(可借助 PackageResourceViewer 插件),其初始内容如下:

{
    "shell_cmd": "python -u \"$file\"",
    "file_regex": "^[ ]*File \"(...*?)\", line ([0-9]*)",
    "selector": "source.python",
    "env": {"PYTHONIOENCODING": "utf-8"},
}

我们把末尾的行改为 "env": {"PYTHONIOENCODING": "gbk"},,即把 Python 解释器的输出编码设为 UTF-8。执行脚本,再次得到乱码,如下:

乱码类型 2-1

注意:这里虽然也是乱码,但与类型 1 不同。

2-2. 展示器输入编码不当。

我们首先撤销对 Python.sublime-build 的所有更改,然后在其末尾增加一行内容 "encoding": "gbk",,即把 Sublime Text 控制台的编码设为 GBK。此时 Python.sublime-build 配置如下:

{
    "shell_cmd": "python -u \"$file\"",
    "file_regex": "^[ ]*File \"(...*?)\", line ([0-9]*)",
    "selector": "source.python",
    "env": {"PYTHONIOENCODING": "utf-8"},
    "encoding": "gbk",
}

执行脚本,得到乱码,如下:

乱码类型 2-2

注意:这里的乱码与类型 1 相同,都是用 GBK 编码解释 UTF-8 字符串造成的。

类型 3:过程 1 与过程 2 同时乱码。

乱码是可以叠加的,即乱码后的字符串可以再次被乱码,得到的乱码与叠加前的乱码均不同。

我们让 Python.sublime-build 文件保持上一步的状态,然后在 Python 脚本的开头重新加上一行 # -*- coding: gbk -*-。执行脚本,会得到前两种完全不同的乱码,如下:

乱码类型 3

以上就是编程中出现乱码的 3 种典型情况。需要指出的是,以上采用 Sublime Text 的控制台作为展示器,其编码可以通过 Build System 中的 encoding 参数进行设置。如果你直接使用命令行如 cmd、bash、cmder 等来编译和运行程序,那就完全省去这些麻烦了,命令行一般会自动识别你的输出编码,因此总能使用正确解码方式,基本不会出现类型 2 乱码,但无法避免类型 1 乱码

希望本文对你有所启发,如果你在编程中遇到了乱码,不妨对下图中的 2 个过程进行控制变量式的排除,如果能够解决你的问题,那便是本文最大的成功。

代码编写和执行过程中的编码和解码

相关文章

  • 编码与乱码——追根究底

    乱码问题是不但是新手程序员之痛,也常常让许多资深 coder 束手无策。最近在社区接连收到关于乱码问题的求助,五花...

  • java基础——servlet乱码问题

    servlet 乱码问题 1. 乱码的本质 乱码的本质就是文件或者流存的编码与读的编码不一样,就会导致乱码。 2....

  • Java 字符编码

    任何乱码问题都是因为编码和解码不一致造成。出现乱码时只需将乱码按照当前编码方式重新进行编码,然后在按照编码时所用的...

  • 9. 字符编码与Python之文件操作

    字符编码 1 字符在内存与硬盘中的编码对应关系 2 文本文件存取乱码问题 3 解决Python解释器读文件时不乱码...

  • 12月7日全天:宽字节注入原理解析

    网页出现乱码的原因: 客户端与数据库的数据传输处编码、数据库存储处编码,两者编码不同,就会出现乱码。还有一种情况,...

  • 插入数据库出现中文乱码问题

    插入数据库出现中文乱码问题 出现这种乱码问题,肯定是编码除了问题,编码和解码不是同一种编码格式就会出现乱码情况! ...

  • 简单介绍计算机常用编码

    计算机常用编码 常用编码介绍 ASCII码表 乱码产生的原因 解决乱码 notepad++等记事本中的乱码问题 常...

  • 编码的本质和乱码的恢复 (下)

    乱码 上节说到乱码出现的主要原因,即在进行编码转换的时候,如果将原来的编码识别错了,并进行了转换,就会发生乱码,而...

  • 编码与解码及乱码解决方案

    编码与解码及乱码解决方案 码表: 注意:Unicode不是一个码表,只是一个规范。 一、编码 编码: 把看得懂的字...

  • 任务4-HTML-1

    1.网页乱码的问题是如何产生的?怎样解决 答:乱码产生的原因是编辑器所保存的编码格式与浏览器解析所使用的编码格式不...

网友评论

    本文标题:编码与乱码——追根究底

    本文链接:https://www.haomeiwen.com/subject/ntispftx.html