一直以來,計算機編碼問題像幽靈一般,不少開發(fā)人員都受過它的困擾。
試想你請求一個數(shù)據(jù),卻得到一堆亂碼,丈二和尚摸不著頭腦。有同事質(zhì)疑你的數(shù)據(jù)是亂碼,雖然你很確定傳了 UTF-8 ,卻也無法自證清白,更別說幫同事 debug 了。
本文從 ASCII 碼說起,帶你扒一扒編碼背后那些事。相信搞清編碼的原理后,你將不再畏懼任何編碼問題。
現(xiàn)代計算機技術(shù)起源于美國,最先遇到的也是英文文本。英文文本一般由 26 個小寫字母、26個大寫字母,外加 0-9 共個10數(shù)字以及若干!$特殊符號組成,共計也不過 100個左右。
而計算機中最基本的存儲單位為 字節(jié) ( byte ),由 8 個比特位( bit )組成,也叫做 八位字節(jié) ( octet )。8 個比特位可以表示 2^8 = 256 個字符,看上去完全可用一個字節(jié)來存儲英文字符。
計算機先驅(qū)們也是這么想的。他們?yōu)槊總€英文字符編號,再加上一些控制符,形成了我們所熟知的 ASCII 碼表。實際上,由于英文字符不多,他們只用了字節(jié)的后 7 位而已。
ASC編碼表
根據(jù) ASCII 碼表,參考上面表,由 01000001 這 8 個比特位組成的八位字節(jié),代表字母 A。
因為01000001對應(yīng)的十進制為65,所以,通常使用下表表示ASC編碼,以方便教學。
128位的ASCII編碼表
西歐人民來了,他們主要使用拉丁字母語言。與英語類似,拉丁字母數(shù)量并不大,大概也就是幾十個。于是,西歐人民打起 ASCII 碼表那個未用的比特位( b8 )的主意。
還記得嗎?ASCII 碼表總共定義了 128 個字符,范圍在 0~127 之間(參考上圖),字節(jié)最高位 b8 暫未使用。于是,西歐人民將拉丁字母和一些輔助符號(如歐元符號)定義在 128~255 之間。這就構(gòu)成了 Latin1 ,它是一個 8 位字符集,定義了以下字符:
256的ASC編碼表
圖中綠色部分是不可打印的( unprintable )控制字符,左半部分8列是 ASCII 碼,右邊部分8列是擴展碼。因此,latin1 字符集是 ASCII 碼的超集:
一個字節(jié)掰成兩半,歐美兩兄弟各用一半。至此,歐美人民都玩嗨了,東亞人民呢?
由于受到漢文化的影響,東亞地區(qū)主要是漢字圈,我們便以中文為例展開介紹(日韓泰等類似)。
漢字有什么特點呢?—— 最大的問題是:數(shù)量多!光常用漢字就有幾千個,這可不是一個字節(jié)能勝任的。一個字節(jié)不夠就兩個唄。道理雖然如此,操作起來卻未必這么簡單。
首先,將需要編碼的漢字和 ASCII 碼整理成一個字符集,同時還需要兼容現(xiàn)有的ASCII編碼 。為什么需要 ASCII 碼呢?因為,在計算機世界,不可避免要跟數(shù)字、英文字母打交道。至于拉丁字母,重要性就沒那么大。
GB2312 字符集總共收錄了 6 千多個漢字,用兩個字節(jié)來表示足矣,但事情遠沒有這么簡單。同樣的數(shù)字字符,物理、化學符號等。在 GB2312 中占用 2 個字節(jié),在 ASCII 碼中占用 1 個字節(jié),這不就不兼容了嗎?計算機里太多東西涉及 ASCII 碼了,看看一個 http 請求:
GET / HTTP/1.1Host: www.example.com
那么,怎么兼容 GB2312 和 ASCII 碼呢?天無絕人之路, 變長 編碼方案應(yīng)運而生。
變長編碼方案,字符由長度不一的字節(jié)表示,有些字符只需 1 字節(jié),有些需要 2 字節(jié),甚至有些需要更多字節(jié)。GB2312 中的 ASCII 碼與原來保持一致,還是用一個字節(jié)來表示,這樣便解決了兼容問題。
在 GB2312 中,如果一個字節(jié)最高位 b8 為 0 ,該字節(jié)便是單字節(jié)編碼,即 ASCII 碼。如果字節(jié)最高位 b8 為 1 ,它就是雙字節(jié)編碼的首字節(jié),與其后字節(jié)一起表示一個字符。
變長編碼方案目的在于兼容 ASCII 碼,但也帶來一個問題:由于字節(jié)編碼長度不一,定位第 N 個字符只能通過遍歷實現(xiàn),時間復雜度從 O(1) 退化到 O(N) 。好在這種操作場景并不多見,因此影響可以忽略。
GB2312 收錄的漢字個數(shù)只有常用的 6 千多個,遇到生僻字還是無能為力。因此,后來又推出了 GBK 和 GB18030 字符集。GBK 是 GB2312 的超集,完全兼容 GB2312 ;而 GB18030 又是 GBK 的超集,完全兼容 GBK 。
因此,對中文編碼文本進行解碼,指定 GB18030 最為健壯:
>>> raw = b'\xfd\x88\xb5\xc4\xb4\xab\xc8\xcb'>>> raw.decode('gb18030')'龍的傳人'
指定 GBK 或 GB2312 就只好看運氣了,GBK 多半還沒事:
>>> raw.decode('gbk')'龍的傳人'
GB2312 經(jīng)常直接拋錨不商量:
>>> raw.decode('gb2312')Traceback (most recent call last):File '<stdin>', line 1, in <module>UnicodeDecodeError: 'gb2312' codec can't decode byte 0xfd in position 0: illegal multibyte sequence
chardet 是一個不錯的文本編碼檢測庫,用起來很方便,但對中文編碼支持不是很好。經(jīng)常中文編碼的文本進去,檢測出來的結(jié)果是 GB2312,但一用 GB2312 解碼就跪:
>>> import chardet>>> raw = b'\xd6\xd0\xb9\xfa\xc8\xcb\xca\xc7\xfd\x88\xb5\xc4\xb4\xab\xc8\xcb'>>> chardet.detect(raw){'encoding': 'GB2312', 'confidence': 0.99, 'language': 'Chinese'}>>> raw.decode('GB2312')Traceback (most recent call last):File '<stdin>', line 1, in <module>UnicodeDecodeError: 'gb2312' codec can't decode byte 0xfd in position 8: illegal multibyte sequence
掌握 GB2312 、 GBK 、 GB18030 三者的關(guān)系后,我們可以略施小計。如果 chardet 檢測出來結(jié)果是 GB2312 ,就用 GB18030 去解碼,大概率可以成功!
>>> raw.decode('GB18030')'中國人是龍的傳人'
GB2312 、 GBK 與 GB18030 都是中文編碼字符集。雖然 GB18030 也包含日韓表意文字,算是國際字符集,但畢竟是以中文為主,無法適應(yīng)全球化應(yīng)用。
在計算機發(fā)展早期,不同國家都推出了自己的字符集和編碼方案,互不兼容。中文編碼的文本在使用日文編碼的系統(tǒng)上是無法顯示的,這就給國際交往帶來障礙。
這時,英雄出現(xiàn)了。統(tǒng)一碼聯(lián)盟 站出來說要發(fā)展一個通用的字符集,收錄世界上所有字符,這就是 Unicode 。經(jīng)過多年發(fā)展, Unicode 已經(jīng)成為世界上最通用的字符集,也是計算機科學領(lǐng)域的業(yè)界標準。
Unicode 已經(jīng)收錄的字符數(shù)量已經(jīng)超過 13 萬個,每個字符需占用超過 2 字節(jié)。由于常用編程語言一般沒有 24 位數(shù)字類型,因此一般用 32位數(shù)字表示一個字符。這樣一來,同樣的一個英文字母,在 ASCII 中只需占用 1 字節(jié),在 Unicode 則需要占用 4 字節(jié)!英美人民都要哭了,試想你磁盤中的文件大小都增大了 4 倍是什么感受!
為了兼容 ASCII 并優(yōu)化文本空間占用,我們需要一種變長字節(jié)編碼方案,這就是著名的 UTF-8 。與 GB2312 等中文編碼一樣,UTF-8 用不固定的字節(jié)數(shù)來表示字符:
如圖,以 0 開頭的字節(jié)為 單字節(jié) 編碼,總共 7 個有效編碼位,編碼范圍為 U+0000 至 U+007F ,剛好對應(yīng) ASCII 碼所有字符。以 110開頭的字節(jié)為 雙字節(jié) 編碼,總共 11 個有效編碼位,最大值是 0x7FF ,因此編碼范圍為 U+0080 至 U+07FF ;以 1110 開頭的字節(jié)為 三字節(jié) 編碼,總共 16 個有效編碼位,最大值是 0xFFFF 因此編碼范圍為 U+0800 至 U+FFFF 。
根據(jù)開頭不同, UTF-8 流中的字節(jié),可以分為以下幾類:
字節(jié)最高位類別有效位0單字節(jié)編碼710多字節(jié)編碼非首字節(jié)110雙字節(jié)編碼首字節(jié)111110三字節(jié)編碼首字節(jié)1611110四字節(jié)編碼首字節(jié)21111110五字節(jié)編碼首字節(jié)261111110六字節(jié)編碼首字節(jié)31
至此,我們已經(jīng)具備了讀懂 UTF-8 編碼字節(jié)流的能力,不信來看一個例子:
除了UTF-8,還有UTF-16,UTF-32編碼,這些編碼設(shè)計到更復雜的程序現(xiàn)實。例如在上面,我們講過英文使用一個字節(jié),漢字 使用兩個字節(jié)。
這使得在程序里,計算字符長度出現(xiàn)了不同,例如
string word1='hi'; string word2='你好'; int length1=word1.Length; int lenth2=word2.Length;
當計算字符長度時, length1表示的長度表示的是2, length2的長度也是2,但是length1是2個字節(jié),而length2是四個字節(jié),導致意想不到的結(jié)果。
UTF-16 LE是windows上默認的Unicode編碼方式,在C++編程里,使用wchar_t表示。所有wchar_t *類型的字符串 VC都自動采用UTF-16的編碼。
但是由于沒有統(tǒng)一的表示UTF-16編碼的字符類型。C++98/03里對wchar_t的定義是非常寬泛的。這導致在Windows平臺上,wchar_t是2字節(jié)的;在Unix-like系統(tǒng)上是4字節(jié)的。代碼移植上,可能會遇到挑戰(zhàn)
UTF-32
優(yōu)勢這個優(yōu)勢就明顯了,所有字符都是4字節(jié),固定長度,劣勢是對以英文為主的字符串來說,空間消耗大。例如一個100M文件,突然變成400M,用戶會傻眼。在記事本上,另存文件時,可以選擇編碼類型。
一直以來,字符集 和 編碼 這兩個詞一直是混著用的?,F(xiàn)在,我們總算有能力厘清這兩者間的關(guān)系了。
字符集 顧名思義,就是由一定數(shù)量字符組成的集合,每個字符在集合中有唯一編號。前文提及的 ASCII 、 latin1 、 GB2312 、GBK 、GB18030 以及 Unicode 等,無一例外,都是字符集。
計算機存儲和網(wǎng)絡(luò)通訊的基本單位都是 字節(jié) ,因此文本必須以 字節(jié)序列 的形式進行存儲或傳輸。那么,字符編號如何轉(zhuǎn)化成字節(jié)呢?這就是 編碼 要回答的問題。
在 ASCII 碼和 latin 中,字符編號與字節(jié)一一對應(yīng),這是一種編碼方式。GB2312 則采用變長字節(jié),這是另一種編碼方式。而 Unicode 則存在多種編碼方式,除了 最常用的 UTF-8 編碼,還有 UTF-16 等。實際上,UTF-16 編碼效率比 UTF-8 更高,但由于無法兼容 ASCII ,應(yīng)用范圍受到很大制約。
認識文本編碼的前世今生之后,應(yīng)該如何規(guī)避編碼問題呢?是否存在一些最佳實踐呢?答案是肯定的。
項目開始前,需要選擇一種適應(yīng)性廣的編碼方案,UTF-8 是首選,好處多多:
如因歷史原因,不得不使用中文編碼方案,則優(yōu)先選擇 GB18030 。這個標準最新,涵蓋字符最多,適應(yīng)性最強。盡量避免采用 GBK ,特別是 GB2312 等老舊編碼標準。
如果你使用的編程語言,字符串類型支持 Unicode ,那問題就簡單了。由于 Unicode 字符串肯定不會導致諸如亂碼等編碼問題,你只需在輸入和輸出環(huán)節(jié)稍加留意。
舉個例子,Python 從 3 以后, str 就是 Unicode 字符串了,而 bytes 則是 字節(jié)序列 。因此,在 Python 3 程序中,核心邏輯應(yīng)該統(tǒng)一用 str 類型,避免使用 bytes 。文本編碼、解碼操作則統(tǒng)一在程序的輸入、輸出層中進行。
假如你正在開發(fā)一個 API 服務(wù),數(shù)據(jù)庫數(shù)據(jù)編碼是 GBK ,而用戶卻使用 UTF-8 編碼。那么,在程序 輸入層 , GBK 數(shù)據(jù)從數(shù)據(jù)庫讀出后,解碼轉(zhuǎn)換成 Unicode 數(shù)據(jù),再進入核心層處理。在程序 核心層 ,數(shù)據(jù)以 Unicode 形式進行加工處理。由于核心層處理邏輯可能很復雜,統(tǒng)一采用 Unicode 可以減少問題的發(fā)生。最后,在程序的 輸出層 將數(shù)據(jù)以 UTF-8 編碼,再返回給客戶端。
整個過程偽代碼大概如下:
# input# read gbk data from database and decode it to unicodedata = read_from_database().decode('gbk')# core# process unicode data onlyresult = process(data)# output# encoding unicode data into utf8response_to_user(result.encode('utf8'))
這樣的程序結(jié)構(gòu)看起來跟個三明治一樣,非常形象:
當然了,還有很多編程語言字符串還不支持 Unicode 。Python 2 中的 str 對象,跟 Python 3 中的 bytes 比較像,只是字節(jié)序列;C 語言中的字符串甚至更原始。
這都無關(guān)緊要,好的編程習慣是相通的:程序核心層統(tǒng)一使用某種編碼,輸入輸出層則負責編碼轉(zhuǎn)換。至于核心層使用何種編碼,主要看程序中哪種編碼使用最多,一般是跟數(shù)據(jù)庫編碼保持一致即可。
注:本文主要來源博客園的fasionchan編寫的《一文說清文本編碼那些事》,略有改動。
聯(lián)系客服