一文了解字符編碼知識
字符編碼的奧祕
本文旨在講解常見的字符編碼,如:Unicode
、UTF-8
、GBK
字符集,以及emoji
。
起初計算機在美國發明,自然大家考慮的是如何表示英文,英語字母總共 26 個,加上特殊字符,用 128 個字符,一個byte
即足以表示出來。這個就是大家所熟知的ASCII
編碼。對應關係很簡單,一個字符對應一個byte
。
但很快人們發現,其他非英語國家的文字遠遠超過ASCII
碼,不同國家推出了自己不同的編碼方式,中國的gb2312
就是我們國家自己推行的編碼方式,這樣下去每個國家都有自己的編碼方式,來回轉換太麻煩了;這時候大家當然想統一字符編碼,這時候出現了新的編碼方式,unicode
編碼方式,將編碼統一,規定了每個字符對應的unicode
碼。
Unicode 概述
Unicode
(統一碼,萬國碼)是基於通用字符集(Universal Character Set
)的標準發展。它爲每種語言中的每個字符設定了統一併且唯一的二進制編碼,以滿足語言、跨平臺進行文本轉換、處理的要求。
Unicode
是 1988 年由 Apple 和 Xerox 共同建立的一項標準。1991 年,成立專門的協會來開發和推動Unicdoe
。
Unicode
用數字 0 到 0X10FFFF 來映射這些數字,最多容納 1114112 個字符。1114112 是怎麼計算出來的?將 0X10FFFF 分成 0X10 和 0XFFFF 兩部分。我們知道 0XFFFF 是 65535,那麼 [0,65535] 區間內,總共是 65536 個。同理,0X10 用 10 進製表示爲 16,那麼 [ 0,16 ] 左右閉區間,總共是 17 個(我們稱之爲 17 個平面)。所以 17 乘以 65536 = 1114112。
這裏需要注意:第 0 平面最爲重要,後面第 1,2…16 所有平面都是通過第 0 平面中的一個代理區表示的(詳見圖 4)。
世界上各種字符,用 Unicode 排布
當我們看到 0X10FFFF 的時候,會產生一個疑問。怎麼是三個字節呢?現代計算機都是 32 位或者 64 位了吧。如果是 32 位的話,第一個字節幹什麼用,是全用 0 填充麼?怎麼第二個字節也就用到 0x10?
前面我們提到 “通用字符集”。目前世界上有兩個標準UCS-2
用 2 個字節編碼和UCS-4
用 4 個字節編碼。那麼 0X10FFFF 即UCS-4
了。
把 0x10FFFF 用二進制表示:0000-00000001-00001111-11111111-1111
如上所示,UCS-4
根據最高位爲 0 的最高字節分成 2^7=128 個 group。每個 group 再根據次高字節分爲 256 個平面(plane)。每個平面根據第 3 個字節分爲 256 行 (row),每行第 4 字節有 256 個碼位(cell)。每個平面有 2^16=65536 個碼位。如圖 1:
上面我們講過,14-15-16 平面基本都是PUA
,最重要的是下面的 0-1-2 三個平面,特別是第 0 個平面,也就是下圖中色彩斑斕的平面;
(圖 2)
圖中每層代表一個平面。Unicode
計劃使用了 17 個平面,一共有 17*65536=1114112 個碼位。在Unicode 5.0.0
版本中,已定義的碼位只有 238605 個,分佈在平面 0、平面 1、平面 2、平面 14、平面 15、平面 16。其他平面尚未使用,如下圖所示:
(圖 3)
其中,平面 15 和 16 上定義了兩個各佔 65534 個碼位的專用區(Private Use Area
),分別是 0xF0000-0xFFFFD 和 0x100000-0x10FFFD。所謂專用區,就是保留給大家放自定義字符的區域,可以簡寫爲PUA
。下圖說明了第 0 平面的字符分佈:
(圖 4)
平面 0 也有一個專用區:0xE000-0xF8FF,有 6400 個碼位。平面 0 的 0xD800-0xDFFF,共 2048 個碼位(見上圖),是一個被稱作代理區(Surrogate
)的特殊區域。代理區的目的是:用兩個UTF-16
字符表示BMP
以外的字符。如前所述在Unicode 5.0.0
版本中,238605-65534*2-6400-2048=99089。餘下的 99089 個已定義碼位分佈在平面 0、平面 1、平面 2 和平面 14 上,它們對應着Unicode
目前定義的 99089 個字符。平面 0、平面 1、平面 2 和平面 14 上分別定義了 52080、3419、43253 和 337 個字符。中國漢字的Unicode
位置,71226 個漢字分別分佈在兩個平面上,第 0 個平面上有 27973 個常用漢字,第 2 個平面 43253 個漢字,你會發現第 2 個平面上清一色都是漢字。
UTF-8 編碼
回到本文的重點UTF-8
編碼,utf-8
是Unicode
的一種存儲、傳輸方式,是一種可變長編碼方式;是Unicode
實現方式之。UTF
是 “Unicode/UCS Transformation Format
” 的首字母縮寫。Unicode
是字符集也是編碼集,UTF-8
是編碼集;UTF-8
以字節爲單位對Unicode
進行編碼。它是可變長的編碼方式的從表 1 中也能開出來。從Unicode
到UTF-8
的編碼方式如下:
(圖 5)
例:“漢” 字的Unicode
編碼是 0x6C49。0x6C49 在 0x0800-0xFFFF 之間,使用用 3 字節模板了:1110xxxx 10xxxxxx 10xxxxxx。將 0x6C49 寫成二進制是:0110 1100 0100 1001, 用這個比特流依次代替模板中的 x,得到:11100110 10110001 10001001,即 E6 B1 89。因爲,常用漢字第一個的編碼從 0X3e00,所以常用漢字的UTF8
都是三個字節。如果在平面 2 中的漢字必須 4 個字節了。如果你用 C 語言做過協議棧,就會清楚記得,給漢字開闢內存時候要使用 4 個字節。
此外,使用正則表達式來區分漢字,通常使用下面的代碼:
NSPredicate* predicate = [NSPredicate predicateWithFormat:@"SELF MATCHES %@",@"[\u4e00-\u9fa5]"];
**_判斷是否爲中文的正則表達式_**
if([predicate evaluateWithObject:name]){
是中文
}else{
不是中文
}
那麼對於一些生僻字,就不靈了,如下圖:
(圖 6)
如四個日組成的漢字,運行結果卻 “不是漢字”,因爲它的Unicode
編碼是 0xD84C-DEAB(這是代理區)。不在 [e4E00-9FA5] 區間。但,此正則表達式對於鑑別常用漢字已經是足夠使用了。
代理區
第 0 平面中,0xD800-0xDFFF,共 2048 個碼位用於表示代理區。在輸入框輸入一個emoji
——😊微笑,然後通過UTF-8
轉化工具,看看它的編碼是這樣:
(圖 7)
可以看到Unicode
編碼是 D83D-DE03。utf-8
的編碼是 F09F-9883。常見字符Unicode
很少四個字節的,這個不尋常的。通常utf-8
是 1 到 3 個字節的,也就是說在Unicode
編碼空間的第 0 個平面上。可以簡單推測:既然emoji
的utf-8
是 4 個字節,說明在 1,2…16 個平面中的某一個 我們再看看 “微笑” 的emoji
符號的Unicode
:D83D-DE03,已經超過了表格中最大的 0X10FFFF 了,怎麼回事???下面我們根據utf-8
的值:F09F-9883. 來反推Unicode
對應的數值吧,看看究竟是爲什麼:
(圖 8)
得出的結果是 0x1-F603,這個結果跟Unicode
:D83D-DE03 的值相差很大,所以,中間肯定經過了一些轉換步驟,這個轉換就是utf-16
的代理!!!
UTF-16
UTF
是 “Unicode/UCS Transformation Format
” 的首字母縮寫,即把Unicode
字符轉換爲某種格式之意。
正常情況下,一個Unicode
兩個字節,在轉化uft-8
的時候,根據協議,兩個兩個字節,對應一個uft-8
這樣完成轉化或者稱爲映射!
其實在第 0 個平面中,專門有一個代理區域,不表示任何字符,只用於指向第 1 到第 16 個平面中的字符,這段區域是:D800——DFFF。其中 0xD800——0xDBFF 是前導代理 (lead surrogates
),0xDC00——0xDFFF 是後尾代理 (trail surrogates
)。
一個代理對兒(前導,後尾),就表示一個utf-16
的字符。就那emoji
的微笑來說,前導是代理:D83D;後尾代理是:DE03。根據下圖可以得出utf-16
的值是:0x1-F603。這就照應上了。
具體的公式是:0x10000 + (前導 - 0xD800) * 0x400 + (後導 - 0xDC00) =utf-16
編碼。
(圖 9)
筆者做一個形象的比喻:這對兒(前導代理,後尾代理)就像一個指針,指向了第 1——16 平面上的每一個碼位。經過計算,不難得出:16 個平面 * 每個平面碼位 65536 = 1,048,576 個,前導 X 後尾代理,可以表示的碼位也是 1,048,576 個(哈!真是一個完美的解決方案)如圖 9 所示。
if(Unicode第一個字節 >=0xD8 && Unicode <=0xDB){
這是代理區域,表示第1——16平面的字符。每四個字節表示一個單元
}
else{
這是正常映射區域,表示第0個平面。每兩個字節表示一個單元。
}
Emoji
並不都是四個字節。舉個例子有些複雜表情會更長:比如👩👩👦👦,使用NSString
標示時候:str.length
是 11,data.length
是 25:他是由四個emoji
組成的。只不過在編碼的開始加入了特殊的控制字符。👩👩👦👦=👩+👩+👦+👦
NSString* str = @“👩👩👦👦”;
NSData* data = [str dataUsingEncoding:NSUTF8StringEncoding];
NSLog(@“emoji str-len:%d data-len:%d”,str.length,data.length);
結果是:emoji str-len:11 data-len:25
。
詳細查看網站 https://apps.timwhitlock.info/unicode/inspect
iOS 上實現顯示漢字的 Unicode 和 UTF8 代碼
主要思想:
使用NSString
的方法dataUsingEncoding
,參數可以選擇NSUnicodeStringEncoding
和NSUTF8StringEncoding
然後得到NSData
;
如果用GBK
需要自己生成一個NSStringEncoding
,代替NSUTF8StringEncoding
。
NSStringEncoding gbkEncoding =CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingGB_18030_2000);
NSData
包含了首地址和長度。比如 “聯通” 經過Unicode
後是 4 個字節,UTF-8
之後是 6 個字節,都可以從NSData
中得到。然後,用char*
指向NSSdata
後,逐個將char
打印出來就行。如下圖所示:
(圖 10)
注意:由於 intel cpu 是採用小端,所以如果不進行大端轉換,視覺上是顛倒的,這裏我已經給轉成大端的了,大端即正常視覺模式。小故事:早期 window 電腦用記事本存儲 “聯通” 兩個字,關閉再打開,顯示亂碼。原因就是 windows 默認保存的編碼是ANSI
(在中國是GB2312
),而 “聯”ANSI
編碼是 0xC1AA 二進制排列是 1100 0001 1010 1010;“通”ANSI
編碼是 0xCDA8 二進制排列是 1100 1101 1010 1000;開頭是 110 開頭,再次打開記事本,系統會使用utf8
打開。就會異常。現在的操作系統會存儲一個BOM
,來表明是那種編碼避免了亂碼的問題。
GBK 簡單介紹
GBK
——專門爲解決漢字的編碼而生成的解決方案。那麼,一個漢字究竟被存儲爲什麼,就需要:先查unicode
碼錶,然後根據在碼錶的位置進行計算。例如:“電” 字,在碼錶中是 3575,而在GB2312
的碼錶中爲 B5E7。GBK
的中文編碼是雙字節來表示的,英文編碼是用ASCII
碼錶示的,即用單字節表示。但GBK
編碼表中也有英文字符的雙字節表示形式,所以英文字母可以有 2 種GBK
表示方式。爲區分中文,將其最高位都定成 1。英文單字節最高位都爲 0。當用GBK
解碼時,若高字節最高位爲 0,則用ASCII
碼錶解碼;若高字節最高位爲 1,則用GBK
碼錶解碼。
編碼方式:
字符有一字節和雙字節編碼,00–7F 範圍內是第一個字節,和ASCII
保持一致,此範圍內嚴格上說有 96 個文字和 32 個控制符號。之後的雙字節中,前一字節是雙字節的第一位。總體上說第一字節的範圍是 81–FE(也就是不含 80 和 FF),第二字節的一部分領域在 40–7E,其他領域在 80–FE。具體來說,定義的是下列字節:
(圖 11)
雙字節符號可以表達的 64K 空間如下圖所示。綠色和黃色區域是GBK
的編碼,紅色是用戶定義區域。沒有顏色的區域是不正確的代碼組合:
(圖 12)
要點總結
-
GBK
編碼是GB2312
編碼的超集,向下完全兼容GB2312
。 -
GB18030
編碼向下兼容GBK
和GB2312
,GBK
、GB2312
等與UTF8
之間都必須通過Unicode
編碼才能相互轉換。 -
GBK
,GB2312
以及Unicode
都既是字符集,也是編碼方式,而UTF-8
只是編碼方式,並不是字符集。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/fVbQyQ2D2eQo0U5HQmbxRg