33
And the Lord spake, saying, "First shalt thou take out the Holy Pin. Then, shalt thou count to three. No more. No less. Three shalt be the number thou shalt count, and the number of the counting shall be three. Four shalt thou not count, nor either count thou two, excepting that thou then proceed to three. Five is right out. Once the number three, being the third number, be reached, then, lobbest thou thy Holy Hand Grenade of Antioch towards thy foe, who, being naughty in My sight, shall snuff it."

上帝说:『首先取下栓,然后不多不少数到三。应该数到三,你数到的数字是三。你除了数到三,既不要数到四,也不要数到二,五是数多了。「三」一旦被数到,成为被数到的第三个数字,就高高的向敌人扔出安提拉之神圣手榴弹,阿门。』
—— 巨蟒与圣杯 Monty Python and the Holy Grail (1975)

UTF-8的来历

clipboard.png

UTF-8的规范里充斥着这样神秘的句子:“第一个位元组由110开始,接着的位元组由10开始”,“第一个位元组由1110开始,接着的位元组由10开始”。

那么这到底是什么意思呢?为什么要这么做呢?

我们先从二进制说起。我们都知道,一个字节是由8个二进制位构成的,最小就是0000 0000,最大就是1111 1111。那么一个字节所能表示的最多字符数就是28次方,也就是256。对于26个英文字母来说,大小写全算上就是52个,再加上10个阿拉伯数字,62个字符,用可以表达256个不同字符的一个字节来存储是足够了。

但是,我们中国的常用汉字就有3000多个,用一个只能表达256个字符的字节显然是不够存储的。至少也需要有2个字节,1个字节是8个二进制位,2个字节就是16个二进制位,最多可以表达216次方,也就是256*256=6553665536个字符足够容纳所有中国的汉字,外带日语、韩语、阿拉伯语、稀其古怪语等等各种各样的字符。所以这样就产生了Unicode,因为它用2字节表示字符,所以更严格来讲应该叫UCS-2,后来因为怪字符太多,2字节都不够用了,所以又搞出来了一个4字节表示的方法,称作UCS-4。不过现在对绝大多数人来讲UCS-2已经是足够了。

Unicode本来是一个好东西,用2字节表示65536种字符,全人类皆大欢喜的事情。但是偏偏有一帮子西洋人,非要认为这个东西是一种浪费,说我们英文就最多只需要26个字母就够了,1个字节就够了,为什么要浪费2字节呢?比如说字母A就是0100 0001,这一个字节就够了的东西,你弄2字节,非要在前面加800000 0000 0100 0001,这不是浪费吗?我们就偏要用1字节表示英文。

好吧,我们全人类只好做妥协,规定每个字节,只要看见0打头的,就知道这是英文字母,这肯定不是汉字,只有看见1开头的,才认为这是汉字。

但是我们汉字用1个字节表示不下,那好办,用21开头的字符表示1个汉字。这样本来16个二进制位,减去2个开头的1,只剩下14个二进制位了,214次方就是16384个字符,对于中文来讲,也是足够用了。但是无奈他们还是想表达65536种字符,那怎么办呢?就需要3个字节才能容纳得下了,于是UTF-8粉墨登场。

首先,首位为0的字符被占了,只要遇到0开头的字符,就知道这是一个1字节的字符,不必再往后数了,直接拿来用就可以,最多表示128种字符,从0000 00000111 1111,也就是从0127

接下来的事情就比较蹊跷了。我们怎么用1开头的字符既表示2字节,又表示3字节呢?假设我们只判断首位的1,这显然是不行的,没有办法区分,所以我们可以用10或者11开头的字符来表示2字节,但是3字节又该以什么开头?或者可以用10开头表示2字节,用11开头表示3字节?那么4字节的字符将来又该怎么办?也许我们可以用110开头表示3字节,用111开头表示4字节?那么5字节6字节呢?似乎我们看到了一个规律:前面的1越多,代表字节数越多。

这时候,看一下我们的第一种方案:用10开头表示2字节,那么我们的一个字符将是

10xx xxxx 10xx xxxx

110表示3字节,那么一个3字节的字符将是:

110x xxxx 110x xxxx 110x xxxx

这样无疑是能区分得开的。但是4字节怎么办?

1110 xxxx 1110 xxxx 1110 xxxx 1110 xxxx

吗?这样也能区分开,但似乎有点浪费。因为每个字节的前半扇都被无用的位占满了,真正有意义的只有后面一半。

或者我们干脆这样做得了,我们来设计方案二:为了节省起见,所有后面的字符,我们统统都以10开头,只要遇见10我们就知道它只是整个字符流的一部分,它肯定不是开头,但是10这个开头已经被我们刚刚方案一的2字节字符占用了,怎么办?好办,把2字节字符的开头从10改成110,这样它就肯定不会和10冲突了。于是2字节字符变成

110x xxxx 10xx xxxx

再往后顺推,3字节字符变成

1110 xxxx 10xx xxxx 10xx xxxx

4字节字符变成

1111 0xxx 10xx xxxx 10xx xxxx 10xx xxxx

好像比刚才的方案一有所节省呢!并且还带来了额外的好处:如果我没有见到前面的110或者1110开头的字节,而直接见到了10开头的字节,毫无疑问地可以肯定我遇到的不是一个完整字符的开头,我可以直接忽略这个错误的字节,而直接找下一个正确字符的开头。

这个改良之后的方案二就是UTF-8

UTF-8表示的字符数

现在,我们来算一下在UTF-8方案里,每一种字节可以表示多少种字符。

1字节的字符,以0开头的,0xxx xxxx,后面7个有效位,27次方,最多可以表示128种字符。

2字节的字符,110x xxxx 10xx xxxx,数一数,11x,所以是211次方,210次方是102411次方就是2048,很不幸,只能表示2048种字符,而我们的常用汉字就有3000多个,看来在这一区是放不下了,只好挪到3字节。

3字节的字符,1110 xxxx 10xx xxxx 10xx xxxx,数一数,16x216次方,最多可以表示65536个字符,所以我们的汉字就放在这一区,所以在UTF-8方案里我们的汉字都是以3个字节表示的。

所以这也就是这一张表的来历:

clipboard.png

UTF-8和UTF-16

那么UTF-88是从哪儿来的呢?它的意思就是说我们以28次方为一个字节,为一个最小单元。那么如果我们以216次方为一个最小单元,这就变成了UTF-16,它的规则和UTF-8相同,唯一不同的是它最小也要用162进制位表示一个字符,而162进制位直接可以表示65536种字符,所以在UTF-16方案里,我们汉字直接就可以如英文一样被堂而皇之地放在第1区了,也就是说,和英文具有同等的身份,都占用162进制位,也就相当于UTF-8里的2字节哦,看,这样一来,如果我们用UTF-16来存储英文的话,会造成浪费,因为英文在UTF-8里只占1字节,而在UTF-16里要占2字节,但是如果我们用UTF-16来存储中文的话,不但不浪费,反而还节省了呢!因为我们的中文在UTF-8里要占用3字节,而在UTF-16里只占用2字节,节省了33%之多呢!


如果觉得我的文章对你有用,请随意赞赏
14 条评论
周梦康 · 1月5日

比我写的好,我昨天也写了一个类似的 https://mengkang.net/1129.html

+1 回复

2

不好这么讲,大家是从不同的角度谈问题。

张京 作者 · 1月5日
CarterLi · 1月7日

所以关键就是看你储存的英文字符多还是汉字多了

+1 回复

秋之 · 1月9日

首先赞一个~(≧▽≦)/~
文中有一处多了一个X
再往后顺推,3字节字符变成
1110x xxxx 10xx xxxx 10xx xxxx 这里应该是1110 xxxx 10xx xxxx 10xx xxxx

+1 回复

1

感谢指正!

张京 作者 · 1月9日
garfileo · 1月4日

不过 UTF-16 对西文来说,可能浪费会更剧烈。一个字母俩字节,随便一个英文单词所占用的字节数,要比一个汉字多了好多倍。

回复

cuzfinal · 1月4日

好的,知道了。

回复

h_Davy · 1月10日

想知道开启 HTTP 协议 GZip 压缩后,两者传输数据量的比较。
毕竟 UTF-8 意味着更大的字符集,能兼容更多的信息。

回复

代码宇宙 · 1月10日

文章不错,浅显易懂。但是对 Unicode 的理解有些问题。Unicode 和 UTF-8 不是等价之物,UTF-8 并不是为了弥补 Unicode 的不足而发明的。

回复

刀尖红叶 · 1月11日

英文在UTF-8里只占1字节,而在UTF-16里要占2字节,浪费100%;
中文在UTF-8里要占用3字节,而在UTF-16里只占用2字节,节省了33%;
综合考虑还是UTF-8节省!

回复

v1_alpha · 1月12日

有的中文是需要2个UTF-16,一个UTF-16不够用

回复

yooye · 1月17日

UTF-8想要偏移到指定个数,只能从头开始,感觉不是什么好的设计

回复

新しい世界 · 3月13日

中文字,比如"龥", 码点是 0x9fa5
转换成二进制是
1001 1111 1010 0101
所以其 utf-8 表示应该是

xxxx 1001 xx11 1110 xx10 0101
1110 xxxx 10xx xxxx 10xx xxxx
---------
1110 1001 1011 1110 1010 0101

e9 be a5
试了一下,确实是这样 ?

回复

新しい世界 · 3月13日

有一个问题,如果一串字符储存在文本文件中(无BOM),如何识别储存的文本是 uf8 格式还是 utf16 格式的呢?

回复

载入中...