字符編碼演變史

開始記錄我的 2019-Read-Record 記錄一些有意思的知識點和疑難雜癥。

1 整理字符工作

有這么一幫人,他們對字節(jié),編碼等計算機概念一竅不通。他們的職責(zé)是對人類日常生活中用到字符和文字進(jìn)行歸納和整理:包含一組不重復(fù),無序元素的集合,如下:


我的字符集

此時集合包含10個元素,無序且無重復(fù)元素,為了更直觀且抽象的表示,他們采用兩種方式來表示:

編號 字符
1 p
2 m
3 s
4 t
5 學(xué)
6 習(xí)
7
8
9
10
… ... … ...

采用一維數(shù)字編號,查找關(guān)系為 1->p、2->m 以此類推.

區(qū)/位 1 2 3 4
1 p m s t
2 學(xué) 習(xí)
3 ... ...
4 ... ... ... ...

采用二維區(qū)位編號,查找需要一對數(shù)字(1,1)->p(1,2)->m 以此類推。

上述知識完全不涉及計算機知識,僅僅是集合中字符元素歸納整理的兩種方式罷了,之后我們談及更多的是一維數(shù)字編號方式。

2. 計算機背景下的字符整理

2.1 定長編碼

計算機底層無法存儲“p”,“m”...“碼”等字符,而采用一連串的0、1表示數(shù)據(jù),以字節(jié)(byte)為單位(00000000 - 11111111,16進(jìn)制表示為:0x00 - 0xFF)。因此計算機底層真正存儲的是字符編號,注意到計算機底層的編號索引是從0開始,所以我們需要對方式一編號稍作修改。

編號 編碼后計算機實際存儲 字符
1 0 p
2 1 m
3 2 s
4 3 t
5 4 學(xué)
6 5 習(xí)
7 6
8 7
9 8
10 9
… ... ... ... … ...

倘若計算機用一個字節(jié)存儲字符的編號,同時以一個字節(jié)解讀數(shù)據(jù),例如計算機底層的一串?dāng)?shù)據(jù)是“00000000 00000001 00000010 00000011 00000100 00000101 00000110 00000111 00001000”根據(jù)上表可以解讀為“pmst學(xué)習(xí)字符編碼”。

那么問題來了,一個字節(jié)最多可以表示256個字符。隨著源源不斷地往集合中加入新字符,編號不斷累加直到超過255,一個字節(jié)已經(jīng)無法滿足我們的需求。

因此我們改用兩個字節(jié)來表示字符編號,同時以兩個字節(jié)單位解讀數(shù)據(jù)。此時范圍為 0x0000 - 0xFFFF,容量為65536個字符。

盡管解決了字符容量問題,但是對于“m”這個字符我們需要用2個字節(jié)0x0001來表示,原先只需要一個字節(jié)0x01表示,底層存儲字節(jié)多了一倍。顯然這種并不是我們所期望的。

2.2 變長編碼

我們既要考慮集合字符的容量問題,又要減少不必要的字節(jié)浪費,因此引入了"變長編碼"概念,對于那些編號超過255的字符,我們保留兩個字節(jié)表示,而對于那些編號較小的字符,我們當(dāng)然希望使用1個字節(jié)存儲,而不是2個字節(jié)。

變長設(shè)計的核心問題自然就是如何區(qū)分不同的變長字節(jié),只有這樣才能在解碼時不發(fā)生歧義。

道理我都懂,但是按照表格映射關(guān)系解析計算機底層數(shù)據(jù)時,何時用一個字節(jié)解讀,何時又用兩個字節(jié)解讀呢?這是個棘手的問題!

答案是高位區(qū)分。

編號 編碼后計算機實際存儲 字符
1 0 p
65 64(0x40) x
128 127(0x7F) h
129 32768(0x80 00) e
130 32769(0x80 01) l
131 32770(0x80 02) o
… ... ... ... … ...

仔細(xì)觀察上述表格,集合中字符編號未變,但是在計算機中實際存儲規(guī)則變了,計算機現(xiàn)在知道如何區(qū)分用一個字節(jié)去解讀還是兩個字節(jié)。當(dāng)計算機讀到0x00-0x7F范圍的字節(jié)值,會直接按照表格映射關(guān)系去解釋,例如碰到0x40這個字節(jié),會直接解釋成“x”;而碰到0x80-0xFF范圍字節(jié)值,會先保留它不作解釋,會繼續(xù)讀入下一個字節(jié)后才進(jìn)行映射,例如“0x80 0x01”,先讀入0x80,計算機知道處于0x80-0xFF范圍,需要再讀入一個字節(jié)才能解釋,于是再次讀入0x01,合并后為0x8001,到表格映射關(guān)系中找到對應(yīng)的字符“l(fā)”。

總結(jié): 這里以一個簡單的示例解釋了變長編碼的概念。上面提到的簡單變長編碼方式弊端很多,首先可表示字符的范圍現(xiàn)在為0x00-0x7F 以及 0x8000 - 0xFFFF,硬生生地把0x80-0x7FFF這段可利用空間去除了。至于原因,請思考:倘若這段區(qū)間也做字符映射,那么計算機碰到 0x80 或是其他值,它還能區(qū)分用1個字節(jié)還是2個字節(jié)解釋嗎?

3 Unicode 字符集

還記得第一節(jié)說到專門有一幫人負(fù)責(zé)歸納整理世界上所有的字符嗎?即Unicode字符集,看下定義:

Unicode(統(tǒng)一碼、萬國碼、單一碼)是計算機科學(xué)領(lǐng)域里的一項業(yè)界標(biāo)準(zhǔn),包括字符集、編碼方案等。Unicode 是為了解決傳統(tǒng)的字符編碼方案的局限而產(chǎn)生的,它為每種語言中的每個字符設(shè)定了統(tǒng)一并且唯一的二進(jìn)制編碼,以滿足跨語言、跨平臺進(jìn)行文本轉(zhuǎn)換、處理的要求。1990年開始研發(fā),1994年正式公布

現(xiàn)在來說說這個字符集的容量有多少。按照之前一維數(shù)字編號,目前范圍是0x0000 - 0x10FFFF(21位 bits)。這里要引入“平面”這一概念,一個平面能放置65536(0xFFFF)個字符。因此Unicode字符集可以劃分為17個平面:

平面號 平面范圍
Plane0 —— Basic Multilingual Plane 基本多語言平面 0x0000-0xFFFF
Plane1 —— 后續(xù)16個平面統(tǒng)稱為SP Supplementary Planes) 0x10000-0x1FFFF
Plane2 0x20000-0x2FFFF
... ...
Plane15 0xF0000-0xFFFFF
Plane16 0x100000-0x10FFFF

用圖表示:

Unicode 字符集平面

第一個平面即是BMP(Basic Multilingual Plane 基本多語言平面),也叫Plane 0,它的碼點范圍是U+0000~U+FFFF。這也是我們最常用的平面,日常用到的字符絕大多數(shù)都落在這個平面內(nèi)。

上圖彩色的平面即 BMP,范圍為0x0000 - 0xFFFF,但我們并沒有“塞滿”這個平面,其中部分位置用于保留,換句話說,BMP的容量只有6萬多個字符,想象一下把這些字符整合起來放在一起,密密麻麻!GNU Unifont就制作了一張這樣的圖片。見http://unifoundry.com/pub/unifont-7.0.03/unifont-7.0.03.bmp 打開可能會費一點時間。

前面說到BMP是我們最常用的平面,日常用到的字符絕大多數(shù)都集中在這里,而對于中文來說,我們常用[\u4E00-\u9FA5]正則表達(dá)式來匹配,其實也就是指定中文在Unicode字符集中的編號范圍0x4E00 - 0x9FA5。其實稍加計算就知道這個范圍的容量不過兩萬多點,中文顯然不止這個數(shù)。其實9FA5后面還有不少的漢字,它們中間又還夾雜著一些符號,所以想正確地表示Unicode中的漢字還是個不小的挑戰(zhàn)。

這里引用一段:應(yīng)該說,Unicode處在不斷發(fā)展中,它有一百多萬的空間,目前也只是定義了十萬左右的字符,還會不斷增加,漢字自然也有可能增加,所以漢字的范圍實際上是動態(tài)的,變化的。當(dāng)然了,常用的基本落在了這一范圍內(nèi),而事實上已經(jīng)包含了許多的不常用漢字,畢竟連只有6千多字的GB2312中都含有大量的不常用漢字。在要求不那么嚴(yán)格的應(yīng)用中,按以上范圍去判斷基本也OK,而“漢字”這一概念實際上也沒有準(zhǔn)確定義,比方說上圖中一些“偏旁部首”,這些是“漢字”嗎?

平常我們看到諸如 \uU+ 緊跟十六進(jìn)制數(shù)字,其實就是Unicode字符集中字符的編號了,例如 \u4E00 其實就是對應(yīng) Unicode 字符集中的漢字“一”。再次強調(diào)這里其實不涉及計算機知識,而是單純的歸納整理的方式而已:一維數(shù)字編號。

現(xiàn)在說說計算機底層如何存儲這個編號,是直接存儲0x4E00嗎,還是找到一種映射關(guān)系,將0x4E00處理成其他數(shù)據(jù)后再存儲到計算底層里,這里涉及的東西很多,下文會一一解答。

4 UTF - Unicode 轉(zhuǎn)換格式

至此,我們對Unicode字符集有了初步的了解,再次強調(diào)一下:即使對字節(jié),編碼等計算機概念一竅不通此時是沒有任何關(guān)系的!正如前面多次強調(diào),字符集就是收集日常所用的所有字符,然后將其歸納整理,比如Unicode這個組織用數(shù)字對每個字符編號,將世界上所有文字和符號的字符收錄,從0開始編號,一直到0x10FFFF(強調(diào):21位 bits)。想象下,有一張兩列的表,左邊是編號,右邊是對應(yīng)的字符,看到這你可能脫口而出:一一對應(yīng)關(guān)系。即得到編號0去查表,就知道對應(yīng)的字符是NULL;同理,拿“a”字符去查索引,應(yīng)該是0x0041(十進(jìn)制的65)。

編號 字符
0 NULL
... ...
0x10FFFF 未知...

再換種表示形式,本質(zhì)其實是一樣的:

3.png

更多時候 Unicode 字符集中的字符表示形式應(yīng)該有兩種:U+[xxxx] 和 \u[xxxx],其中x代表十六進(jìn)制數(shù)字。等等!Uncode字符集范圍不是到21位的 0x10FFFF嗎,怎么這里只有4個x? 難道是因為常用的都在第一平面BMP,它的范圍是0x0000-0xFFFF?如果你已經(jīng)開始這么思考,恭喜你,差不多已經(jīng)理解我要表達(dá)的意思了。

4.1 UTF-32

計算機底層并非直接存儲"a","1","丁"等字符,而是先轉(zhuǎn)變成另外一種形式后再存儲。首先想到的且最簡單的:將編號以二進(jìn)制存儲。如下:

編號 編碼后的索引 字符
0 0 NULL
... ... ...
0x10FFFF 0x10FFFF 未知...

編碼后的索引和之前歸納整理的編號一模一樣,編碼或者說映射關(guān)系其實很簡單:

int map(int idx){
  return idx;
}

早期字符數(shù)量還沒有現(xiàn)在這么龐大,使用2個字節(jié)存儲編號足矣(0x0000-0xFFFF)。但隨著新字符的加入,字符數(shù)量已經(jīng)超出了上限65536。既然2個字節(jié)不夠表示,解決方法很簡單:4個字節(jié),即 UTF-32 編碼。

嘗試下,創(chuàng)建一個文件并寫入內(nèi)容“1a”,然后以 UTF-32 編號格式存儲,接著以16進(jìn)制查看這個文件內(nèi)容(你可以選擇可視化工具,或者使用hexdump命令查看),最后看到的應(yīng)該是0x00000031 0x00000061;現(xiàn)在從計算機讀取內(nèi)容,即``0x00000031 0x00000061`,編輯器每讀入4個字節(jié)開始解碼,查表得到相應(yīng)的字符最后呈現(xiàn)給我們,這也是最終我們看到的,這里用到的是定長編碼方式。

思考:既然 UTF32 存儲方便容易理解,為何不一用到底?干嘛還要UTF8-16,UTF-8呢?

其實原因前面已經(jīng)提及過一次:當(dāng)存儲內(nèi)容是英文字符或是其他較小編號時,每個字符都要使用4個字節(jié)來存儲,太過浪費,利用率極低!

為此才有了變長編碼 UTF-16 和 UTF-8 。

4.2 UTF-8

UTF-8是變長的編碼方案,可以有1,2,3,4四種字節(jié)組合。在前面的定長與變長篇章我們提到UTF-8采用了高位保留方式來區(qū)別不同變長,如下:

0XXXXXXX                              有效編碼位:7 bits
110XXXXX 10XXXXXX                     有效編碼位:11 bits
1110XXXX 10XXXXXX 10XXXXXX            有效編碼位:16 bits
11110XXX 10XXXXXX 10XXXXXX 10XXXXXX   有效編碼位:21 bits

一問:為何使用 0,110,111011110來作為區(qū)分?使用1,11,1111111可以嗎?
一答:顯然使用后者無法區(qū)分,比如讀入第一個1,你無法確定是一個字節(jié)數(shù)據(jù);繼續(xù)讀入一個1,你還是無法確定是2個字節(jié)數(shù)據(jù),同理,讀入111 也一樣無法確定,但是讀入1111就可以確定是四個字節(jié)數(shù)據(jù)了,至于前面的,我們必須讀到0才可以確定幾位數(shù),比如110,我們才知道是兩個字節(jié)數(shù)據(jù),但是這樣使得有效數(shù)據(jù)位變少,因此我們還是打算以0,110,111011110區(qū)分。
二問:為何第二位起使用固定的 10?
二答:倘若我們使用0,10,1101110區(qū)分讀入幾個字節(jié),講道理之后可以不用保留位啊,直接讀 XXXX XXXX不是利用率更高嗎? —— 我個人認(rèn)為計算機可以從任何一個字節(jié)讀入,萬一從某個XXXX XXXX讀取時,恰巧這個數(shù)又是以110或者1110打頭的,那一旦開始就讀錯,之后就是連鎖反應(yīng),全部都解釋錯誤了。所以我們對于后面的字節(jié)也需要保留位,至于10,應(yīng)該是為了增加可利用位數(shù)吧,總不能用11110來作為保留位吧。

如上,0,10,110這些都是保留的固定位,X表示是有效編碼位。單字節(jié)最高位都是0,多字節(jié)的最高位都是1.
針對多字節(jié)來講,我們可以稱之為N(N > 1)字節(jié)模式,首字節(jié)以“N個1再加0”打頭,后跟“N-1”個以“10”打頭的字節(jié)。

說完用 UTF-8 編碼后的四種計算機底層存儲形式,再來反向說說讀取過程。計算機每次讀入一個字節(jié),以高位來區(qū)分是用1個字節(jié)解碼,還是2,3,4字節(jié)解碼。

重點來了:我們實際真正要用的是有效編碼位,也就是X,UTF-8 編碼就是從這些二進(jìn)制數(shù)據(jù)中提取有效編碼位,組合得到一個新的十六進(jìn)制值作為索引去Unicode字符集中查找對應(yīng)字符?!?就是這么簡單!

  1. 一字節(jié)有效編碼位有7位,2^7=128,可以表示字符集區(qū)間 U+0000U+007F(0127)。

一字節(jié)留給了ASCII,所以UTF-8兼容ASCII。

  1. 二字節(jié)有效編碼位只有5+6=11位,最多只有2^11=2048個編碼空間,所以數(shù)量眾多的漢字是無法容身于此的了。字符集區(qū)間 U+0080U+07FF(1282047)使用二字節(jié)。

注意:這里編號范圍是128~2047,因為去掉了一字節(jié)的碼點(因為下限是U+0080),所以不會占滿2048個編碼空間,是有冗余的,但你不能把適用于一字節(jié)的碼點放到這里來編碼。下同。
這個需要解釋下為什么范圍設(shè)定了U+0080~U+07FF 其實可以看下編號后的字符集 U+D800~U+DFFF 是空的!要用作代理區(qū),其實吧就是為了不影響之后的解碼沖突。

  1. 三字節(jié)模式可看到光是保留位就達(dá)到4+2+2=8位,相當(dāng)一字節(jié),所以只剩下兩字節(jié)16位有效編碼位,它的容量實際也只有65536。碼點U+0800U+FFFF(204865535)使用三字節(jié)編碼。

我們前面說到,一些漢字字典收錄的漢字達(dá)到了驚人的10萬級別?;旧?,常用的漢字都落在了這三字節(jié)的空間里,這就是我們常說的漢字在UTF-8里用三字節(jié)表示。當(dāng)然了,這么說并不嚴(yán)謹(jǐn),如果這10萬的漢字都被收錄進(jìn)來的話,那些偏門的漢字自然只能被擠到四字節(jié)空間上去了。

  1. 四字節(jié)的可以看到它的有效位是3+6+6+6=21位,前面說到最大的碼點10FFFF也是21位,U+FFFF以上的增補平面的字符都在這里來表示。

按照UTF-8的模式,它還可以擴展到5字節(jié),乃至6字節(jié)變長,但Unicode說了碼點就到10FFFF,不擴充了,所以UTF-8最多到四字節(jié)就足夠了。

下面演示如何將漢字”你“(U+4F60)編碼存儲到計算機底層,來自字符集與編碼一文,強烈推薦!

上圖顯示了一有效位為 15 位的碼點到三字節(jié)轉(zhuǎn)換的一個基本原理,我們還可看到原來4F60 中的一頭一尾的兩個 4 和 0 在轉(zhuǎn)換后還存在于最終的三字節(jié)結(jié)果中。UTF-8 三字節(jié)模式固定了 1110 的開頭模式,所以多數(shù)漢字總是以 1110 開頭,換成 16 進(jìn)制形式,1110 就是字母 E。

如果看到一串的 16 進(jìn)制有如下的形式:EX XX XX EX XX XX…每三個三個字節(jié)前面都是 E 打頭,那么它很可能就是一串漢字的 UTF-8 編碼了。

其它變長字節(jié)轉(zhuǎn)換道理也類似,其中分組從低位開始,高位如不足則補零。這里就不再示例了。

UTF-8 編碼的誤解:首先并非將 Unicode 字符集編號直接存儲到計算機底層,其次前文已經(jīng)說過按照高位來區(qū)分讀取多少個字節(jié)(四種情況=1個字節(jié) - 0xxxxxxx,2 - 110xxxxx,3 - 1110xxxx,4 - 11110xxx),但是對于讀到的數(shù)據(jù)并非一定是字符在 Unicode 字符集中的編號,我們需要通過提取有效編碼位并組合成新的索引,才去字符集中查詢。

小結(jié):UTF-8 變長編碼方案很好地幫我們解決了字節(jié)利用率的問題,在 UTF32 編碼方案里,字符"a"底層存儲4個字節(jié) 0x00000041。而改用 UTF8 只需要一個字節(jié)。當(dāng)然嘍,正如上面演示的 UTF-8編碼方式,比UTF-32編碼f(x){return x}的要復(fù)雜一些。

另外我們也能注意到,對于U+0800~U+FFFF范圍內(nèi)(即 BMP 平面)的中文字符,上文說了用 UTF-8 編碼需要三個字節(jié),但是中文顯然不可能只有6萬多個字,那些偏門的中文在Unicode字符集中的編號都是在 U+FFFF 之上了,顯然用 UTF-8 編碼要用四個字節(jié)。

4.3 UTF-16

UTF-8 作為變長編碼方案對于 U+0000~U+07FF 范圍字符的字節(jié)利用率是值得肯定的,而對于 BMP 中U+0800~U+FFFF 確要用3個字節(jié),至于大于U+FFFF的通通都是4個字節(jié),有沒有在這方面更好的方案呢?

有,就是作為壓軸出場的UTF-16,它是一種變長的2或4字節(jié)編碼模式。對于BMP內(nèi)的字符使用2字節(jié)編碼,其它的則使用4字節(jié)組成所謂的代理對來編碼。這是如何做到的呢? 且聽我一一道來。

還記得GNU Unifont就制作了一張Unicode字符集成員的圖片嗎?地址:http://unifoundry.com/pub/unifont-7.0.03/unifont-7.0.03.bmp ,請找到D8行至DF行,這里不對應(yīng)任何字符,是一塊空白區(qū)域,也就是所謂的代理區(qū)(Surrogate Area),在Unicode字符集中的范圍是U+D800 ~ U+DFFF(容量為2048),如果以U+DBFF為中心點,將其一分為二,也就有了高代理區(qū)(D800–DBFF)和低代理區(qū)(DC00–DFFF)兩部分,各占1024。下面請回想第一章的二維表示方式,如果行列都有1024,那么這張表格就有1024*1024=16*65536= 16 * 0xFFFF,我們的一個平面容量為0xFFFF,所以它恰好可以表示增補的16個平面中的所有字符。 這張表格如下:

U+0800~U+DFFF 之所以不對應(yīng)字符,是因為以代理方式存儲到內(nèi)存中時,只要遇到字節(jié)在這個范圍內(nèi)的 我們就知道需要進(jìn)行代理轉(zhuǎn)換了!計算得到0xFFFF編號之上的字符了 也就是那16個補充平面。Unicode 字符集編號顯然也會為了編碼適當(dāng)做出調(diào)整,比如這里為了代理映射,我們將U+0800~U+DFFF這段區(qū)域保留了,不映射字符了。

再次強調(diào):UTF-16 是一種變長的2或4字節(jié)編碼模式,我們將 Unicode 字符集劃分2個范圍來討論:

  1. 字符集區(qū)間 U+0000~U+FFFF:計算機以2個字節(jié)為單位讀取底層數(shù)據(jù),如果讀到的兩個字節(jié)不在代理區(qū)的范圍(U+D800 ~ U+DFFF),那么就直接當(dāng)做編號直接去Unicode 字符集中查詢,例如0x0041查詢到的是”a“,0x4F60查詢到的是”你“ —— 還記得UTF-8是編碼成3個字節(jié)存儲和讀取解碼的嗎?

  2. 字符集區(qū)間 U+10000~U+10FFFF,即16個增補平面:計算機同樣以2個字節(jié)為單位讀取底層數(shù)據(jù),如果讀到的兩個字節(jié)高代理區(qū)的范圍(D800–DBFF),接著再次讀取2個字節(jié),一定要是低代理區(qū)(DC00–DFFF)。這樣2+2 組成一個代理對(Surrogate Pair)查詢對應(yīng)的字符。注意必須是這種先高后低的順序,如果出現(xiàn)兩個高,兩個低,或者先低后高,都是非法的。

UTF-16的例子其實很簡單,核心其實不過是上面的那種二維表格,(D800,DC00)對應(yīng)了 Unicode 字符集中編號為U+01000的字符;(DBFF,DFFF)對應(yīng)了 Unicode 字符集中編號U+10FFFF的字符。就是這么簡單。

5 BOM

reserved

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容