UTF-8:一些好像没什么用的冷知识

在乔纳森·斯威夫特的著名讽刺小说《格列夫游记》中,小人国内部分裂成Big-endian和Little-endian两派,区别在于一派要求从鸡蛋的大头把鸡蛋打破,另一派要求从鸡蛋的小头把鸡蛋打破。

然后忘了这个故事,咱们开始吧。

Charles同学这周踩了个坑,数据插入MySQL时报错:

1366 Incorrect string value: 
'\xF0\x90\x8D\x83...' for column 'content' at row 1

按惯例搜一下,据说是因为mysql用的 utf8 不支持 emoji,需要修改配置文件,将字符集改成 utf8mb4:

[mysqld]
character-set-server = utf8mb4
stackoverflow.com/questions/10957238

但是 Charles 同学已经给 MySQL 加上了这个配置,仍然报错。

image.png

实际上,MySQL 还有另外一个配置,用于指定客户端和服务器之间连接时使用的字符集:

[mysqld]
character-set-client-handshake = utf8mb4

当然,也可以在MySQL Client中指定,具体需要参考client的文档,或者简单粗暴地在连接成功以后执行(但不推荐):

SET NAMES utf8mb4;

utf8 和 utf8mb4

那么,什么是utf8mb4?和utf8有啥区别呢?

根据MySQL的manual:

The utfmb4 character set has these characteristics: 
- Supports BMP and supplementary characters.
- Requires a maximum of four bytes per multibyte character.

https://dev.mysql.com/doc/ref...

(文档中utf8mb4打错了,我是原样复制的)

翻译过来就是,utf8mb4 支持BMP(Unicode Basic Multilingual Plane)和补充字符,每个字符最多 4 字节(这里 “mb4” 大概就是 multi byte 4 的简写了)。

冷知识:Unicode编码一共有 17 个 "Plane"(0~16),其中 Plane 0 就是BMP,包含绝大多数常用字符,比如希腊、希伯来、阿拉伯、CJK(Chinese-Japanese-Korean)字符等。Plane 1~16 被称为 "supplementary planes",包含不常用的其他字符,例如 emoji 和某些特殊的CJK字符。所以目前 Unicode 字符的最大编码为 0x10FFFF。

至于 utf8,MySQL文档里也有说明:

utf8 is an alias for the utf8mb3 character set. 

Note
The utf8mb3 character set is deprecated and will be removed in a future MySQL release. Please use utf8mb4 instead

https://dev.mysql.com/doc/ref...

简单说就是挂羊头卖狗肉了,看到的是 utf8,实际用的是 utf8mb3 。

utf8mb3 的文档就不贴了(懒),和 ut8mb4 的区别就在于最多只支持3个字节,因此不支持Unicode的补充字符集。

也就是说,MySQL里的utf8,实际上是一个阉割版的utf8。

MySQL 从  5.5.3 才开始支持完整版的 utf8(utf8mb4),并且后续计划移除 utf8mb3,utf8 未来在mysql中也会变成 utf8mb4 的别名,所以以后默认都使用 utf8mb4 就对了。

话说回来,MySQL为什么会有这种奇怪的设定呢?

其实最初是从性能上考虑的,这个精简版的 utf8 在运行的时候可以更快一点点。

要知道 MySQL 已经是一个 24 岁的老项目了,在1995年诞生时,Intel 才只推出了 Pentium Pro,对比现在的CPU,性能可以说是非常差了。

冷知识:差到什么程度呢?举个例子,早期的 Windows Beta 版,桌面右下角时间是可以显示秒数的,但由于当时硬件的性能问题,微软在发布 Windows 95 之前就移除了该功能,直到Windows 7(2009年)才允许通过修改注册表开启。

真正的utf8

那么真正的 utf8 长什么样呢?

在查文档之前,不妨先动手创建一个文档看一下

$ echo '0Aa你好' > utf-8.txt

$ file utf-8.txt
utf-8.txt: UTF-8 Unicode text

$ xxd utf-8.txt #用16进制的方式查看
0000: 3041 61e4 bda0 e5a5 bd0a      0Aa.......

可以看到,开头"0Aa" 对应3个字节 0x30、0x41、0x61(十进制48、65、97,大写A < 小写a 就是这么来的)。

最后一个字符 0x0a 是  echo  默认输出的换行。

冷知识:可以加上 -n 参数让 echo 不输出换行符。换行符在不同OS下不同,在Linux/Unix下是 "\n" (0x0a),在Windows下是 "\r\n"(0x0d 0x0a),在早期Mac下是 "\r" (0x0d),从Mac OS 10.0(2001)开始也和Unix一样用 "\n" 了。在C/Python等语言下,fopen/open默认使用“文本模式”打开文件,读取时会统一转换成 "\n",写入时将"\n"转换为按os的默认值。还有一些其他场景可能需要注意,例如http协议中header使用 "\r\n" 换行。

中间的 "e4bda0e5a5bd" 这 6 个字节对应的就是 “你好” 了,每个字符 3 个字节。

那到底如何确定一个 utf8 字符是几个字节呢?

这里贴一个 wikipedia 的表格:

image.png

简单解释一下:

  • 第一个字节中,开头 1 的数量表明了这个utf8字符包含几个字节
  • 0开头,表示只需要1个字节,剩余7 bit可以表示unicode中的0~127,正好和 ascii 编码兼容。
  • 110 开头,表示需要2个字节,包含 11  bit,可以表示大部分非 CJK 字符(希腊、阿拉伯等字符)
  • 1110 开头,表示需要3个字节,包含 16 bit,正好可以表示所有BMP的字符,比如 “你好”都在 这个Plane里面,所以一共需要6个字节。
  • 11110 开头,表示需要4个字节,包含 21 bit,最多可以包含 32 个 Plane,超过了当前 17 个 Plane 的
  • 后续字符都是 10 开头,不会和首字符混淆,因此在解析的时候很容易识别,就算遇到了错误的编码字符(例如按字节截断到字符中间),也可以简单跳过,定位下一个字符。

前面展示了 ASCII 字符和中文,咱们顺便再看看 emoji 的 utf-8 编码长什么样:

$ echo -n 😀 > 1.txt

$ xxd 1.txt
0000: f09f 9880    ....

根据上面的表格,我们可以算出,这个GRIN FACE(露齿笑)的 Unicode 码点是 0x1F600,在Unicode的Plain 1中,因此utf-8编码需要4个字节。

字符集和编码规范

上文提到了 Unicode  和 utf-8 这两个名词,但是很多同学其实没搞明白他俩的区别是啥。

一般我们提到 Unicode 时指的是字符集(Character Set),其中包含了一系列字符,并为每一个字符指定了码点(Code Point,即该字符的编号)。

Unicode标准里包含了很多编码规范,utf-8 是其中一种,指定了一个Unicode字符的码点应该如何存储,例如ASCII用一个字节,超过一个字节的根据需要的bit数量,分别存储到多个字节中。

除了 utf-8 之外,Unicode还有多种不同的编码规范,例如

  • ucs-2,就是简单地一一对应 BMP 的编码,每个字符使用2个字节,因此不支持补充字符。
  • ucs-4,用4个字节来存储 unicode 编码,因此支持Unicode中的所有plain。Unicode后续的修订也会保证添加的码点都在31bit范围内。
  • utf-16,BMP内的字符等于ucs-2,其他plane用4个字节表示
  • windows和ecma规范(javascript)使用utf-16作为其内部编码。
  • utf-32,ucs-4的子集,一般可以认为就是ucs-4。

然鹅 utf-8 几乎统治了互联网,超过93%的网页是用UTF-8编码的,以至于IETF要求所有网络协议都需要标明内容的编码,并且必须支持UTF-8。

至于原因么,还记得开头的那个故事吗?utf-8避免了上述编码中的字节序(big endian、little endian)的问题。

当然这只是一个原因,我认为更重要的是,utf-8保持了对ascii的兼容,路径依赖的强大惯性,会导致上述4种编码在实际推广中带来很高的迁移成本(按理应该在这里讲讲马屁股宽度的故事,不过跑题太远了)。

utf-8 在保持后向兼容的前提下,能支持所有Unicode字符,相比ucs4还能节省大量存储空间、数据传输量,因此统治了互联网,也就在情理之中了。

除了Unicode之外,还有很多其他字符集,例如最经典的ASCII,由于字符少,其编码规范也相当简单。

在中国,比较常见的字符集还有GB2312(1980年)、GBK(1993年)、GB18030(2000年),这些标准都规定了对应的编码规范,所以这些名字既可以表示字符集,也可以表示编码规范。

其中GB2312只包含 7445 个字符,其中汉字 6763 个(只包含常用汉字,很多生僻字都不支持),编码规范也兼容ASCII。GBK(GB13000)兼容GB2312,添加了更多字符,GB18030是进一步的补充。

冷知识:我们可以使用 iconv 命令行工具来修改文件的字符编码

$ iconv -f gb18030 -t utf-8 < gb18030.txt 
0Aa你好

也可以在vim中这么干

:set fileencoding=gb18030

此外,使用 windows 的同学可能还见到过一个奇怪的代号 "cp936"(在上述iconv命令、vim中都可以使用),这是微软对 GB2312 的实现,在 Win95 以后实际上也支持了大部分 GBK 字符。

总结

  1. Unicode是一个字符集,包含17个Plane,每个Plane 65536个码点,Plane0是BMP,其他Plane是补充字符
  2. UTF-8是一种编码规范,用1~4个字节编码Unicode字符,兼容ASCII,中文3字节,补充字符如emoji需要4字节)
  3. MySQL中的utf8是阉割版的、只支持BMP的编码,以后记得都使用utf8mb4;除了server编码,记得也要修改连接的编码(客户端编码)。
  4. 除了utf-8之外,还有好几个没什么卵用的字符集/编码。
  5. 我在网盟广告业务线(穿山甲),由于业务持续高速发展,长期缺人。关于字节跳动面试的详情,可参考我之前写的《程序员面试指北:面试官视角

~ 投递链接 ~

后端开发(上海)
https://job.toutiao.com/s/sBAvKe

后端开发(北京)
https://job.toutiao.com/s/sBMyxk

广告策略研发(上海)
https://job.toutiao.com/s/sBDMAK

所有职位
https://job.toutiao.com/s/sB9Jqk

欢迎关注

微信扫码
image.png

阅读 582

推荐阅读
felix021
用户专栏

这个人很懒,什么都没留下。

841 人关注
18 篇文章
专栏主页