我們知道,在計算機內部,所有的資訊都是以二進位制形式進行儲存。無論是字元,或是視訊音訊檔案,最終都會對應到一串由 0 和 1 構成的數字串。所以從我們能看懂的人類資訊轉變為機器級別的二進位制語言的過程就可以理解為一種編碼的過程,自然,相反的過程就是所謂的解碼的過程。
可以這麼說,所有的亂碼都是源於解碼方式與編碼方式的不一致。就好像我用英文給你寫了一封信(我要表達的資訊用英文這種方式 [編碼] 了),而你只懂中文,你用中文去讀信的內容(用中文 [解碼]),於是整封信在你看來就是所謂的 [亂碼]。其實,所謂的亂碼不是什麼複雜的問題,僅僅就是解碼的方式不同於編碼的方式而已,只要換成合適的解碼方式就好了。
本文根據計算機編碼的演變歷史,從最早的 ASCLL 編碼,到一統編碼界的 Unicode 編碼方式,探討一下我們的 [人類資訊] 究竟是如何被編碼成 [計算機級資訊] 的。
一、始終被相容的 ASCLL
上個世紀中旬,美國人發明了計算機,當時並沒有考慮到計算機的普及程度會如此之快,所以當時美國人只制定了英文字元和一些控制字元與二進位制之間的對映標準,這個最初的標準就是 ASCLL 編碼標準。
ASCLL 首先對所有需要編碼的字元進行了一個編號(總共編排了 128 個字元),例如:數字 0 的編號是 48,字母 a 的編號是 97 等。於是 ASCLL 使用一個位元組(8 個位元位)來描述這些字元,將他們各自的編號的十進位制轉換成二進位制即可。於是從 00000000 -- 01111111 (0-127)都被編排了字元。所以,所有采用 ASCLL 編碼標準的檔案在解析的時候,每八位二進位制一起被解釋成一個字元,這樣所有的英文字元、數字、其他一些字元都已經可以被儲存被讀取了。下面附一張經典的 ASCLL 表:
可見,雖然一個位元組只用了七個位元位,但是包含的字元還是相當多的,對於美國人來說,這完全足夠用了,但是對於一些歐洲國家,乃至我們偉大的中國來說,一個位元組實在是太少了,於是很多地區國家就有了自己的擴充套件編碼標準,但無一例外的相容 ASCLL 編碼(畢竟人家是鼻祖)。
二、Windows-1252 來自歐洲人的擴充套件
美國人的 ASCLL 標準只定義了 128 個字元的編碼方式,使用了 00000000 -- 01111111 這個區間段的二進位制。於是歐洲人直接使用 10000000 -- 11111111(128-255)區間段的 127 個二進位制位來定義他們自己的一些符號。
三、GB2312 來自中國人的擴充套件
我偉大的中華民族有著成千上萬的漢字,美國人的一個位元組的編碼標準怎麼能好使? GB2312 (國家標準編碼)主要針對的是我們日常中經常使用的一些簡體中文,總共收錄 6763 個漢字,採用雙位元組編碼,向前相容 ASCLL 標準。
那麼有一個問題,ASCLL 標準的字元采用的一個位元組進行編碼方式,而我們的中文漢字採用的兩個位元組進行編碼,計算機在解碼的時候究竟是一次讀取一個位元組並把它按照 ASCLL 標準解析成一個字元,還是一次讀取兩個位元組並把它按照我們的 GB2312 標準解析成一個漢字呢?
GB2312 規定,編碼漢字的兩個位元組中,第一個位元組的最高位必須為 1。這樣,由於 ASCLL 標準的所有字元(00000000-01111111),最高位都是 0,所以當計算機讀取到某個位元組的最高位為 1 的時候,就連著讀取兩個位元組按照 GB2312 標準解析為一個漢字,否則則認為這是一個普通字元並按照 ASCLL 將它解析為一個普通字元。
下面我們簡單描述一下 GB2312 的具體編碼細節:
首先,GB2312 是通過所謂的 [分割槽] 來編排每一個漢字的。
- 01-09 區編排了一些特殊字元
- 16-55 區編排了一級常用漢字
- 56-87 區編排了二級常用漢字
- 00-15 區及 88-94 區則未有編碼
GB2312 的編碼方式:0xA0 + 區號,0xA0 + 位號。例如:[楊] 的區位號是 4978(49 區 78 位),所以楊的 GB2312 編碼為:0xA0 + 49 ,0xA0 + 78 ,即:D1EE。所以以前有一種區位輸入法,就是通過輸入四位的數字來進行打字的,而這四位數字就是該漢字的區位號。至於為什麼要在區號位號加 0xA0 ,查了很多資料,沒有明確的說法,可能就是一種規定吧。
其實仔細想一下,所謂的編碼過程不就是兩個步驟的組合麼,理解這一點很重要。
- 使用唯一的一個標識編排表示該字元
- 制定統一的規則將標識對映為底層二進位制
ASCLL 標準如此,GB2312 也是如此。
例如:ASCLL 為所有字元進行編號,並且相互不重複(第一步),然後制定了一個規則,某個字元編號的二進位制就是它的字元編碼(第二步)。
例如:GB2312 為所有的漢字進行分割槽編號,相互不重複(第一步),然後制定規則使得可以通過區位號得到該漢字的二進位制字元編碼(第二步)。
GBK 向下相容並擴充套件了 GB2312 ,收錄了 21003 個漢字,依然是採用的固定兩個位元組來編碼漢字,只是高位位元組的取值範圍不同而已,此處不再贅述。
四、有雄心的 Unicode
上面我們介紹了美國人的編碼標準、歐洲人的編碼標準、中國人的編碼標準,當然這只是冰山一角,世界上存在著各種各樣的編碼標準。每個國家的計算機廠商都要根據不同的地域使用不同的編碼標準來生產計算機,繁瑣低效。有沒有一種編碼標準能收錄世界上所有的字元,並提供儲存實現呢?
Unicode 的誕生就是為了統一世界上所有編碼的,它編排了世界上近乎所有的字元,總共收錄將近 110 多萬個字符集合,編號範圍從 0x000000 到 0x10FFFF。但大多數字符在範圍:0x0000 到 0xFFFF 之間(即小於 65536),每個字元都有一個 Unicode 編號並且一般用十六進位制表示,前置 U+。例如:[楊] 的 Unicode 表示為:U+6768。
Unicode 是一種編碼標準,它只是為世界上的所有字元進行了編號,並沒有指定每個字元每個編號該如何對映為某個二進位制串,而 Unicode 的主要實現者有:UTF-32,UTF-16 和 UTF-8。下面,我們分別來看看這些實現者的具體實現細節。
1、UTF-32
這是一種最粗暴的實現方式,採用固定四個位元組儲存單個字元,所有的字元都使用四個位元組進行儲存,空間浪費,實際使用中很少採用。
2、UTF-16
針對 Unicode 的儲存實現來說,應當遵循一個基本的理念:越常用的字元應當使用越少的位元組數表示,而越少見的字元才應該用最多的位元組數進行表示。下面我們看看 UTF-16 的具體實現細節:
Unicode 的編碼範圍從 0x000000 - 0x10FFFF,總共可以編排 1,112,064 個字元。UTF-16 的策略是,編號範圍 0x00000 - 0x10000(0-65532)屬於常用字元,採用固定的兩個位元組儲存。其中,字元所對應的二進位制數值就是該字元本身編號的二進位制字面量值。但是,其中 0xD800 到 0xDFFF 編號區間沒有編排任何字元,這個區間將用於後續的增補字集編碼,這裡暫時先不說。
可見,對於常用的字元來說,採用兩個位元組進行編碼,但是不常用不代表用不到,我們接著看看那些增補字符集,也就是所謂的不常用字符集是如何編碼的。
對於編號範圍 0x10000 - 0x10FFFF 之間的字元來說,UTF-16 使用固定的四個位元組進行儲存,但是你會發現 0x10000 - 0x10FFFF 之間總共有 FFFF 個字元,即 2^20=1,048,576 個字元,也就是需要 20 個位元位才能編碼這麼多字元。所以,我們的四個位元組裡,前兩個位元組共 16 位至少要提供 2^10(111...111,十個一)種可能,後兩個位元組也要提供 2^10 種可能,才能組合編排所有的增補字符集。
但是,現在有一個問題:一串二進位制數值,我如何判斷某個字元是常用字元(使用固定的兩個位元組儲存的),或是增補字元(使用四個位元組儲存的)?
UTF-16 的解決辦法如下:
每個 Unicode 字元都有一個自己的 Unicode 編號,並且對於增補字元來說,他們的編號都大於 0x10000 。用字元本身的編號減去 0x10000 即可得到該字元在所有增補字符集中的排列序號。這個序號的值必然位於範圍:0x00000 - 0xFFFFF 之間,佔 20 個位元位 ,因為剩下的增補字元數目不會超過 0xFFFF 個。
對於前 兩個位元組(維基百科上稱做前導代理),定義他們的取值範圍:0xD800(0xD800 + 0x0000)到 0xDBFF(0xD800 + 0x3FF [10 個 1]),剛好提供了 2^10 種可能取值。
對於後 兩個位元組(維基百科上稱做後尾代理),同樣定義了他們的取值範圍:0xDC00(0xDC00 + 0x0000)到 0xDFFF(0xDC00 + 0x3FF [10 個 1]),也剛好提供了 2^10 種可能取值。
所以,如果發現前兩個位元組的二進位制數值位於範圍 0xD800 到 0xDBFF 之間,則說明這個字元屬於增補字元並且在編碼的時候採用四個位元組固定儲存了,依次讀取四個位元組即為當前字元的二進位制數值。否則,則說明這是一個由兩個固定位元組儲存的基本常用字元,依次讀取兩個位元組就好了。
下面看幾個示例:
1、Unicode 編號 U+0024 的字元
首先,判斷得知該編號小於 0x10000,該字元隸屬於普通常用字符集,所以該字元的 UTF-16 編碼值就是其本身的編號二進位制形式。
2、Unicode 編號 U+24B62 的字元
首先,判斷該字元的編號值是大於 0x10000 的,說明該字元隸屬於增補字符集。
於是,用 0x24B62 減去 0x10000 得到該字元在增補字符集中的排序:0x14B62 。
通過 UTF-16 編碼標準,得到前導代理和後導代理,組合後就是該字元的 UTF-16 編碼。以下是計算過程:
0x14B62 -> 0001 0100 1011 0110 0010
前導代理項:0001 0100 10 + 0xD800 = 0xD852
後尾代理項:11 0110 0010 + 0xDC00 = 0xDF62
所以,U+24B62 字元的 UTF-16 編碼為:0xD852 DF62
總結一下 UTF-16 的編碼標準,對於編號小於 65536 的字元,採用固定兩個位元組以編號的二進位制作為編碼的值。對於增補字符集(編號大於 65536),首先拿本身的 Unicode 編號減去 65536 得到當前字元在增補字符集中的排列序號,接著分出兩個代理項並加上特定的數值,使得他們各自位於特定的範圍中,並以此來區分某個字元究竟是兩個位元組儲存的還是四個位元組儲存的。
3、UTF-8
UTF-8(8-bit Unicode Transformation Format),是一種針對 Unicode 的可變長度字元編碼。使用一到四個位元組來編碼 Unicode 字元,最常用的字元使用最少的位元組數進行儲存,很少用的字元使用相對多一點的位元組數進行儲存。
UTF-8 的編碼規則如下圖所示:
對於編號小於 127 的字元來說,UTF-8 編碼標準等同於 ASCLL 編碼標準。
對於其餘編號範圍,按照如圖中所示的格式進行編碼,其他的也不多說了,現在我們通過一個示例來看看究竟是如何編碼的。
漢字 [楊] 的 Unicode 編號是:0x6768 ,十進位制:26472
顯然,該漢字的 UTF-8 標準編碼格式為:1110xxxx 10xxxxxx 10xxxxxx
0x6768 的二進位制是:0110 0111 0110 1000
從這個二進位制的最後一位開始,依次從後向前替換編碼格式中的 [x] 即可。
顯然,結果已經出來了,對應的十六進位制程式碼為:0xE69DA8
總結一下,UTF-8 編碼標準對所有 Unicode 編號進行了分類,排名越靠前,儲存時使用的位元組數目就越少。不同範圍的 Unicode 編號字符集在進行 UTF-8 編碼的時候會有不同的模板,以自己編號的二進位制按照相應的規則去套模板,即可得到相對應的 UTF-8 編碼。
相反的,指定了 UTF-8 編碼的檔案,計算機在進行解碼的時候,以位元組為最小單位。如果當前位元組的最高位是 0,那麼反向我們上述的幾個步驟,可以得到該字元的 Unicode 編號二進位制形式,繼而查表可以得到該字元。
如果當前位元組開頭有多個一,那麼有幾個一,該字元的編碼後的二進位制數值就有幾個位元組,順序讀取即可。然後同樣的反向操作,自然可以得到相對應的字元。
常見的幾種編碼方式就簡單介紹到這,關於編碼這塊,始終要記得本篇中所總結過一個結論。所有的編碼標準實際上都做了兩件事情,第一件就是為所有需要編碼的字元進行一個編號或標識,第二件就是指定一個規則統一得將這個編號或標識與二進位制串進行一個對映。
文章中的所有程式碼、圖片、檔案都雲端儲存在我的 GitHub 上:
(https://github.com/SingleYam/overview_java)
歡迎關注微信公眾號:撲在程式碼上的高爾基,所有文章都將同步在公眾號上。