徹底搞懂字元編碼(unicode,mbcs,utf-8,utf-16,utf-32,big endian,little endian...)

raindayinrain發表於2018-06-27

最近有一些朋友常問我一些亂碼的問題,和他們交流過程中,發現這個編碼的相關知識還真是雜亂不堪,不少人對一些知識理解似乎也有些偏差,網上百度,google的內容,也有不少以訛傳訛,根本就是錯誤的(例如說 unicode編碼是兩個位元組),各種軟體讓你選擇編碼的時候,常常是很長的一個選單,讓使用者不知道該如何選。基於這樣的問題,我就寫下我的理解吧,一方面幫助一些需要幫助的人糾正認識,一方面作為自己以後備查的資料。

1.ASCII(American Standard Code for Information Interchange)
美國資訊交換標準程式碼,這是計算機上最早使用的通用的編碼方案。那個時候計算機還只是拉丁文字的專利,根本沒有想到現在計算機的發展勢頭,如果想到了,可能一開始就會使用unicode了。當時絕大部分專家都認為,要用計算機,必須熟練掌握英文。這種編碼佔用7個Bit,在計算機中佔用一個位元組,8位,最高位沒用,通訊的時候有時用作奇偶校驗位。因此ASCII編碼的取值範圍實際上是:0x00-0x7f,只能表示128個字元。後來發現128個不太夠用,做了擴充套件,叫做ASCII擴充套件編碼,用足八位,取值範圍變成:0x00-0xff,能表示256個字元。其實這種擴充套件意義不大,因為256個字元表示一些非拉丁文字遠遠不夠,但是表示拉丁文字,又用不完。所以擴充套件的意義還是為了下面的ANSI編碼服務。

2.ANSI(American National Standard Institite )
美國國家標準協會,也就是說,每個國家(非拉丁語系國家)自己制定自己的文字的編碼規則,並得到了ANSI認可,符合ANSI的標準,全世界在表示對應國家文字的時候都通用這種編碼就叫ANSI編碼。換句話說,中國的ANSI編碼和在日本的ANSI的意思是不一樣的,因為都代表自己國家的文字編碼標準。比如中國的ANSI對應就是GB2312標準,日本就是JIT標準,香港,臺灣對應的是BIG5標準等等。當然這個問題也比較複雜,微軟從95開始,用就是自己搞的一個標準GBK。GB2312裡面只有6763個漢字,682個符號,所以確實有時候不是很夠用。GBK一直能和GB2312相互混淆並且相安無事的一個重要原因是GBK全面相容GB2312,所以沒有出現任何衝突,你用GB2312編碼的檔案通過GBK去解釋一定能獲得相同的顯示效果,換句話說:GBK對GB2312就是,你有的,我也有,你沒得的,我還有!

好了,ANSI的標準是什麼呢,首先是ASCII的程式碼你不能用!也就是說ASCII碼在任何ANSI中應該都是相同的。其他的,你們自己擴充套件。所以呢,中國人就把ASCII碼變成8位,0x7f之前我不動你的,我從0xa0開始編,0xa0到0xff才95個碼位,對於中國字那簡直是杯水車薪,因此,就用兩個位元組吧,此編碼範圍就從0xA1A1 - 0xFEFE,這個範圍可以表示23901個漢字。基本夠用了吧,GB2312才7000多個呢!GBK更猛,編碼範圍是從0x8140 - 0xFEFE,可以表示3萬多個漢字。可以看出,這兩種方案,都能保證漢字頭一個位元組在0x7f以上,從而和ASCII不會發生衝突。能夠實現英文和漢字同時顯示。

BIG5,香港和臺灣用的比較多,繁體,範圍: 0xA140 - 0xF9FE, 0xA1A1 - 0xF9FE,每個字由兩個位元組組成,其第一位元組編碼範圍為0xA1~0xF9,第二位元組編碼範圍為0x40-0x7E與0xA1-0xFE,總計收入13868個字 (包括5401個常用字、7652 個次常用字、7個擴充字、以及808個各式符號)。

那麼到底ANSI是多少位呢?這個不一定!比如在GB2312和GBK,BIG5中,是兩位!但是其他標準或者其他語言如果不夠用,就完全可能不止兩位!

例如:GB18030:
GB18030-2000(GBK2K)在GBK的基礎上進一步擴充套件了漢字,增加了藏、蒙等少數民族的字形。GBK2K從根本上解決了字位不夠,字形不足的問題。它有幾個特點:它並沒有確定所有的字形,只是規定了編碼範圍,留待以後擴充。編碼是變長的,其二位元組部分與GBK相容;四位元組部分是擴充的字形、字位,其編碼範圍是首位元組0x81-0xfe、二位元組0x30-0x39、三位元組0x81-0xfe、四位元組0x30-0x39。它的推廣是分階段的,首先要求實現的是能夠完全對映到Unicode3.0標準的所有字形。它是國家標準,是強制性的。

搞懂了ANSI的含義,我們發現ANSI有個致命的缺陷,就是每個標準是各自為陣的,不保證能相容。換句話說,要同時顯示中文和日本文或者阿拉伯文,就完全可能會出現一個編碼兩個字符集裡面都有對應,不知道該顯示哪一個的問題,也就是編碼重疊的問題。顯然這樣的方案不好,所以Unicode才會出現!

3.MBCS(Multi-Byte Chactacter System(Set))
多位元組字元系統或者字符集,基於ANSI編碼的原理上,對一個字元的表示實際上無法確定他需要佔用幾個位元組的,只能從編碼本身來區分和解釋。因此計算機在儲存的時候,就是採用多位元組儲存的形式。也就是你需要幾個位元組我給你放幾個位元組,比如A我給你放一個位元組,比如”中“,我就給你放兩個位元組,這樣的字元表示形式就是MBCS。

在基於GBK的windows中,不會超過2個位元組,所以windows這種表示形式有叫做DBCS(Double-Byte Chactacter System),其實算是MBCS的一個特例。C語言預設存放字串就是用的MBCS格式。從原理上來說,這樣是非常經濟的一種方式。

4.CodePage

內碼表,最早來自IBM,後來被微軟,oracle ,SAP等廣泛採用。因為ANSI編碼每個國家都不統一,不相容,可能導致衝突,所以一個系統在處理文字的時候,必須要告訴計算機你的ANSI是哪個國家和地區的標準,這種國家和標準的代號(其實就是字元編碼格式的代號),微軟稱為Codepage內碼表,其實這個內碼表和字符集編碼的意思是一樣的。告訴你內碼表,本質就是告訴了你編碼格式。

但是不同廠家的內碼表可能是完全不同,哪怕是同樣的編碼,比如, UTF-8字元編碼 在IBM對應的內碼表是1208,在微軟對應的是65001,在德國的SAP公司對應的是 4110 。所以啊,其實本來就是一個東西,大家各自為政,搞那麼多新名詞,實在沒必要!所以標準還是很重要的!!!

比如GBK的在微軟的內碼表是936,告訴你內碼表是936其實和告訴你我編碼格式是GBK效果完全相同。那麼處理文字的時候就不會有問題,不會去考慮某個程式碼是顯示的韓文還是中文,同樣,日文和韓文的內碼表就和中文不同,這樣就可以避免編碼衝突導致計算機不知如何處理的問題。當然用這個也可以很容易的切換語言版本。但是這都是治標不治本的方法,還是無法解決同時顯示多種語言的問題,所以最後還是都用unicode吧,永遠不會有衝突了。

5.Unicode(Universal Code)
這是一個編碼方案,說白了就是一張包含全世界所有文字的一個編碼表,不管你用的上,用不上,不管是現在用的,還是以前用過的,只要這個世界上存在的文字元號,統統給你一個唯一的編碼,這樣就不可能有任何衝突了。不管你要同時顯示任何文字,都沒有問題。因此在這樣的方案下,Unicode出現了。Unicode編碼範圍是:0-0x10FFFF,可以容納1114112個字元,100多萬啊。全世界的字元根本用不完了,Unicode 5.0版本中,才用了238605個碼位。所以足夠了。

因此從碼位範圍看,嚴格的unicode需要3個位元組來儲存。但是考慮到理解性和計算機處理的方便性,理論上還是用4個位元組來描述。

Unicode採用的漢字相關編碼用的是《CJK統一漢字編碼字符集》— 國家標準 GB13000.1 是完全等同於國際標準《通用多八位編碼字符集 (UCS)》 ISO 10646.1。《GB13000.1》中最重要的也經常被採用的是其雙位元組形式的基本多文種平面。在這65536個碼位的空間中,定義了幾乎所有國家或地區的語言文字和符號。其中從0x4E00到 0x9FA5 的連續區域包含了 20902 個來自中國(包括臺灣)、日本、韓國的漢字,稱為 CJK (Chinese Japanese Korean) 漢字。CJK是《GB2312-80》、《BIG5》等字符集的超集。

CJK包含了中國,日本,韓國,越南,香港,也就是CJKVH。這個在UNICODE的Charset chart中可以明顯看到。 unicode的相關標準可以從unicode.org上面獲得,目前已經進行到了6.0版本。

下面這段描述來自百度百科:
Unicode字符集可以簡寫為UCS(Unicode Character Set)。早期的 unicodeUnicode標準有UCS-2、UCS-4的說法。UCS-2用兩個位元組編碼,UCS-4用4個位元組編碼。UCS-4根據最高位為0的最高位元組分成2^7=128個group。每個group再根據次高位元組分為256個平面(plane)。每個平面根據第3個位元組分為256行 (row),每行有256個碼位(cell)。group 0的平面0被稱作BMP(Basic Multilingual Plane)。將UCS-4的BMP去掉前面的兩個零位元組就得到了UCS-2。每個平面有2^16=65536個碼位。Unicode計劃使用了17個平面,一共有17*65536=1114112個碼位。在Unicode 5.0.0版本中,已定義的碼位只有238605個,分佈在平面0、平面1、平面2、平面14、平面15、平面16。其中平面15和平面16上只是定義了兩個各佔65534個碼位的專用區(Private Use Area),分別是0xF0000-0xFFFFD和0x100000-0x10FFFD。所謂專用區,就是保留給大家放自定義字元的區域,可以簡寫為PUA。   平面0也有一個專用區:0xE000-0xF8FF,有6400個碼位。平面0的0xD800-0xDFFF,共2048個碼位,是一個被稱作代理區Surrogate)的特殊區域。代理區的目的用兩個UTF-16字元表示BMP以外的字元。在介紹UTF-16編碼時會介紹。如前所述在Unicode 5.0.0版本中,238605-65534*2-6400-2408=99089。餘下的99089個已定義碼位分佈在平面0、平面1、平面2和平面14上,它們對應著Unicode目前定義的99089個字元,其中包括71226個漢字。平面0、平面1、平面2和平面14上分別定義了52080、3419、43253和337個字元。平面2的43253個字元都是漢字。平面0上定義了27973個漢字。

6.Unicode的實現方案
Unicode其實只是一張巨大的編碼表。要在計算機裡面實現,也出現了幾種不同的方案。也就是說如何表示unicode編碼的問題。

(1)UTF-8(UCS Transformation Format 8bit)
這個方案的意思以8位為單位來標識文字,注意並不是說一個文字用8位標識。他其實是一種MBCS方案,可變位元組的。到底需要幾個位元組表示一個符號,這個要根據這個符號的unicode編碼來決定,最多4個位元組。

編碼規則如下:
Unicode編碼(16進位制) ║ UTF-8 位元組流(二進位制)  
 000000 - 00007F ║ 0xxxxxxx   
000080 - 0007FF ║ 110xxxxx 10xxxxxx   
000800 - 00FFFF ║ 1110xxxx 10xxxxxx 10xxxxxx   
010000 - 10FFFF ║ 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx   
UTF-8的特點是對不同範圍的字元使用不同長度的編碼。對於0x00-0x7F之間的字元,UTF-8編碼與ASCII編碼完全相同。

UTF-8編碼的最大長度是4個位元組。從上表可以看出,4位元組模板有21個x,即可以容納21位二進位制數字。Unicode的最大碼位0x10FFFF也只有21位。   

例1:“漢”字的Unicode編碼是0x6C49。0x6C49在0x0800-0xFFFF之間,使用用3位元組模板了:1110xxxx 10xxxxxx 10xxxxxx。將0x6C49寫成二進位制是:0110 1100 0100 1001, 用這個位元流依次代替模板中的x,得到:11100110 10110001 10001001,即E6 B1 89。   

例2:Unicode編碼0x20C30在0x010000-0x10FFFF之間,使用用4位元組模板了:11110xxx 10xxxxxx 10xxxxxx 10xxxxxx。

將0x20C30寫成21位二進位制數字(不足21位就在前面補0):0 0010 0000 1100 0011 0000,用這個位元流依次代替模板中的x,得到:11110000 10100000 10110000 10110000,即F0 A0 B0 B0。

(2)UTF-16
UTF-16編碼以16位無符號整數為單位。注意是16位為一個單位,不表示一個字元就只有16位。現在機器上的unicode編碼一般指的就是UTF-16。絕大部分2個位元組就夠了,但是不能絕對的說所有字元都是2個位元組。這個要看字元的unicode編碼處於什麼範圍而定,有可能是2個位元組,也可能是4個位元組。這點請注意!

下面演算法解釋來自百度百科。

我們把Unicode unicode編碼記作U。編碼規則如下:
  如果U<0x10000,U的UTF-16編碼就是U對應的16位無符號整數(為書寫簡便,下文將16位無符號整數記作WORD)。如果U≥0x10000,我們先計算U’=U-0x10000,然後將U’寫成二進位制形式:yyyy yyyy yyxx xxxx xxxx,U的UTF-16編碼(二進位制)就是:110110yyyyyyyyyy 110111xxxxxxxxxx。為什麼U’可以被寫成20個二進位制位?Unicode的最大碼位是0x10ffff,減去0x10000後,U’的最大值是0xfffff,所以肯定可以用20個二進位制位表示。

例如:Unicode編碼0x20C30,減去0x10000後,得到0x10C30,寫成二進位制是:0001 0000 1100 0011 0000。用前10位依次替代模板中的y,用後10位依次替代模板中的x,就得到:1101100001000011 1101110000110000,即0xD8430xDC30。   

按照上述規則,Unicode編碼0x10000-0x10FFFF的UTF-16編碼有兩個WORD,第一個WORD的高6位是110110,第二個WORD的高6位是110111。可見,第一個WORD的取值範圍(二進位制)是11011000 00000000到11011011 11111111,即0xD800-0xDBFF。第二個WORD的取值範圍(二進位制)是11011100 00000000到11011111 11111111,即0xDC00-0xDFFF。為了將一個WORD的UTF-16編碼與兩個WORD的UTF-16編碼區分開來,Unicode編碼的設計者將0xD800-0xDFFF保留下來,並稱為代理區(Surrogate):   

D800-DB7F ║ High Surrogates ║ 高位替代   
DB80-DBFF ║ High Private Use Surrogates ║ 高位專用替代   
DC00-DFFF ║ Low Surrogates ║ 低位替代   
高位替代就是指這個範圍的碼位是兩個WORD的UTF-16編碼的第一個WORD。低位替代就是指這個範圍的碼位是兩個WORD的UTF-16編碼的第二個WORD。那麼,高位專用替代是什麼意思?我們來解答這個問題,順便看看怎麼由UTF-16編碼推導Unicode編碼。   

如果一個字元的UTF-16編碼的第一個WORD在0xDB80到0xDBFF之間,那麼它的Unicode編碼在什麼範圍內?我們知道第二個WORD的取值範圍是0xDC00-0xDFFF,所以這個字元的UTF-16編碼範圍應該是0xDB80 0xDC00到0xDBFF 0xDFFF。我們將這個範圍寫成二進位制:   1101101110000000 11011100 00000000 - 1101101111111111 1101111111111111   按照編碼的相反步驟,取出高低WORD的後10位,並拼在一起,得到   1110 0000 0000 0000 0000 - 1111 1111 11111111 1111 即0xe0000-0xfffff,按照編碼的相反步驟再加上0x10000,得到0xf0000-0x10ffff。這就是UTF-16編碼的第一個WORD在0xdb80到0xdbff之間的Unicode編碼範圍,即平面15和平面16。因為Unicode標準將平面15和平面16都作為專用區,所以0xDB80到0xDBFF之間的保留碼位被稱作高位專用替代。

(3)UTF-32
這個就簡單了,和Unicode碼錶基本一一對應,固定四個位元組。
為什麼不採用UTF-32呢,因為unicode定義的範圍太大了,其實99%的人使用的字元編碼不會超過2個位元組,所以如同統一用4個位元組,簡單倒是簡單了,但是資料冗餘確實太大了,不好,所以16位是最好的。就算遇到超過16位能表示的字元,我們也可以通過上面講到的代理技術,採用32位標識,這樣的方案是最好的。所以現在絕大部分機器實現unicode還是採用的utf-16的方案。當然也有UTF-8的方案。比如windows用的就是UTF16方案,不少linux用的就是utf8方案。

  1. 編碼儲存差異

這裡就要引出兩個名詞:
LE(little endian):小位元組位元組序,意思就是一個單元在計算機中的存放時按照低位在前(低地址),高位在後(高地址)的模式存放。

BE(big endian):大位元組位元組序,和LE相反,是高位在前,低位在後。

比如一個unicode編碼為:0x006C49,如果是LE,那麼在檔案中的存放順序應該是:49 6c 00如果是BE ,那麼順序應該是:00 6c 49

8.編碼格式的檢測

到底採用什麼編碼,如果能檢測就好了。專家們也是這麼想的,所以專家給每種格式和位元組序規定了一些特殊的編碼,這些編碼在unicode 中是沒有使用的,所以不用擔心會衝突。這個叫做BOM(Byte Order Mark)頭。意思是位元組序標誌頭。通過它基本能確定編碼格式和位元組序。

UTF編碼 ║ Byte Order Mark   
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
所以通過檢測檔案前面的BOM頭,基本能確定編碼格式和位元組序。
但是這個BOM頭只是建議新增,不是強制的,所以不少軟體和系統沒有新增這個BOM頭(所以有些軟體格式中有帶BOM頭和NoBOM頭的選擇),這個時候要檢測什麼格式,就比較麻煩了當然可以檢測,但是不能保證100%準確,只能通過編碼範圍從概率上來檢查,雖然準確度還是比較高,但是不能保證100%。所以,時常看到檢測錯誤的軟體,也不奇怪了。

總結:
終於寫完了,其實這些問題都是不統一導致的,屬於歷史問題,所以才會有這些困惑,這裡也呼籲所有的軟體 開發人員自覺的採用Unicode標準進行文書處理,我相信在不久的將來,這些困擾都不會存在了,因為所有軟體都是unicoded ,只要有字型檔,任何文字都能同時顯示,也可以到任何語言的平臺上的去執行,不再有亂碼的困惑! 其實現在絕大部分軟體已經是這麼做的了!

另外也不要被很多名詞屬於所迷惑,其實這些只是標準的問題,根本沒有什麼新的東西,更沒有什麼複雜的東西。

相關文章