8

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

 3 years ago
source link: https://mp.weixin.qq.com/s/G5CbBdVNjwWCRYoZHQ8KHQ
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

Original felix021 felix021 2020-03-29

本文一共 3023 字,阅读时间自己算。

在乔纳森·斯威夫特的著名讽刺小说《格列夫游记》中,小人国内部分裂成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 已经加上了这个配置,仍然报错。

640?wx_fmt=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/refman/8.0/en/charset-unicode-utf8mb4.html

(文档中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/refman/8.0/en/charset-unicode-utf8.html

简单说就是挂羊头卖狗肉了,看到的是 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.txtutf-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 的表格,建议横屏食用:

640?wx_fmt=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.txt0000: 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. 我在网盟广告业务线(穿山甲),由于业务持续高速发展,长期缺人、不限HC。关于字节跳动面试的详情,可参考我之前写的《程序员面试指北:面试官视角

~ 投递链接 ~

后端开发(上海)

https://job.toutiao.com/s/sBAvKe

640?wx_fmt=png

后端开发(北京)

https://job.toutiao.com/s/sBMyxk

640?wx_fmt=png

广告策略研发(上海)

https://job.toutiao.com/s/sBDMAK

640?wx_fmt=png

其他地区、职能线

https://job.toutiao.com/s/sB9Jqk

640?wx_fmt=png


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK