編解碼
人類世界常見的語言文字多種多樣,有英文字母例如a,有阿拉伯數字例如6,有中文例如好 等等。但是計算機的世界裡面只有二進位制即0和1,所以我們要儲存和計算的時候就需要將人類世界的語言文字轉換為計算機能識別的二進位制,而人類的語言文字與計算機二進位制相互轉換的過程就是編解碼。
ASCII
上個世紀60年代,美國製定了一套字元編碼,對英語字元與二進位制之間的關係,做了統一規定被稱為 ASCII 碼。ASCII 碼一共規定了128個字元的編碼,例如大寫的字母A是十進位制65(二進位制01000001
),而計算機中一個位元組(byte)有8位(bit),一位能表示一個二進位制0或者1,所以一個位元組能表示最多256個符號。但是ASCII只有128個符號,所以ASCII碼只佔用了一個位元組的後面7位,最前面的一位統一規定為0。
GB2312
既然有了美國針對英語字元制定的ASCII碼,那麼為了能讓計算機能處理中文,於是中國也制定了一套中文與二進位制之間的關係編碼,那就是中華人民共和國國家標準簡體中文字符集。
其中流行比較廣泛的就是GB2312。GB2312使用兩個位元組儲存字元,採用區位碼方法來表示字元所在的區和位。其中第一個位元組稱為“高位位元組”,對應分割槽的編號,第二個位元組稱為“低位位元組”,對應區段內的個別碼位,GB2312標準共收錄6763個漢字,同時收錄了包括拉丁字母、希臘字母,日文平假名及片假名字母、俄語西裡爾字母在內的682個字元。
GB2312的出現基本滿足了漢字的計算機處理需要,它所收錄的漢字已經覆蓋中國大陸99.75%的使用頻率,但對於人名、古漢語等方面出現的罕用字和繁體字GB2312不能處理。因此後來又出現了GBK及GB18030漢字字符集以解決這些問題。
Unicode
美國有ASCII碼,中國有GB2312,那韓國、日本等世界上各個國家都有自己的編碼,同一個二進位制數字可以被解釋成不同的符號。因此要想正確讀取一個字元,就必須知道它的編碼方式,否則用錯誤的編碼方式解碼,就會出現亂碼。
正因為世界各國都有自己的編碼,導致程式很難適配所有編碼。所以需要一種全世界通用的編碼,將世界上所有的符號都納入其中,為每一個字元都賦予一個獨一無二的編碼,採用統一的編解碼就不會出現亂碼,這就是 Unicode。
Unicode使用最多4個位元組來表示,通常使用十六進位制表示,即範圍為00000000
-FFFFFFFF
。Unicode 是一個很大的集合,每個符號的編碼都不一樣。比如,U+0041表示英語的大寫字母A、U+4E25表示漢字嚴。
Unicode規範:https://datatracker.ietf.org/doc/html/rfc3629#ref-UNICODE
UTF-8
有了Unicode統一全世界字元的編碼,但Unicode 只是一個符號集,它只規定了符號的二進位制程式碼,卻沒有規定這個二進位制程式碼應該如何儲存。比如,漢字嚴的 Unicode是十六進位制數4E25
,對應二進位制數100111000100101
。這個二進位制的表示至少需要2個位元組,而目前Unicode最大4個位元組,如果全部使用4個位元組來進行儲存,無疑會大大的浪費儲存空間。
UTF-8 是 Unicode 的實現方式之一,採用一種變長的編碼方式它可以使用1~4個位元組表示一個符號,根據不同的符號而變化位元組長度。
UTF-8規範:https://datatracker.ietf.org/doc/html/rfc3629#section-3
UTF-8編碼規則
對於單位元組的符號,位元組的第一位設為0,後面7位為這個符號的 Unicode 碼 對於n位元組的符號(n > 1),第一個位元組的前n位都設為1,第n + 1位設為0 其餘位元組的前兩位設為10,所有位元組其他二進位制位為這個符號的 Unicode 碼 填充二進位制位時從低位往高位填充即從右往左,不足位使用0進行填充
Unicode符號範圍(十六進位制) | UTF-8編碼方式(二進位制) |
---|---|
0000 0000-0000 007F | 0xxxxxxx |
0000 0080-0000 07FF | 110xxxxx 10xxxxxx |
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
UTF-8規則解讀
「為什麼不直接儲存Unicode對應的二進位制,而是需要定義一套規則」
假如UTF-8像ASCII碼一樣直接儲存對應的二進位制,比如漢字“好”對應的Unicode十六進位制為
597d
,二進位制為10110010 1111101
。那麼怎麼區分這個二進位制10110010 1111101
,是一個Unicode字元而不是兩個(10110010
和1111101
)呢?正因為Unicode字元轉換成二進位制最多可能佔用4個位元組,當超過一個位元組的時候無法區分是多個單位元組的字元還是單個多位元組的字元,所以UTF-8不能直接像ASCII碼一樣直接儲存對應的二進位制。「為什麼只有一個位元組的時候需要特殊處理用0開頭」
目前ASCII一共128個字元,從00000000到01111111,為了相容ASCII所以只有一個位元組的時候就用0開始。
「為什麼第一位元組要設計為n位填充1,n+1位填充0」
為了區分一個多位元組的編碼,是「多個」單位元組的字元?還是「單個」多位元組的字元?節省空間的做法就是用編碼的第一個位元組的前幾位來表示這個編碼佔用幾個位元組 至於為什麼是用1而不是0? 假設用0來表示,漢字"好"的位元組編碼就是
00000000
01011001
01111101
。這樣有一個問題就是無法正確識別出編碼所佔的位元組數,所以還需要在表示位元組數位和實際儲存位中間設定一個分隔位,分割位取值簡單做法就是與表示位元組位數值取反即可。 比如某一個Unicode字元的位元組編碼是兩位元組的二進位制00111111
11110000
。這樣又有一個問題就是該二進位制的第一位元組與單位元組的規則衝突,所以設計多位元組的的第一位元組n位填充1,n+1作為分隔符與n位的填充符取反即為0。「為什麼n-1位元組以10開頭」
上面的設計其實已經滿足UTF-8的正常編解碼了,但是還有一個問題。假設有一個二進位制編碼
11100010 11000011 111001111
11011100 10001111
表示有兩個字元。 第一個字元三個位元組對應的二進位制編碼為11100010 11000011 111001111
,第二個字元佔兩個位元組對應的二進位制編碼位11011100 10001111
。如果因為某些原因導致寫入的時候出錯了寫成了11000010 11000011 111001111
11011100 10001111
。這時候讀取程式就會識別為第一個字元兩個位元組(11000010 11000011
),第二個字元三個位元組(111001111 11011100 10001111
)這樣讀取所有字元都是錯的。 為了解決讀取錯誤的問題,因為第一個位元組包含字元位元組數資訊,所以只需要區分開編碼的第一個位元組和其他位元組。讀取包含字元位元組資訊的第一個位元組錯誤時就能知道編碼錯誤從而採取對應措施。 而已知第一個位元組使用n位填充1,n+1位填充0的規則。所以非第一位元組使用最少兩個位10
即可與第一位元組的0
(單位元組)、110
(二位元組)、1110
(三位元組)、11110
(四位元組)區分開。 現在我們再看看,如果錯誤的寫成了11000010 10000011 101001111
11011100 10001111
,程式讀取時錯誤的將該二進位制識別為第一個字元兩個位元組(11000010 11000011
)。當讀取第二個字元時(111001111 11011100 10001111
),即第三個位元組(111001111
)時,根據第一位元組規則,字元的第一位元組永遠不可能為10
,這時程式就知道這個編碼錯誤了。就能進行對應的處理方式,提示錯誤或者跳過該位元組繼續往下讀,如果繼續往下讀最多也就當前字元出錯至少其他字元能正常讀取,將錯誤率降至最低。「為什麼多餘位填充0而不是1」
試想一下,如果多餘位填充1。漢字“祽” 對應的Unicode二進位制為
11110010 1111101
,則對應的UTF-8二進位制編碼為11101111 10100101 10111101
。而漢字“㥽”對應的Unicode二進位制為11100101 111101
,對應的UTF-8二進位制編碼為11101111 10100101 10111101
。那麼在解碼的時候UTF-8二進位制11101111 10100101 10111101
。應該解碼為漢字“祽”還是漢字“㥽”呢? 相反如果多餘位填充0,那麼漢字“祽”對應的UTF-8二進位制編碼為11100111 10100101 10111101
,而漢字“㥽”對應的UTF-8二進位制編碼為11100011 10100101 10111101
,就不會出現編碼衝突的問題。