一、前言
字符编码这个问题,困扰了无数程序员,一不小心就会掉进坑里,每当在开发中遇到乱码或者emoji表情符的奇怪问题时总是让人头疼不已,本文就来从根源上研究一下字符编码的本质和原理。
二、何为编码
编码的本质其实就是翻译,举个几个🌰:
- 两个说普通话的中国人进行交流时,是不需要编码的,一个人说「你好」,另一个就能直接听明白意思,反之亦然。
- 一个中国人和美国人交流时,就需要多一个中文到英文、和英文到中文的翻译过程,才能实现双向交流。
- 一个人类和计算机交流时,也是一样的道理,大家都知道计算机只认识二进制0和1,因此必须把人类语言转为二进制,才能把信息传递给计算机,这个过程叫做编码(encoding),反之则称为解码(decoding)。
三、怎么编码
1. ASCII
一种直观的想法就是,制作一个映射表,把人类语言和计算机二进制对应起来就行了,这样的思想就孕育出了ASCII编码(因为是美国人设计的,所以只有常用英文字符),如下表所示,非常直观:
2. Unicode
但是ASCII表太小了,最多只能编码128个字符,但人类语言又那么多,显然无法满足需求,于是世界各地的人们就分别设计了符合自己语言需求的映射表。在自己的地区使用时是没有问题的,但是一旦进行国际交流,由于映射规则各不相同,就会导致混乱。
于是人们设计了一个很大的映射表——Unicode,总共可以编码100多万个字符(最多到 0x10FFFF),目的是为了容纳世界上所有的人类语言。Unicode为每个字符分配了一个固定的数值,称为编码点(Code Point),这个值是全局唯一的。而且Unicode向前兼容 ASCII,原先在 ASCII 中定义的字符映射,在 Unicode 中也是一模一样的。
3. UTF-8
按理说设计好了 Unicode,大家都按照 Code Point 编码解码,混乱的问题就已经解决了,但是还有一个因素要考虑,就是存储成本。
Unicode设计是三个字节,如果不加考虑直接存储 Code Point,那么所有的字符都需要占用三个字节。比如 'A',如果使用ASCII,则一个字节即可 0x41,如果使用 Unicode,则要编码为 0x000041,这凭空多出来两个字节,对于英语文本来说完全是一个额外的存储开销。
为了降低存储成本,人们发明了变长编码,这里要注意下,并不是重新定义映射规则,还是用的 Unicode 的定义,只是以一种更加灵活的形式来存储以节约空间。以目前通用性最高的 UTF-8 为例,原本只需要一个字节的 ASCII 字符,仍然只占一个字节,而像中文及日语这样的复杂字符就需要2个到3个字节来存储。
UTF-8 的规则挺简单的:
- 对于单字节的符号,字节的第一位设为0,后面7位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的。
- 对于n字节的符号(1 < n <= 4),第一个字节的前n位都设为1,第n + 1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。
维基百科的一览表:
Number of bytes | Bits for code point | First code point | Last code point | Byte 1 | Byte 2 | Byte 3 | Byte 4 |
---|---|---|---|---|---|---|---|
1 | 7 | U+0000 | U+007F | 0xxxxxxx | |||
2 | 11 | U+0080 | U+07FF | 110xxxxx | 10xxxxxx | ||
3 | 16 | U+0800 | U+FFFF | 1110xxxx | 10xxxxxx | 10xxxxxx | |
4 | 21 | U+10000 | U+10FFFF | 11110xxx | 10xxxxxx | 10xxxxxx | 10xxxxxx |
四、Emoji
😝😊😂……这些表情符大家应该都已经不陌生了,但它们并不是什么新的概念,其实也是 Unicode 的一部分,比如这个表情 😀的Code Point是:0x1F600,查上面的 UTF-8 编码表可以看出需要4个字节来编码:0xF09F9880。完整 emoji 列表可以参见这里:https://unicode.org/emoji/charts/full-emoji-list.html
。
MySQL emoji问题:
UTF-8 是能支持所有的 Unicode 字符的,因此按理说 emoji 不应该会导致问题,但是在MySQL里实现的 utf8 最长只使用3个字节,如果向一个编码为 utf8 的列中插入一个表情符时,就会报类似Incorrect string value: '\xF0\x9F\xA6\x96 ...' for column 'name'
这样的错,可以看出此时想要插入的值已经有四个字节了,因此需要指定该列编码格式为 utf8mb4 才行。
五、参考资料
http://cenalulu.github.io/linux/character-encoding/
http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html
https://en.wikipedia.org/wiki/ASCII
https://en.wikipedia.org/wiki/Unicode
https://en.wikipedia.org/wiki/UTF-8