作為一個程序員,你應該知道的編碼知識
在諜戰劇里,我們經常看到這樣一個橋段,特工人員,千辛萬苦拿到一條信息,打開一看是一串數字,然后趕緊跑到一個秘密地方,拿出一個密碼本(也可能是一本唐詩選),按照一定規則(只有自己人知道),比如第一個數字表示頁數,第二個數字表示行數,第三個數字表示第幾個字,逐一將信息翻譯出來。如果這個過程中用了錯誤的密碼本,或者不知道規則,那么將會解碼失敗。
計算機的編解碼過程跟上面的過程是一樣一樣的。
計算機只認 0
和 1
,所有的影像和字符最終都會轉換成計算機能夠認識的二進制。一個二進制位(bit)可以表示兩種狀態 0
和 1
,一個字節(byte)由八個二進制位組成,所以一個字節一共可以表示256( 2^8
)種狀態。如果我們規定每種狀態代表一個字符,那么一個字節就可以表達出 256
個字符。
ASCII
計算機是由美國人發明的,所以在最初設計編碼的時候,就只考慮了英文的編碼。英文字符很少,加上一些特殊字符,一共也就100個左右,確切的說是128個。這樣的話用一個字節進行編碼就完全夠了,不僅夠用了,而且還富裕出一位,即第一位一直沒有參與編碼,統一定為 0
。這就是所謂的 ASCII
編碼。在 ASCII
編碼中,空格 SPACE
是 32
(二進制 00100000
),大寫的字母 A
是 65
(二進制 01000001
)。
非ASCII
隨著計算機的普及,歐洲也開始普及計算機,歐洲人發現 ASCII
規定的 128
個字符不能滿足他們的使用,比如,在法語中,字母上方有注音符號,就無法用 ASCII
碼表示。于是,一些歐洲國家就決定,把字節中閑置的第一位編入新的符號。比如,法語中的 é
的編碼為 130
(二進制 10000010
)。這樣一來,這些歐洲國家使用的編碼體系,最多可以表示 256
個符號。這就是大家經常見到的 ISO-8859-1
編碼,也叫 Latin1
編碼。
中文編碼
隨著計算機的普及,國人也開始使用計算機,但是發現按照之前的編碼方式,根本就沒有漢字什么事兒,也就是計算機根本沒辦法認識漢字。
GB2312
為了能夠讓計算機認識漢字,我們決定對漢字進行編碼,本著敢想敢干的精神,我們規定用兩個字節表示一個漢字。
具體規則是這樣的:一個小于 127
的字節代表的意義與原來的 ASCII
相同,但兩個大于 127
的字節連在一起時,就表示這是一個漢字,前面的一個字節稱為高字節,后面一個字節稱為低字節,這樣我們就可以組合出 6763
個簡體漢字。這就是大家常說的 GB2312
編碼。
GBK
很顯然 GB2312
編碼的 6763
個漢字,并不能適應所有的使用場景,比如“喆”字就不再其中,于是在 GB2312
的基礎上又進行了新的擴展,規定只要第一個字節是大于 127
的就OK,至于第二個字節是大于 127
還是小于 127
都無所謂了。經過這樣的改動之后,收錄的漢字及符號就可以達到 2W
多個,這就是我們常說的 GBK
編碼。
再后來,人們繼續對第二個字節進行擴展,發展出了 GB18030
編碼,比 GBK
又多出了一些字符編碼。
至此,所有的漢字編碼都是用兩個字節表示的,但是英文是用一個字節表示。上了一些年紀的程序員都體驗過,一個漢字算兩個英文字符的經歷。
BIG-5
上面提到的都是簡體中文編碼,雖然 GBK
及 GB18030
包含了部分繁體字,但是也不全面,于是臺灣同胞就發了專門支持繁體字的 Big5
編碼,也就是大家經常說的大五碼。
一個小問題
不知道大家有沒有注意到一個問題,在單字節編碼的時候,對于那些大于 127
小于 256
的編碼,在不同的國家代表的字母很可能不一樣。比如, 130
在法語編碼中代表了 é
,在希伯來語編碼中卻代表了字母 Gimel (ג)
,在俄語編碼中又會代表另一個符號。在漢字的雙字節編碼中也存在這樣的問題,比如 BIG5
編碼跟 GBK
編碼都是雙字節編碼,但是代表的漢字卻不一樣。
這就相當于,同樣一串二進制數值,A特工組織按照他們的規則解析出來可能是“你好”,而B特工組織按照他們的規則解析出來可能是“滾蛋”。特工組織之間的翻譯標準不一樣是相當有必要的,但是計算機的編碼規則如果各不相同就比較麻煩了。比如你跟臺灣的志玲姐姐聊天,志玲姐姐用 BIG5
編碼給你發了一封信,然后你用 GBK
去解碼,……,也許就沒有然后了。
Unicode
為了解決上面的問題,有個叫 ISO
的國際標準組織,決定放棄所有區域性編碼,如 BIG5
, GBK
等,重新制定一個新的編碼,這個編碼集將包含所有字符的編碼,這樣大家就都統一了,這套編碼的英文全稱“Universal Multiple-Octet Coded Character Set”,簡稱UCS, 俗稱 “Unicode“。 Unicode
的出現相當于秦始皇對度量衡跟貨幣進行了統一。
Unicdoe
按照日常字符的使用頻繁度劃分了 17
個平面,編號為 0-16
, 0
號平面稱為基本多語言平面(Basic Multilingual Plane,簡稱 BMP
),包含了日常使用最頻繁的字符,編碼范圍從 0000
到 FFFF
,這樣該平面可以表示 2^16=65536
個字符;其它平面的編碼范圍也是從 0000
到 FFFF
,所以其它平面也可以編碼 65535
個字符,這樣 17
個平面一共可以編碼 17×65,536 = 1,114,112
個符號。
我們最常用的 Unicode
編碼使用的是多語言平面的編碼,即所有字符都用兩個字節進行編碼(其它平面可能需要三個或四個字節)。舉個例子比如中國的'中'字 Unicode
碼是 4E2D
,小寫'a'的 Unicode
碼是 0061
.
這里面存在兩個問題,如果所有英文字符都是按照 Unicode
編碼,那么會出現浪費存儲空間的問題。明明一個字節可以搞定的事情,偏偏要用兩個字節。
第二個問題就是計算機如何知道這是 Unicode
編碼還是 ASCII
編碼,也就是 2
個字節表示的一個字符,還是 2
個字符呢。
UTF
UTF
的全稱是 Unicode Transformation Format
,也就是 Unicode
的轉換格式。上面提到了,如果直接使用 Unicode
碼進行存儲會存在浪費空間的問題,而 UTF-8
的出現就是為了解決該問題, UTF-8
使用變長的方式存儲 Unicode
碼,也就是英文字符繼續使用一個字節進行存儲,但是漢字要使用 3
個字節。那么 UTF-8
是如何做到的呢。
首先,對于單字節的符號,字節的第一位設為 0
,后面 7
位為這個符號的 Unicode
碼。因此對于英語字母, UTF-8
編碼和 ASCII
碼是相同的。
其次,對于 n
字節的符號( n > 1
),第一個字節的前 n
位都設為 1
,第 n + 1
位設為 0
,后面字節的前兩位一律設為 10
。剩下的沒有提及的二進制位,全部為這個符號的 Unicode
碼。
下表總結了編碼規則,字母 x
代表可用的編碼位。
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
編碼進行解讀會發現,如果一個字節的第一位是 0
,則這個字節單獨就是一個字符;如果第一位是 1
,則連續有多少個 1
,就表示當前字符占用多少個字節。
舉個例子
假設“hello世界”這樣一個字符串,他們的 Unicode
的編碼分別是
- 1h--0068
- 2e--0065
- 3l--006C
- 4l--006C
- 5o--006F
- 6世--4E16
- 7界--754C
按照 UTF-8
的編碼規則可以得到如下 UTF-8
編碼
- 1h--01101000
- 2e--01100101
- 3l--01101100
- 4l--01101100
- 5o--01101111
- 6世--11100100-10111000-10010110
- 7界--11100111-10010101-10001100
可以看到用 UTF-8
編碼之后,英文字符占用一個字節,而漢字占用了三個字節,一共需要 11
個字節,而如果直接存儲 Unicode
碼則需要 14
個字節。 UTF-8
編碼對于英文來說節省了很大空間,但是對于中文來說增加了空間。
Little endian 和 Big endian
上面提到 Unicode
是用兩個字節表示字符,如果第一個字節在前,就是"大端方式"(Big endian),第二個字節在前就是"小端方式"(Little endian)。'世'字的 Unicode
碼是 4E16
,一個字節是 4E
,一個字節是 16
, 存儲的時候如果 4E
在前就是大端存儲,如果是 16
在前就是小端存儲。
那么計算機是怎么知道一個文件是采用哪種編碼方式呢?
Unicode
規范定義,每一個文件的最前面分別加入一個表示編碼順序的字符,這個字符的名字叫做"零寬度非換行空格"(zero width no-break space),用 FEFF
表示。這正好是兩個字節,而且 FF
比 FE
大 1
。
如果一個文本文件的頭兩個字節是 FE FF
,就表示該文件采用大頭方式;如果頭兩個字節是 FF FE
,就表示該文件采用小頭方式。
總結
UTF-8
編碼是基于 Unicode
字符集的一種編碼實現。現在幾乎所有的編程語言和操作系統都支持 Unicode
編碼,使用 Unicode
編碼之后,再也不會出現上文提到的一個漢字等于兩個英文字符的尷尬局面。
GBK
, BIG5
等都屬于區域性編碼只能在固定范圍內使用,比如 GBK
只適合在簡體中文環境使用,雖然 GBK
相比于 UTF-8
更節省空間,但現在全世界都變成地球村了,所以還是建議大家都使用 UTF-8
編碼。
ANSI
:在 window
下,如果我們用記事本打開文檔,經常會見到 ANSI
編碼方式,這是 Windows
默認的編碼方式。對于英文文檔采用 ASCII
編碼,對于簡體中文文檔采用 GB2312
編碼(只針對 Windows
簡體中文版,如果是繁體中文版會采用 Big5
碼)。