記錄一下字符編碼的有關事項,這篇文章先說說字符編碼的一些歷史和原理。
1.ASCII編碼與ANSI標準
1字節(1B) = 8比特(8b) = 8個二進制位
1B的信息量 0b00000000 ~ 0b11111111 = 2^8 = 256個狀態
標準ASCII編碼使用1B的后7位,而第一位默認置0,即:
0b00000000 ~ 0b01111111 = 2^7 = 128個狀態
這128個狀態位存儲了控制字符、數字、大小寫字母和其他符號。詳見ASCII編碼表[]
漸漸地,128個狀態不再能滿足人們的需求,不同國家(語言區)都想把自己語言的字符編進ASCII編碼中,于是人們紛紛自發的使用起了1B的另外128個狀態,稱為擴展ASCII編碼,其使用1B的后7位,同時第一位默認置1,即:
0b10000000 ~ 0b11111111 = 2^7 = 128個狀態
由于是自發行為,每個國家(語言區)都對擴展ASCII編碼有自己獨特的定義,因此彼此之間的擴展ASCII編碼是不能通用的。(標準ACII編碼區仍然通用)
麻煩還不止于此,部分國家(語言區)的字符數量眾多(如漢字就是十萬級別的數量),顯然擴展ASCII編碼的128個狀態不能滿足,于是有了下面的一種編碼方式(ANSI標準編碼):
- 保留標準ASCII編碼的128個字符不變,稱為半角
- 其余的字符用兩個字節來表示,這樣理論上能新增256*256=65536個編碼,稱為全角
這也是大家最早所認知的下面這句話的由來
英文(半角)占一個字節,漢字(全角)占兩個字節
全角字符中用于編碼的兩個字節被稱為高位字節和低位字節
全角字符 = 高位字節 + 低位字節
高位字節和低位字節都可以選擇用三種策略編碼:
- 標準ASCII編碼的128個狀態
- 擴展ASCII編碼的128個狀態
- 以上都用的256個狀態
不難發現當這兩個字節都使用標準ASCII編碼的128個狀態時,計算機是無法分辨這是兩個半角字符,還是一個全角字符的。因此全角的編碼區域實際上只有:
256*256 - 128*128 = 49152個
針對這個5萬個左右的編碼區,不同國家(語言區)制定了不同的編碼標準,我們熟悉的有:
- GB2312,大陸1980年標準(其中一級漢字3755個,二級漢字3008個,包括拉丁字母、希臘字母、日文平假名及片假名字母、俄語西里爾字母在內的全角字符682個。)
- GBK,大陸1995年標準(GB2312的擴展,增加不常用漢字與字符,總編碼量擴展到23940)
- BIG-5,臺灣標準
2. Unicode編碼
隨著互聯網的發展,信息交流變得越來越頻繁,這就促使一種“大一統”的編碼方式出現。這種編碼可以將世界上所有的字符都編入其中,且每個狀態位和字符都唯一對應。正如它的名字代表的意思那樣,Unicode編碼做到了。
Unicode將編碼和存儲這兩個邏輯過程獨立開來。
- 世界上的每一個字符都有且只有一個Unicode編碼方案即:字符S->Unicode(S)
- 每一個Unicode碼都可以通過的多種方案來存儲,這些方案的名稱即是我們常聽到的UTF-8、UTF-16、UTF-32等等。
2.1 編碼邏輯
下面先說一下Unicode的編碼方式:
Unicode碼的編碼范圍:0x000000 ~ 0x10FFFF
它的后四位稱為一個plane:0x0000 ~ 0xFFFF = 2^16 = 65536個狀態
前兩位代表plane的編號,一共有:0x00 ~ 0x10 = 17個plane
所以Unicode編碼一共有:17 * 65536 = 1114112個狀態
這足以將世界上所有的字符都編碼進去,而且還有很大的富余。同時別忘了,預留的空白plane可遠遠多于17個(16*16=256個)
事實上,在Unicode 5.0版本中只用到了0,1,2,14,15,16這幾個編號plane中的238605個狀態
2.2 存儲邏輯
Unicode編碼總算是將全世界的字符都“裝”下了,當然為了達到此目的,經過Unicode編碼后的字符所占空間變的很大。本來只占一個字節的普通ASCII字符和占兩個字節的ANSI字符統統都變成了占三個字節的Unicode字符,造成了空間的極大浪費。因此Unicode編碼的編碼方式和存儲方式分開獨立實現,存儲傳輸方案專注于實現Unicode編碼如何節省存儲空間。
常見的存儲傳輸方案有:UTF-8,UTF-16,UTF-32等
UTF-8方案
UTF-8最大的一個特點,就是它是一種變長的存儲方式。它可以使用1~4個字節存儲一個Unicode碼,根據不同的Unicode碼而變化存儲字節的長度。
UTF-8的存儲規則很簡單,只有二條:
- 對于小于0x7F的Unicode碼,UTF-8編碼只有一個字節,字節的第一位設為0,后面7位為Unicode碼。這也是為了英語字母的UTF-8編碼和普通ASCII碼是相同的。
- 對于其他Unicode碼,落入下表的相應的范圍中。其中x代表空白位,用Unicode碼補充。
Unicode符號范圍(十六進制) | UTF-8存儲方式(二進制) |
---|---|
000000 ~ 00007F | 0xxxxxxx |
000080 ~ 0007FF | 110xxxxx 10xxxxxx |
000800 ~ 00FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
010000 ~ 10FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
UTF-16方案
UTF-16方案也是一種變長存儲方式,它采用兩個字節或四個字節來存儲Unicode碼。
當Unicode碼位于編號為00的plane(即0x000000 ~ 0x00FFFF之間)時,Unicode碼正好對應了兩個字節(16位二進制)的長度。即使用兩個字節順序存儲。
當Unicode碼位于其他編號的plane(即0x010000 ~ 0x10FFFF)時,Unicode碼減去0x10000后正好對應成20位二進制,依次填入以下四個字節的20個空位中:
110110xx xxxxxxxx 110111xx xxxxxxxx
前兩個字節稱為高位WORD,以110110開頭;后兩個字節稱為低位WORD,以110111開頭。
UTF-32方案
UTF-32方案是一種定長存儲方案,它總是采用4個字節存儲Unicode碼,由于Unicode碼只有24位,于是UTF-32方案不對Unicode碼做任何變化直接存儲。
有人會問,用3個字節不也是可以完全存儲Unicode的所有編碼嗎?我個人的理解是,由于Unicode的前身:通用字符集(Universal Character Set, UCS)分為了2字節編碼的UCS-2和4字節編碼的UCS-4。為了使編碼方案統一,UCS-4承諾不再向0x10FFFF之后編碼,并由UCS-2作為編號為0的plane(Basic Multilingual Plane, BMP)共同組成Unicode編碼。3者之間的關系為:
UCS-4編碼中0x00000000 ~ 0x0010FFFF的部分組成了Unicode編碼
Unicode編碼中0x000000 ~ 0x00FFFF的部分組成了UCS-2編碼
因此UTF-16作為UCS-2編碼的存儲方案,UTF-32作為UCS-4編碼的存儲方案使用4個字節,還是仍然保留著。
關于字節序(Byte Order Mark, BOM)
在UTF-16和UTF-32中存在著Little endian (LE)和Big endian (BE)兩種傳輸字節流的方式(顯然很蛋疼)
為了讓機器識別出這兩種傳輸方式,在Unicode編碼規范中設定了一個叫做"ZERO WIDTH NO-BREAK SPACE"的狀態位0x00FEFF,它不對應任何實際含義的字符,且總是出現在文件的開頭,用于標志該文件是使用什么方式讀取字節流的(LE or BE)。按照上文三種存儲方案的規則:
編碼方案 | BOM(十六進制) |
---|---|
UTF-8 | EF BB BF |
UTF-16LE | FF FE |
UTF-16BE | FE FF |
UTF-32LE | FF FE 00 00 |
UTF-32BE | 00 00 FE FF |
可見由于UTF-8不存在LE或BE的區分,因此它的BOM只有一種,可有可無。所以在UTF-8的方案下,有UTF-8 BOM和UTF-8 無BOM兩種,而它們的區別僅僅是文件開頭有沒有EF BB BF 這三個字節罷了