一直以來常被編碼帶來的亂碼問題所困惑,花了點時間粗略搞懂了這些編碼之間的來龍去脈。
主要參考的是這三篇文章:
1,2,3
ASCII表
Only:口口口口 口口口口
每個“口”代表1bit,隻存放0/1
最大為二進制1111 1111,即最多隻表示到十進制255
思想:8bit=1Byte表示1個字元,是以最多隻能表示2^8=256個字元,一開始美國的計算機設計者隻使用了126個字元,後來計算機發展到全球時,256-126=130個字元裝不完中文、韓文等字元。

ASCII編碼表
GB2312
Case1:口口口口 口口口口
Case2:口口口口 口口口口 口口口口 口口口口
以下采用16進制表示{0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F},如:十進制15表示為16進制F(即0xF),十進制16表示為16進制10(即0x10)。
思想:這是中國人自己設計的漢字字元集和編碼,是擴充ASCII表之後的映射字典,總共收錄了簡化漢字 6763 個,字母和符号 682個。采用2Byte或者1Byte表示一個字元,2Byte表示的字元俗稱“全角字元”;1Byte表示的是半角字元。
編碼方法:規定,一個小于127的字元的意義與ASCII的意義相同,但兩個大于等于127的字元連在一起時,就表示一個漢字!
是以他的編碼順序會出現這樣的情況:從“十六進制B0FE”(十進制45310)直接跳到“十六進制B1A1(十進制45473)”,也就是十進制的45311~45472中間這162個号碼沒有用到哦。
其中,高位位元組用16進制的{A1,A2,…A9,AA,AB,AC,AD,AE,AF,B0,B1,…BF,…,F0…F5,F6,F7}表示;低位位元組用16進制的{A1,…FE}表示。
十六進制的B0A1(B0:高位;A1:低位)= 十進制45217,是以“啊”在GB2312中是第45217号;“阿”的編号是45218号。是以GB2312是對ASCII的擴充,裡面包含了126個ASCII編碼。
GB2312編碼表
GBK
由于中國漢字太多,GB2312編碼方式還是裝不完,後來人們規定,把GB2312中的規定改掉,隻要求高位位元組(Byte)大于等于十進制127就表示漢字的開始,裡面包含了GB2312的所有編碼,并增加了近20000個漢字。
對應與上面的GB2312來說,從“十六進制B0FE”(十進制45310)直接跳到“十六進制B140(十進制45376)”,也就是十進制的45311~45375中間隻有64個号碼沒有用到。
是以GBK是GB2312的擴充,後來加上了少數名族等字元,也就是拓展為了GB18030編碼。
GBK編碼表
Unicode
早期 Only:(口口口口 口口口口)+(口口口口 口口口口)
這是包括了地球上所有文化、所有字母和符号的編碼,它包含了ASCII編碼裡的所有内容,并規定所有的字元都必須用兩個位元組Byte表示(不像GBK或者GB2312那樣,有的用兩個位元組表示,有的隻用一個位元組表示),其中,ASCII表中涉及到的那126個字元,其數字ID與ASCII的數字ID保持一緻(即十六進制44在Unicode和ASCII中都表示的是字元“D”),但他完全沒有考慮到所有國家原有的編碼系統,加上各國的編碼方案和範圍完全不一樣,是以GBK和Unicode之間沒有一個可以互相轉換編碼的公式,隻能通過查表來進行轉換。
在GBK中,中文字元“啊”對應的是十六進制B0A1,即十進制第45217号,而在Unicode中,“啊”對應的是十六進制554A,即十進制第21835号。同時可以看到,GBK中字元“啊”後面接的是字元“阿”;而在Unicode中,“啊”的下一個字元是“啋”(Unicode和GBK之間的編碼順序完全不一緻)。
是以Unicode基本包含了世界上所有國家的字元,但是它和ANSI編碼的編碼方式(順序及編号等)又完全不一樣。
ANSI編碼:2個Byte來表示1個字元的編碼。如:簡體中文Windows作業系統中,ANSI 編碼代表 GBK 編碼;繁體中文Windows作業系統中,ANSI編碼代表Big5;日文Windows作業系統中,ANSI 編碼代表 Shift_JIS 編碼。
漢字的Unicode編碼範圍
UTF-8 、UTF-16、UTF-32、UCS-2與UCS-4
Unicode定義了所有可以用來表示字元的數值集合,如十進制21835(二進制101 0101 0100 1011)映射為中文字元“啊”。但是在儲存檔案和傳輸過程中,計算機隻認識0、1。為了傳輸友善,計算機每次傳輸8個bit或者16個bit,而不會傳輸3bit或者1bit,也就是傳輸方式使用的都是2的次方數量。
是以你可以發明一種編碼方案,如把15個bit補齊為16個bit,簡單的在它的二進制前多加一個0,即0101 0101 0100 1011去存儲這個字元“啊”,這也就是最早的UCS-2編碼,使用固定長度的2Byte去表示一個字元。可是後來還是發現2Byte所能表示的容量還是過小了,是以就拓展到了使用4Byte表示世界上的符号,總共2^32=42億的數字ID呢!可是當人們将16個bit的USC-2拓展為USC-4,此時,“啊”的編碼就變成了0000 0000 0000 0000 0101 0101 0100 1011,這種固定長度簡單的編碼方式變得更加占用空間了,多了太多太多沒有必要的0。
UTF-16則是屬于UCS-2的拓展版本,對于UCS-2中的編碼使用2Byte表示,對于拓展的UCS-2無法容納的字元,則用4Byte對其進行編碼。此時,UTF-8這種可變長的編碼方式才應運而生。
Unicode在網絡上的傳輸,有大概三個标準: UTF-8、UTF-16和UTF-32,即分别每次傳輸 8個bit、16個bit和32個bit,而UTF-16和UTF-32基本無人使用。
舉個栗子:想要儲存“I am 中國人”
GBK儲存:“I am ”包含5個半角字元(兩個空格),他們都的儲存空間是5Byte=40bit;“中國人”包含3個全角字元,儲存空間是6Byte=48bit。是以,GBK儲存這句話需要用到5+6=11Byte。
UCS-2儲存:需要用到8個二位元組的空間,也就是16Byte。
可以看到英文越多,使用固定寬度的位元組去儲存字元浪費的空間越大,但是世界上流行最廣的是英語,加上在很久以前,無論是硬碟大小還是網絡速度,都非常的有限,人們非常希望對傳輸的内容進行壓縮和限制。
UTF-8就是對Unicode再進行一次映射,而且他希望的是,最常用的英文字母,使用最少的位元組,不常用的,反正也不常用,多兩個位元組其實沒關系。
最短的 UTF-8 字元隻需要使用 1 個位元組,最長是 5 個位元組。
UTF-8 的編碼:
1) 對于單位元組的符号,位元組的第一位設為0,後面7位為這個符号的 Unicode 碼。是以對于英語字母,UTF-8 編碼和 ASCII 碼是相同的。
2) 對于n位元組的符号(n > 1),第一個位元組的前n位都設為1,第n+1位設為0,後面位元組的前兩位一律設為10。剩下的沒有提及的二進制位,全部為這個符号的Unicode碼。
Unicode符号範圍 | UTF-8編碼方式
(十六進制) | (二進制)
----------------------+---------------------------------------------
1位元組儲存編碼範圍 | 0xxxxxxx(隻占用8bit)
2位元組儲存編碼範圍 | 110xxxxx 10xxxxxx(16bit)
3位元組儲存編碼範圍| 1110xxxx 10xxxxxx 10xxxxxx(24bit)
4位元組儲存編碼範圍| 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx(32bit)
UTF-8 的解碼:
如果一個位元組的第一位是0,則這個位元組單獨就是一個字元;如果第一位是1,則連續有多少個1,就表示目前字元占用多少個位元組。
如:“你”的 Unicode 是16進制4F60(二進制100 1111 0110 0000),4F60為3位元組儲存編碼範圍,是以UTF-8編碼為:11100100 10111101 10100000,UTF-8的16進制表示則為e4bda0。
win筆記本的一個Bug:建立一個文本檔案時,記事本的編碼預設是ANSI, 如果你在ANSI的編碼輸入漢字,那麼他實際就是GB系列的編碼方式,在這種編碼下,輸入“聯通”兩個字,之後再打開筆記本,你會發現一堆亂碼。
查詢上述GBK的表可以看到,“聯”對應的十六進制數字為C1AA(二進制1100 0001+1010 1010);“通”十六進制為CDA8(二進制1100 1101+1010 1000);第一二個位元組、第三四個位元組的起始部分的都是“110”和“10”,正好與UTF8規則裡的兩位元組模闆是一緻的,于是再次打開記事本時,記事本就誤認為這是一個UTF8編碼的檔案,把第一個位元組的110和第二個位元組的10去掉,我們就得到了“000 0110 1010”,再補上前導的0,即0000 0000 0110 1010,即Unicode的16進制006A,即字母“j”,而“通”字在經過UTF-8解碼後得到Unicode的16進制為0368,這個數字沒有字元映射,是以這二字無法正常顯示。
由此引發了一個比較常見的問題就是:我已經把檔案儲存成了 XX 編碼,為什麼每次打開,還是原來的 YY 編碼?!原因就在于此,你雖然儲存成了 XX 編碼,但是系統識别的時候,卻誤識别為了 YY 編碼,是以還是顯示為 YY 編碼。為了避免這個問題,微軟公司弄出了一個叫 BOM 頭的東西。
可以看到直接以Unicode的原始形式的數字進行固定位元組的編碼儲存是一種極大的浪費,而且也不利于網際網路的傳輸。是以常說的UTF-8編碼準确的來說隻是Unicode數字ID二進制表示的一種壓縮形式。
檔案帶BOM頭的問題
當使用類似 WINDOWS 自帶的記事本等軟體,在儲存一個以UTF-8編碼的檔案時,會在檔案開始的地方插入三個不可見的字元(如:UTF-8的文檔開始插入的0ddxEF 0xBB 0xBF)。它是一串隐藏的字元,用于讓記事本等編輯器識别這個檔案是否以UTF-8編碼。如字元“嚴”的Unicode16進制數字為4E25,使用不同格式儲存這個字元時,不同格式之間的BOM頭如下所示:
1)ANSI:檔案的編碼就是兩個字 節D1 CF,這正是嚴的 GB2312 編碼,這也暗示 GB2312 是采用大頭方式存儲的,沒有BOM頭。
2)UTF-16小端:編碼是四個位元組FF FE 25 4E,其中FF FE表明是小頭方式存儲,真正的編碼是4E25。
3)UTF-16大端:編碼是四個位元組FE FF 4E 25,其中FE FF表明是大頭方式存儲。
3) UTF-8:編碼是六個位元組EF BB BF E4 B8 A5,前三個位元組EF BB BF表示這是UTF-8編碼。
大端小端的問題主要是針對UTF-16這種編碼方式來說的,因為有的存儲器是按大端模式,有的是小端模式,也就是說,在大端模式裡,“嚴”的16進制是高位在前,低位在後,即4E25;小端模式則是低位在前,高位在後,即254E;可是當4E25與254E在編碼中都有對應的字元時,如何判斷到底是哪個字元便成了問題,此時使用BOM也是解決這個方案的方法之一。
windows對于utf-8編碼的檔案自帶BOM,但是其他系統utf-8編碼預設不帶BOM,使用python讀取這類帶BOM頭檔案的時候常常會出現一些問題。如:1,2。是以把檔案儲存為UTF-8的時候,不要帶有 BOM 頭則可以避免這種問題。