前言
很多程序員對(duì)字符編碼不太理解,當(dāng)然平時(shí)接觸的也不是很多。可能只是大概知道 ASCII、UTF8、GBK、Unicode 等概念。字符串無(wú)法直接通過(guò)網(wǎng)絡(luò)被傳輸(也不能直接被存儲(chǔ)),需要先轉(zhuǎn)換成二進(jìn)制格式,再被還原。所以凡是涉及到網(wǎng)絡(luò)傳輸字符的地方,通常都容易遇到編碼問(wèn)題。在 Java 中最常見的是亂碼,而 Python 開發(fā)中遇到最多的是編碼錯(cuò)誤,如:UnicodeDecodeError, UnicodeEncodeError,幾乎每個(gè) Python 開發(fā)者都會(huì)碰到這種問(wèn)題,對(duì)此都是一籌莫展。如果你正打算入門 Python 那么字符串編碼毫無(wú)疑問(wèn)是你入門 Python 前的必備知識(shí)。Python 中的ord()
、chr()
、len()
、encode()
、decode()
等基本函數(shù)都和字符編碼有這內(nèi)在聯(lián)系。
一、基本概念
-
什么是編碼?
編碼(encode)是把數(shù)據(jù)從一種形式轉(zhuǎn)換為另外一種形式的過(guò)程,它是一套算法,比如這里的字符 A 轉(zhuǎn)換成 01000001 就是一次編碼的過(guò)程,解碼(decode)就是編碼的逆過(guò)程。今天我們討論的是關(guān)于字符的編碼,是字符和二進(jìn)制數(shù)據(jù)之間轉(zhuǎn)換的算法。密碼學(xué)中的加密解密有時(shí)也稱為編碼與解碼 -
什么是字符集?
字符集是一個(gè)系統(tǒng)支持的所有抽象字符的集合。它是各種文字和符號(hào)的總稱,常見的字符集種類包括 ASCII 字符集、GBK 字符集、Unicode字符集等。不同的字符集規(guī)定了有限個(gè)字符,比如:ASCII 字符集只含有拉丁文字字母,GBK 包含了漢字,而 Unicode 字符集包含了世界上所有的文字符號(hào)。
二、ASCII 字符集和字符編碼
ASCII 全稱 American Standard Code for Information Interchange,美國(guó)信息交換標(biāo)準(zhǔn)代碼。
- ASCII 字符集是字母、數(shù)字、標(biāo)點(diǎn)符號(hào)以及控制符(回車、換行、退格)等組成的 128 個(gè)字符。
-
ASCII 字符編碼是將這128個(gè)字符轉(zhuǎn)換為計(jì)算機(jī)可識(shí)別的二進(jìn)制數(shù)據(jù)的一套規(guī)則(算法)。通常來(lái)說(shuō),字符集同時(shí)定義了一套同名的字符編碼規(guī)則,例如 ASCII 就定義了字符集以及字符編碼,當(dāng)然這不是絕對(duì)的,比如 Unicode 就只定義了字符集,而對(duì)應(yīng)的字符編碼是
UTF-8
編碼,UTF-16
編碼。
三、EASCII (擴(kuò)展的ASCII)
首先要知道所謂的 EASCII 是在 ASCII 的基礎(chǔ)上擴(kuò)展而來(lái)的。 ASCII 最初是有美國(guó)人創(chuàng)造的,但隨著計(jì)算機(jī)的不斷普及,西歐語(yǔ)言中還有很多字符不在 ASCII 字符集中,這給他們使用計(jì)算機(jī)造成了很大的限制。于是他們想盡辦法把 ASCII 字符集進(jìn)行擴(kuò)充,認(rèn)為 ASCII 只使用了字節(jié)的前 7 位,如果把第八位也利用起來(lái),那么可表示的字符個(gè)數(shù)就是 256。這就是后來(lái)的 EASCII(Extended ASCII,延伸美國(guó)標(biāo)準(zhǔn)信息交換碼)EASCII 碼比 ASCII 碼擴(kuò)充出來(lái)的符號(hào)包括表格符號(hào)、計(jì)算符號(hào)、希臘字母和特殊的拉丁符號(hào)。
然后 EASCII 并沒(méi)有形成統(tǒng)一的標(biāo)準(zhǔn),各國(guó)各商家都有自己的小算盤,都想在字節(jié)的高位做文章,比如 MS-DOS, IBM PC上使用了各自定義的編碼字符集。為了結(jié)束這種混亂的局面,國(guó)際標(biāo)準(zhǔn)化組織(ISO)及國(guó)際電工委員會(huì)(IEC)聯(lián)合制定的一系列8位元字符集的標(biāo)準(zhǔn),叫 ISO 8859,全稱ISO/IEC 8859,它在 ASCII 基礎(chǔ)之上擴(kuò)展而來(lái),所以完全 ASCII,ISO 8859 字符編碼方案所擴(kuò)展的這128個(gè)編碼中,只有 0xA0 ~ 0xFF(十進(jìn)制為160~255)被使用,其實(shí) ISO 8859 是一組字符集的總稱,旗下共包含了 15 個(gè)字符集,包含了 ISO 8859-1 ~ ISO 8859-15,其中 ISO 8859-1 又稱之為 Latin-1,它是西歐語(yǔ)言,其它的分別代表中歐、南歐、北歐等字符集。
四、GB2312 字符集和字符編碼
再后來(lái)計(jì)算機(jī)就開始在中國(guó)普及,但是漢字博大精深,ASCII 字符集所能表示的字符太有限。多以要處理中文顯然一個(gè)字節(jié)是不夠的,至少需要兩個(gè)字節(jié),而且還不能和ASCII編碼沖突,所以,中國(guó)制定了 GB2312 編碼,用來(lái)把中文編進(jìn)去。每個(gè)漢字符號(hào)由兩個(gè)字節(jié)組成,理論上它可以表示65536個(gè)字符,不過(guò)它只收錄了7445個(gè)字符,6763個(gè)漢字和682個(gè)其他字符,同時(shí)它能夠兼容 ASCII,ASCII 中定義的字符只占用一個(gè)字節(jié)的空間。
GB2312 幾乎收錄已經(jīng)覆蓋中國(guó)大陸絕大多數(shù)漢字,但是對(duì)一些罕見的字和繁體字還有很多少數(shù)民族使用的字符都沒(méi)法處理,于是后來(lái)就在 GB2312 的基礎(chǔ)上創(chuàng)建了一種叫 GBK 的字符編碼,GBK 不僅收錄了27484 個(gè)漢字,同時(shí)還收錄了藏文、蒙文、維吾爾文等主要的少數(shù)民族文字。GBK 是利用了 GB2312 中未被使用的編碼空間上進(jìn)行擴(kuò)充,所以它能完全兼容 GB2312和 ASCII。
GB 18030 是現(xiàn)時(shí)最新的字符集,完全兼容 GB 2312-1980 和 GBK, 每個(gè)字符可以有 1、2、4 個(gè)字節(jié)組成。包含繁體漢字以及日韓漢字。單字節(jié)與 ASCII 兼容,雙字節(jié)與 GBK 標(biāo)準(zhǔn)兼容。
五、Unicode 字符集和編碼
你可以想得到的是,全世界有上百種語(yǔ)言,日本把日文編到Shift_JIS里,韓國(guó)把韓文編到Euc-kr里,各國(guó)有各國(guó)的標(biāo)準(zhǔn),就會(huì)不可避免地出現(xiàn)沖突,結(jié)果就是,在多語(yǔ)言混合的文本中,顯示出來(lái)會(huì)有亂碼。因此,Unicode 字符集應(yīng)運(yùn)而生。它把所有語(yǔ)言都統(tǒng)一到一套字符編碼系統(tǒng)中里,這樣就不會(huì)再有亂碼問(wèn)題了。
Unicode 是一種通用字符集,從字符到 Unicode 字符集中碼位的轉(zhuǎn)換也可以叫做 Unicode 編碼,除此 Unicode 字符可以通過(guò) UTF-8、UTF-16、甚至用 GBK 進(jìn)行編碼。如下代碼是 Python 中的 ASCII編解碼 和 UTF-8 編解碼的代碼范例。雖然下面是很基礎(chǔ)的 Python 代碼,但是對(duì)于沒(méi)有接觸的 Python 的開發(fā)者而言,我有必要簡(jiǎn)單的解釋一下。由于Python的字符串類型是str,在內(nèi)存中以 Unicode 表示,一個(gè)字符對(duì)應(yīng)若干個(gè)字節(jié)。如果要在網(wǎng)絡(luò)上傳輸,或者保存到磁盤上,就需要把str變?yōu)橐宰止?jié)為單位的bytes。Python對(duì)bytes類型的數(shù)據(jù)用帶b
前綴的單引號(hào)或雙引號(hào)表示。要注意區(qū)分'ABC'
和b'ABC'
,前者是str,后者雖然內(nèi)容顯示得和前者一樣,但bytes的每個(gè)字符都只占用一個(gè)字節(jié)。另外還要知道下面代碼快的6、7、8行代碼是錯(cuò)誤提示,這里筆者是故意寫出一個(gè)錯(cuò)誤范例,想讓讀者更好的理解代碼中b
的作用。
>>> 'ABC'.encode('ascii')
b'ABC'
>>> '中文'.encode('utf-8')
b'\xe4\xb8\xad\xe6\x96\x87'
>>> '中文'.encode('ascii')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)
>>> b'ABC'.decode('ascii')
'ABC'
>>> b'\xe4\xb8\xad\xe6\x96\x87'.decode('utf-8')
'中文'
六、UTF-8
編碼
6.1 UTF-8
編碼更節(jié)省空間
ASCII編碼是 1 個(gè)字節(jié),而Unicode編碼通常是 2 個(gè)字節(jié)。
- 字母
A
用ASCII編碼是十進(jìn)制的65,二進(jìn)制的01000001
; - 字符
0
用 ASCII 編碼是十進(jìn)制的48,二進(jìn)制的00110000
,注意字符'0'和整數(shù)0是不同的; - 漢字
”中“
已經(jīng)超出了 ASCII 編碼的范圍,用 Unicode 編碼是十進(jìn)制的20013,二進(jìn)制的01001110 00101101
。
雖然統(tǒng)一成Unicode編碼,可以消除亂碼問(wèn)題但是對(duì)于英文文本而言,用 Unicode 編碼比 ASCII 編碼需要多一倍的存儲(chǔ)空間,十分消耗額外的空間。
所以之后又出現(xiàn)了可變長(zhǎng)編碼的UTF-8編碼
。 UTF-8編碼把一個(gè)Unicode字符根據(jù)不同的數(shù)字大小編碼成1-6個(gè)字節(jié),常用的英文字母被編碼成1個(gè)字節(jié),漢字通常是3個(gè)字節(jié),只有很生僻的字符才會(huì)被編碼成4-6個(gè)字節(jié)。
字符 | ASCII | Unicode | UTF-8 |
---|---|---|---|
A | 01000001 | 00000000 01000001 | 01000001 |
中 | x | 1001110 00101101 | 11100100 10111000 10101101 |
6.2 UTF-8
編碼無(wú)需考慮大小端問(wèn)題
6.2.1 大小端是什么?
- 大端模式,是指數(shù)據(jù)的高字節(jié)保存在內(nèi)存的低地址中,而數(shù)據(jù)的低字節(jié)保存在內(nèi)存的高地址中,這樣的存儲(chǔ)模式有點(diǎn)兒類似于把數(shù)據(jù)當(dāng)作字符串順序處理:地址由小向大增加,而數(shù)據(jù)從高位往低位放;這和我們的閱讀習(xí)慣一致。
- 小端模式,是指數(shù)據(jù)的高字節(jié)保存在內(nèi)存的高地址中,而數(shù)據(jù)的低字節(jié)保存在內(nèi)存的低地址中,這種存儲(chǔ)模式將地址的高低和數(shù)據(jù)位權(quán)有效地結(jié)合起來(lái),高地址部分權(quán)值高,低地址部分權(quán)值低。
下面以u(píng)nsigned int value = 0x12345678為例,分別看看在兩種字節(jié)序下其存儲(chǔ)情況,我們可以用unsigned char buf[4]來(lái)表示value。
Big-Endian: 低地址存放高位,如下:
高地址
---------------
buf[3] (0x78) -- 低位
buf[2] (0x56)
buf[1] (0x34)
buf[0] (0x12) -- 高位
---------------
低地址
Little-Endian: 低地址存放低位,如下:
高地址
---------------
buf[3] (0x12) -- 高位
buf[2] (0x34)
buf[1] (0x56)
buf[0] (0x78) -- 低位
--------------
低地址
6.2.2 為何會(huì)有大小端之分?
于 16 位或者 32 位的處理器,由于寄存器寬度大于一個(gè)字節(jié),那么必然存在著一個(gè)如何將多個(gè)字節(jié)排放的問(wèn)題,因?yàn)椴煌僮飨到y(tǒng)讀取多字節(jié)的順序不一樣,x86和一般的OS(如windows,F(xiàn)reeBSD,Linux)使用的是小端模式。但比如Mac OS是大端模式。因此就導(dǎo)致了大端存儲(chǔ)模式和小端存儲(chǔ)模式的存在。但是實(shí)際中,大端和小端并不涉及到優(yōu)劣的區(qū)分。
6.2.3 UTF-8 為何無(wú)需考慮大小端問(wèn)題?
因?yàn)閁TF-8 的編碼單元是1個(gè)字節(jié),再結(jié)合大小端的定義來(lái)看,所以就不用考慮字節(jié)序問(wèn)題。但是對(duì)于 UTF-16 ,它是用 2 個(gè)字節(jié)來(lái)編碼 Unicode 字符,編碼單位是兩個(gè)字節(jié),2 個(gè)字節(jié)需要確定哪個(gè)放于高位,哪個(gè)放于低位, 因此會(huì)涉及大小端問(wèn)題。
六、現(xiàn)代計(jì)算機(jī)系統(tǒng)通用的字符編碼工作方式
在計(jì)算機(jī)內(nèi)存中,統(tǒng)一使用Unicode編碼,當(dāng)需要保存到硬盤或者需要傳輸?shù)臅r(shí)候,就轉(zhuǎn)換為UTF-8編碼。
用記事本編輯的時(shí)候,從文件讀取的UTF-8字符被轉(zhuǎn)換為Unicode字符到內(nèi)存里,編輯完成后,保存的時(shí)候再把Unicode轉(zhuǎn)換為UTF-8保存到文件。如下圖:
瀏覽網(wǎng)頁(yè)的時(shí)候,服務(wù)器會(huì)把動(dòng)態(tài)生成的Unicode內(nèi)容轉(zhuǎn)換為UTF-8再傳輸?shù)綖g覽器。所以你看到很多網(wǎng)頁(yè)的源碼上會(huì)有類似<meta charset="UTF-8" />
的信息,表示該網(wǎng)頁(yè)正是用的UTF-8編碼。