首页   注册   登录
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
felix021
V2EX  ›  程序员

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

  •  
  •   felix021 · 67 天前 · 3804 次点击
    这是一个创建于 67 天前的主题,其中的信息可能已经有所发展或是发生改变。

    在乔纳森·斯威夫特的著名讽刺小说《格列夫游记》中,小人国内部分裂成 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 还有另外一个配置,用于指定客户端和服务器之间连接时使用的字符集:

    [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.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 的表格 Number

    字节数 | 比特数 | Unicode 区间 开始~结束 | 字节 1~4
    1	7	U+0000	U+00007F	0xxxxxxx			
    2	11	U+0080	U+0007FF	110xxxxx 10xxxxxx		
    3	16	U+0800	U+00FFFF	1110xxxx 10xxxxxx 10xxxxxx 
    4	21	U+10000	U+10FFFF	11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
    

    简单解释一下:

    1. 第一个字节中,开头 1 的数量表明了这个 utf8 字符包含几个字节
    2. 0 开头,表示只需要 1 个字节,剩余 7 bit 可以表示 unicode 中的 0~127,正好和 ascii 编码兼容。
    3. 110 开头,表示需要 2 个字节,包含 11 bit,可以表示大部分非 CJK 字符(希腊、阿拉伯等字符)
    4. 1110 开头,表示需要 3 个字节,包含 16 bit,正好可以表示所有 BMP 的字符,比如 “你好”都在 这个 Plane 里面,所以一共需要 6 个字节。
    5. 11110 开头,表示需要 4 个字节,包含 21 bit,最多可以包含 32 个 Plane,超过了当前 17 个 Plane 的
    6. 后续字符都是 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. 我在网盟广告业务线(穿山甲),由于业务持续高速发展,长期缺人、不限 HC 。关于字节跳动面试的详情,可参考我之前写的《程序员面试指北:面试官视角》: https://mp.weixin.qq.com/s/Byvu-w7kyby-L7FBCE24Uw

    ~ 投递链接 ~

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

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

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

    其他地区、职能线 https://job.toutiao.com/s/sB9Jqk

    第 1 条附言  ·  66 天前

    再补充一个冷知识。

    可能有些 PHP 程序员还踩过一个坑 :

    用windows的 notepad 编辑utf-8编码的php文件并保存以后,代码执行不正常了。

    这是因为 notepad 给文件开头打了个标记 (你可以用 xxd 或者 ultraedit 打开试试看)。

    这个标记被称为 UTF-8 BOM,具体值为 0xEF 0xBB 0xBF,用来告诉文件的读取方,这个文件是使用 UTF-8 编码的。

    比如说,你的csv是使用utf-8编码的,如果没有这个BOM,使用微软的Excel打开就会出现乱码。

    至于PHP的问题,由于它在code tag(<?php)之前,PHP的解释器会把它当成 html 文件的一部分,可能就会导致 session 相关函数启动之前,就输出了http body。

    最后,再学习一下移除 BOM 的方法吧:

    • vim:不要炸弹
    :set nobomb
    
    • sed:inplace
    $ sed -i '1s/^\xEF\xBB\xBF//' bom.txt
    
    27 条回复    2020-03-30 10:49:33 +08:00
    wolfan
        1
    wolfan   67 天前 via Android
    现在的 HR 都这么花式了?
    labulaka521
        2
    labulaka521   67 天前 via Android
    硬核招聘
    felix021
        3
    felix021   67 天前
    @wolfan 我不是 HR……
    Archeb
        4
    Archeb   67 天前
    很详细,冷知识也挺有趣的,赞一个

    我倒是对夹点链接啊公众号的没啥所谓,内容好就行...
    SmartKeyerror
        5
    SmartKeyerror   67 天前 via Android
    阔以,赞一个
    james122333
        6
    james122333   67 天前
    当然有用阿 (滑稽)
    不过 win 下应该会显示 ms936
    xfriday
        7
    xfriday   67 天前
    写的好,清晰明了
    felix021
        8
    felix021   67 天前
    @james122333 可能是 win10 什么的换了?我好久没看了,记得早年用 xp 的 cmd,跑一些老 dos 程序出现乱码,这时候选 cp936 一般就好
    also24
        9
    also24   67 天前   ❤️ 2
    感谢楼主分享,补充几个点。

    UTF-8 是如何避免字节序问题的 ?
    字节序问题本质上是对于 『跨字节』数据结构(例如 4 字节的 int )的存储顺序差异。
    UTF-8 虽然是『多字节』编码,但是内部每个字节是互相独立表示的,从字节序角度来看,可以当作单字节看待,自然也就不存在『跨字节』的问题。


    UTF-8 能被大量推广,和自身的变长编码属性也有很大关联。
    UTF-8 对于 ASCII 字符只需要 1 字节,但是对于 CJK 字符,都需要至少 3 字节。
    而 GB-2312 虽然字符集偏小,但是对于常用汉字的表示只需要 2 字节。
    在 ASCII 字符集为主的互联网世界,这样是更省空间的,但是从某种角度来说,其实 CJK 字符集的使用着是做出了牺牲的。

    Emoji 并不是一定 4 字节,比如说我们看一下中国国旗 🇨🇳,就是 8 字节的:
    https://i.loli.net/2020/03/29/JEgZYmiORQ6hPdH.png


    Emoji 还存在 U+200D 这种『骚操作』,这个零宽连字符可以将多个 Emoji 连在一起,造出新的 Emoji:
    https://i.loli.net/2020/03/29/J4K5dZMg2DqRp9O.png

    比如这个一家三口 ,👨‍👩‍👦其实就是 『男』+『女』+『小男孩』
    also24
        10
    also24   67 天前   ❤️ 2
    🤓 楼主的这篇文章倒是提醒了我,之前写的关于更冷门的博多码的文章也分享了一下 /t/657280
    bitdust
        11
    bitdust   67 天前
    除了 utf-8 之外,还有好几个没什么卵用的字符集 /编码。
    =======================================
    恭喜,国内的招标采购没你了
    felix021
        12
    felix021   67 天前 via Android
    @bitdust 假装一点都不在意
    xxapp
        13
    xxapp   67 天前 via Android
    看了半天才发现原来 charles 是个人名😂
    limbo0
        14
    limbo0   67 天前 via Android
    @also24 这个说法不太同意,因为 utf8 前缀编码能识别出顺序,所以才不用指明字节序,你说的好像是已经 decode 后的过程了
    also24
        15
    also24   67 天前   ❤️ 1
    关于 『 8 字节 Emoji 』,这里需要做一点修正,由于这类 Emoji 事实上也是『组合』出来的,并不是严格意义上的单个字符。(但是中间都没有 U+200D 这个连字符)

    我了解到的这类 Emoji 主要分两种:
    1 、Emoji flag sequence
    也就是国旗这一类,比如我举例的中国国旗🇨🇳,其实是由字母🇨和🇳组成的。
    相应的列表可以在这里找到: https://en.wikipedia.org/wiki/Regional_Indicator_Symbol#Emoji_flag_sequences

    2 、Emoji 控制符
    最典型的就是 Emoji 的肤色控制 ,Apple 在 iOS 8.3 引入的多种肤色 Emoji,其实就是 『表情』+『肤色』拼接在一起组成的。比如 👦🏿 其实就是 👦+ 🏿。
    https://i.loli.net/2020/03/29/qjWAexMNQwrcpbG.png

    在不支持这个特性的系统上看,就会看到一个默认肤色的表情+色块。(使用 Android 手机的朋友应该会经常看到)

    除此之外,还有键帽序列、VS-15/16 等控制符也可以实现类似的控制功能,他们都会导致 8 字节 Emoji 的出现。


    BTW:U+200D 其实也是控制符,不过这家伙出场至少就是 11 字节了。
    also24
        16
    also24   67 天前
    @limbo0 #14
    UTF-8 总是第一个字节在前的,也可以换个角度来理解为:UTF-8 总是 大端序


    前缀编码的主要好处应该是可以无视传输过程中出现的局部错误吧,和字节序关联不是很大。
    felix021
        17
    felix021   67 天前
    @xxapp 对,不是那个花瓶,哈哈哈,这里可能是没写清楚,有点歧义
    mritd
        18
    mritd   67 天前
    所以你到底是程序员还是 HR ?
    felix021
        19
    felix021   67 天前
    @mritd 我看着这么像 HR 吗……
    zxcslove
        20
    zxcslove   67 天前
    娓娓道来,兹瓷
    jinliming2
        21
    jinliming2   67 天前 via iPhone
    全家福 emoji 👨‍👩‍👧‍👦
    JoostShao
        22
    JoostShao   67 天前
    BS HR,HR SB
    这种广告文章居然不审核,废了
    找人就说找人,我感觉下面都是这个 HR 的小罗罗来点赞,发评论的,嘻嘻
    python30
        23
    python30   67 天前
    虽然一起在用 utf8mb4
    但是看了这文章更了解了
    bitdust
        24
    bitdust   67 天前   ❤️ 2
    @JoostShao
    这是我看到介绍 utf8 最好的一篇文章~而且是原创的,为什么要审核?
    hmxxmh
        25
    hmxxmh   67 天前 via Android
    第一个有遇到过,之前搞个人博客网站,富文本传表情的时候插入不进去
    reedthink
        26
    reedthink   66 天前
    为啥我的 MySQL 默认 uft8mb4,,,,
    felix021
        27
    felix021   66 天前
    @reedthink 可能新版本 MySQL 改了默认值?
    关于   ·   FAQ   ·   API   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   1739 人在线   最高记录 5168   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 17:23 · PVG 01:23 · LAX 10:23 · JFK 13:23
    ♥ Do have faith in what you're doing.