起因

在编写互联网应用的时候, 特别是在处理不同国家的语言的时候, 很大几率会碰到编码的问题. 如果稍不注意, 通过 API 存入 DB 或者提交给别人网站的信息就讲变为乱码, 为了彻底解决这个问题, 所以详细了解了一下计算机中的各国文字等是如何弄出来的.

基础知识

说是基础知识, 是因为这些知识给人的感觉就好像是不用解释, 就是这样的一样, 其实我也解释不上来 - -||

首先需要统一的观点是: 计算机中的存储, 无论是在哪一个组件里面, 它最终的归宿都是二进制

无论是我们操作的内存, 存储数据的硬盘, 或者网络上传输的内容都是二进制, 最核心的 CPU 到现在也仅仅能处理二进制流 (当然, 如果你要问问什么不能是三进制或者十进制, 这个问题参考知乎吧.

不得不说的 ASCII 码

因为计算机的所有内容都是二进制存储的, 所以很明显的一点就是 “没有人可以像 CPU 一样读二进制啊”, 所有就有一个组织向 teleprinter encoding systems 学习(可以理解为莫斯密码那样的东西), 使用 8 个二进制对 26 个英文字母, 10 个数字和 20 几个特殊字符制作了一个可以互相对应查找的码表 ASCII 码表

我们把通过码表所表达出来的映射规则称为 “编码(encoding)”

我们把这个码表的编码(encoding)称为 ASCII 编码 (encoding).

我们把 a,b,c 这样的字符通过 ASCII 码表转换为 8 位二进制数这样的一个过程称为: 使用 ASCII 编码(encoding)进行 “编码(encode)”

我们把将 8 位二进制数通过 ASCII 码表解析成字母的这个过程称为: 使用 ASCII 编码(encoding) 进行 “解码(decode)”

我们说字母 a 的 ASCII 码是 0110 0001, 十六进制是 61, 十进制是 97

由于中文的博大精深, 使用一个 “编码” 就涵盖了一个名词和一个动词, 但在理解的时候还是要区分开了.

我不知道 ASCII 编码时不时计算机上的第一个编码, 但在我映像里面应该是第一批编码. 随着计算机从美国慢慢发展到全球不同的地方, 肯定是没有办法使用一个 ASCII 来表示所有的文字的, 例如中国字就没办法使用 ASCII 编码(encoding) 的规则来完成, 因为按照其规则最多边编码(encode) 2^8 = 256 个字符. 所以各个国家开始自己弄自己的一套编码(encoding), 出现了 GBK/GBK2312, BIG5 等等..

为了解决不同编码(encoding)各自为政的情况, 出现了一个名为 Unicode 的编码(encoding), 可能还会看到诸如 UCS-2, UCS-4 这样的名称, 这是因为不仅仅只有一个组织想解决这个问题, UCS 全称为 Universal Character Set, 这个是由 ISO 组织主导的, 但是随着这个时代的发展, 这两个组织意识到世界上没必要存在两种不同的编码来做同样的事情, 所以从 Unicode 2.0 开始两者开始合并双方的成果.

乱码的问题

如果世界上大家都使用 Unicode 编码(encoding) 那么就不会存在乱码的问题了, 可现实却使很残酷的… 所以我们在不同语言的网站之间处理数据就很可能碰到编码的乱码的问题. 而乱码问题的发生则是在解码的时候所使用的编码(encoding) 并不是产生所存储的那些二进制时使用的编码(encoding).

那我们来看看几个实际问题的情况:

  1. 通过 Java 代码读取本地文件的内容显示的是乱码.
  2. 抓取一个含有中文字内容的网站回来是乱码.
  3. 通过 API 提交含有中文字内容的信息给一个美国网站, 信息是乱码.

这些问题发生的本质原因都是编码(encode)时使用的编码(encoding)与解码时所使用的编码(encoding)不一样导致的.

当我们编写好的代码在处理文本内容的时候, 是一定会经过解码这个过程的, 一定会需要将存储在计算机中的二进制数据解码为字符, 而解码一定需要一个编码(encoding), 所以回想一下当你涉及到需要读取或者操作一个文件或者一串字符串或者一个页面的内容的时候, 一定会有一个地方携带者编码(encoding)这个 metadata.

  • 在通过 HTTP 协议从网站返回结果的时候会有 Content-Type: text/html; charset=UTF-8 告诉你返回的内容的编码.
  • 在读取文件系统的文件的时候, 这个文件会拥有一个自己的编码(encoding).
  • 对于 JVM 或者 Ruby VM 这样的 VM 来说, 会有一个 VM 级别的编码(encoding)设置.
  • 对于操作系统来说, 也会有一个 LANG=en_US.UTF-8 指定语言与编码(encoding).

然后从最上层开始, 如果没有找到 encoding 信息, 则会一层一层向下寻找, 直到最终找到操作系统所使用的编码(encoding) 作为一个默认编码. 以 Ruby 为例: “通过 Ruby 代码编写抓取某中文网站的内容返回了 HTML 内容, 可是没有告诉你 charset 是什么, 那么这个时候 Ruby VM 会使用 External encoding 来解析这些二进制数据, 如果没有 External encoding 则选择使用 Default External encoding, 而 Default External encoding 使用的是 Locale encoding, 而 Ruby VM 中的 Locale encoding 则是 Ruby VM 所在的操作系统的本地编码”.

所以, 如果想尽可能避免碰到乱码的问题, 那么一个是让自己使用 UTF-8 这样的统一编码, 一个是在编写程序的时候, 自己要能够从某些地方获取或者猜测到正确的编码, 否则就很容易出现乱码的情况. 同时在处理并理清含有乱码这样的问题的时候一定要理解清楚 “encoding”, “encode”, “decode” 以及 “二进制” 与 “字符串”

PS: 如果对计算机是如何处理编码还有一点点疑惑, 可以参看 深入分析 Java 中的中文编码问题 中关于 “按照 XXX 编码” 的部分来理解.