字符编码的前世今生--转
原文地址:http://gitbook.cn/books/599d075614d1bc13375caeaf/index.html
前言
很多程序員對字符編碼不太理解,雖然他們大概知道 ASCII、UTF8、GBK、Unicode 等術(shù)語概念,但在寫代碼過程中還是會遇到各種奇怪的編碼問題,在 Java 中最常見的是亂碼,而 Python 開發(fā)中遇到最多的是編碼錯誤,如:UnicodeDecodeError、UnicodeEncodeError,幾乎每個 Python 開發(fā)者都會碰到這種問題,對此都是一籌莫展,這篇文章從字符編碼的起源開始,講述了編程中應(yīng)該如何應(yīng)對編碼的問題,通過理解本文,你可以從容地定位、分析、解決字符編碼相關(guān)的問題。
說到「字符編碼」我們先要理解什么是編碼以及為什么要編碼。
什么是編碼
但凡學(xué)過計算機的同學(xué)都知道,計算機只能處理0和1組成的二進制數(shù)據(jù),人類借助計算機所看到的、聽到的任何信息,包括:文本、視頻、音頻、圖片在計算機中都是以二進制形式進行存儲和運算。計算機善于處理二進制數(shù)據(jù),但是人類對于二進制數(shù)據(jù)顯得捉襟見肘,為了降低人與計算機的交流成本,人們決定把每個字符進行編號,比如給字母 A 的編號是 65,對應(yīng)的二進制數(shù)是「01000001」,當(dāng)把 A 存到計算機中時就用 01000001 來代替,當(dāng)要加載顯示在文件中或網(wǎng)頁中用來閱覽時,就把二進制數(shù)轉(zhuǎn)換成字符 A,這個過程中就會涉及到不同格式數(shù)據(jù)之間的轉(zhuǎn)換。
編碼(encode)是把數(shù)據(jù)從一種形式轉(zhuǎn)換為另外一種形式的過程,它是一套算法,比如這里的字符 A 轉(zhuǎn)換成 01000001 就是一次編碼的過程,解碼(decode)就是編碼的逆過程。今天我們討論的是關(guān)于字符的編碼,是字符和二進制數(shù)據(jù)之間轉(zhuǎn)換的算法。密碼學(xué)中的加密解密有時也稱為編碼與解碼,不過它不在本文討論范圍內(nèi)。
什么是字符集
字符集是一個系統(tǒng)支持的所有抽象字符的集合。它是各種文字和符號的總稱,常見的字符集種類包括 ASCII 字符集、GBK 字符集、Unicode字符集等。不同的字符集規(guī)定了有限個字符,比如:ASCII 字符集只含有拉丁文字字母,GBK 包含了漢字,而 Unicode 字符集包含了世界上所有的文字符號。
有人不禁要問,字符集與字符編碼是什么關(guān)系?別急,先往下面
ASCII:字符集與字符編碼的起源
世界上第一臺計算機,1945年由美國賓夕法尼亞大學(xué)的兩位教授-莫奇利和??颂卦O(shè)計和研制出來,美國人起草了計算機的第一份字符集和編碼標(biāo)準(zhǔn),叫 ASCII(American Standard Code for Information Interchange,美國信息交換標(biāo)準(zhǔn)代碼),一共規(guī)定了 128 個字符及對應(yīng)的二進制轉(zhuǎn)換關(guān)系,128 個字符包括了可顯示的26個字母(大小寫)、10個數(shù)字、標(biāo)點符號以及特殊的控制符,也就是英語與西歐語言中常見的字符,這128個字符用一個字節(jié)來表示綽綽有余,因為一個字節(jié)可以表示256個字符,所以當(dāng)前只利用了字節(jié)的7位,最高位用來當(dāng)作奇偶校驗。如下圖所以,字符小寫 a 對應(yīng) 01100001,大寫 A 對應(yīng) 01000001。
ASCII?字符集是字母、數(shù)字、標(biāo)點符號以及控制符(回車、換行、退格)等組成的128個字符。ASCII?字符編碼是將這128個字符轉(zhuǎn)換為計算機可識別的二進制數(shù)據(jù)的一套規(guī)則(算法)?,F(xiàn)在可以回答前面的那個問題了,通常來說,字符集同時定義了一套同名的字符編碼規(guī)則,例如 ASCII 就定義了字符集以及字符編碼,當(dāng)然這不是絕對的,比如 Unicode 就只定義了字符集,而對應(yīng)的字符編碼是 UTF-8,UTF-16。
ASCII 由美國國家標(biāo)準(zhǔn)學(xué)會制定,1967年定案,最初是美國國家標(biāo)準(zhǔn),后來被國際標(biāo)準(zhǔn)化組織(International Organization for Standardization, ISO)定為國際標(biāo)準(zhǔn),稱為ISO 646標(biāo)準(zhǔn),適用于所有拉丁文字字母。
EASCII:擴展的ASCII
隨著計算機的不斷普及,計算機開始被西歐等國家使用,然后西歐語言中還有很多字符不在 ASCII 字符集中,這給他們使用計算機造成了很大的限制,就好比在中國,你只能用英語跟人家交流一樣。于是乎,他們想著法子把 ASCII 字符集進行擴充,以為 ASCII 只使用了字節(jié)的前 7 位,如果把第八位也利用起來,那么可表示的字符個數(shù)就是 256。這就是后來的 EASCII(Extended ASCII,延伸美國標(biāo)準(zhǔn)信息交換碼)EASCII 碼比 ASCII 碼擴充出來的符號包括表格符號、計算符號、希臘字母和特殊的拉丁符號。
然后 EASCII 并沒有形成統(tǒng)一的標(biāo)準(zhǔn),各國個商家都有自己的小算盤,都想在字節(jié)的高位做文章,比如 MS-DOS, IBM PC上使用了各自定義的編碼字符集,為了結(jié)束這種混亂的局面,國際標(biāo)準(zhǔn)化組織(ISO)及國際電工委員會(IEC)聯(lián)合制定的一系列8位元字符集的標(biāo)準(zhǔn),叫 ISO 8859,全稱ISO/IEC 8859,它在 ASCII 基礎(chǔ)之上擴展而來,所以完全 ASCII,ISO 8859 字符編碼方案所擴展的這128個編碼中,只有0xA0~0xFF(十進制為160~255)被使用,其實 ISO 8859是一組字符集的總稱,旗下共包含了15個字符集,分別是 ISO 8859-1 ~ ISO 8859-15,ISO 8859-1 又稱之為 Latin-1,它是西歐語言,其它的分別代表 中歐、南歐、北歐等字符集。
GB2312:滿足國人需求的字符集
后來,計算機開始普及到了中國,但面臨的一個問題就是字符,漢字博大精深,常用漢字有3500個,已經(jīng)大大超出了 ASCII 字符集所能表示的字符范圍了,即使是 EASCII 也顯得杯水車薪,1981 年國家標(biāo)準(zhǔn)化管理委員會定了一套字符集叫?GB2312,每個漢字符號由兩個字節(jié)組成,理論上它可以表示65536個字符,不過它只收錄了7445個字符,6763個漢字和682個其他字符,同時它能夠兼容 ASCII,ASCII 中定義的字符只占用一個字節(jié)的空間。
GB2312 所收錄的漢字已經(jīng)覆蓋中國大陸99.75%的使用頻率,但是對一些罕見的字和繁體字還有很多少數(shù)民族使用的字符都沒法處理,于是后來就在 GB2312 的基礎(chǔ)上創(chuàng)建了一種叫 GBK 的字符編碼,GBK 不僅收錄了27484 個漢字,同時還收錄了藏文、蒙文、維吾爾文等主要的少數(shù)民族文字。GBK 是利用了 GB2312 中未被使用的編碼空間上進行擴充,所以它能完全兼容 GB2312和 ASCII。而 GB 18030 是現(xiàn)時最新的字符集,兼容 GB 2312-1980 和 GBK, 共收錄漢字70244個,采用多字節(jié)編碼,每個字符可以有1、2、4個字節(jié)組成,某種意義上它能容納161 萬個字符,包含繁體漢字以及日韓漢字,單字節(jié)與ASCII兼容,雙字節(jié)與GBK標(biāo)準(zhǔn)兼容。
Unicode :統(tǒng)一江湖的字符集
盡管我們有了屬于自己的字符集和字符編碼 GBK,可世界上還有很多國家擁有自己的語言和文字,比如日本用 JIS,臺灣用 BIG5,不同國家之間交流起來就很困難,因為沒有統(tǒng)一的編碼標(biāo)準(zhǔn),可能同一個字符,在A國家用兩字字節(jié)存儲,而到了B國家是3個字節(jié),這樣很容易出現(xiàn)編碼問題,于是在 1991 年,國際標(biāo)準(zhǔn)化組織和統(tǒng)一碼聯(lián)盟組織各自開發(fā)了 ISO/IEC 10646(USC)和 Unicode 項目,這兩個項目的目的都是希望用一種字符集來統(tǒng)一全世界所有字符,不過很快雙方都意識到世界上并不需要兩個不兼容的字符集。于是他們就編碼問題進行了非常友好地會晤,決定彼此把工作內(nèi)容合并,雖然項目還是獨立存在,各自發(fā)布各自的標(biāo)準(zhǔn),但前提是兩者必須保持兼容。不過由于 Unicode 這一名字比較好記,因而它使用更為廣泛,成為了事實上的統(tǒng)一編碼標(biāo)準(zhǔn)。
以上是對字符集歷史的一個簡要回顧,現(xiàn)在重點來說說Unicode,Unicode 是一個囊括了世界上所有字符的字符集,其中每一個字符都對應(yīng)有唯一的編碼值(code point),注意了!它不是字符編碼,僅僅是字符集而已,Unicode 字符如何進行編碼,可以是 UTF-8、UTF-16、甚至用 GBK 來編碼。例如:
Unicode 本身并沒有規(guī)定一個字符究竟是用一個還是三個或者四個字節(jié)表示。Unicode 只規(guī)定了每個字符對應(yīng)到唯一的代碼值(code point),代碼值 從 0000 ~ 10FFFF 共 1114112 個值 ,真正存儲的時候需要多少個字節(jié)是由具體的編碼格式?jīng)Q定的。比如:字符 「A」用 UTF-8 的格式編碼來存儲就只占用1個字節(jié),用 UTF-16 就占用2個字節(jié),而用 UTF-32 存儲就占用4個字節(jié)。
UTF-8:Unicode編碼
UTF( Unicode Transformation Format)編碼 和 USC(Universal Coded Character Set) 編碼分別是 Unicode 、ISO/IEC 10646 編碼體系里面兩種編碼方式,UCS 分為 UCS-2 和 UCS-4,而 UTF 常見的種類有 UTF-8、UTF-16、UTF-32。因為 Unicode 與 USC 兩種字符集是相互兼容的,所以這幾種編碼格式也有著對應(yīng)的等值關(guān)系
UCS-2 使用兩個定長的字節(jié)來表示一個字符,UTF-16 也是使用兩個字節(jié),不過 UTF-16 是變長的(網(wǎng)上很多錯誤的說法說 UTF-16是定長的),遇到兩個字節(jié)沒法表示時,會用4個字節(jié)來表示,因此 UTF-16 可以看作是在 UCS-2 的基礎(chǔ)上擴展而來的。而 UTF-32 與 USC-4 是完全等價的,使用4個字節(jié)表示,顯然,這種方式浪費的空間比較多。
UTF-8 的優(yōu)勢是:它以單字節(jié)為單位用 1~4 個字節(jié)來表示一個字符,從首字節(jié)就可以判斷一個字符的UTF-8編碼有幾個字節(jié)。如果首字節(jié)以0開頭,肯定是單字節(jié)編碼,如果以110開頭,肯定是雙字節(jié)編碼,如果是1110開頭,肯定是三字節(jié)編碼,以此類推。除了單字節(jié)外,多字節(jié)UTF-8碼的后續(xù)字節(jié)均以10開頭。
1~4 字節(jié)的 UTF-8 編碼看起來是這樣的:
0xxxxxxx 110xxxxx 10xxxxxx 1110xxxx 10xxxxxx 10xxxxxx 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx-
單字節(jié)可編碼的 Unicode 范圍:\u0000~\u007F(0~127)
-
雙字節(jié)可編碼的 Unicode 范圍:\u0080~\u07FF(128~2047)
-
三字節(jié)可編碼的 Unicode 范圍:\u0800~\uFFFF(2048~65535)
-
四字節(jié)可編碼的 Unicode 范圍:\u10000~\u1FFFFF(65536~2097151)
UTF-8 兼容了 ASCII,在數(shù)據(jù)傳輸和存儲過程中節(jié)省了空間,其二是UTF-8 不需要考慮大小端問題。這兩點都是 UTF-16 的劣勢。不過對于中文字符,用 UTF-8 就要用3個字節(jié),而 UTF-16 只需2個字節(jié)。而UTF-16 的優(yōu)點是在計算字符串長度,執(zhí)行索引操作時速度會很快。Java 內(nèi)部使用 UTF-16 編碼方案。而 Python3 使用 UTF-8。UTF-8 編碼在互聯(lián)網(wǎng)領(lǐng)域應(yīng)用更加廣泛。
來看一張圖,下圖是Windows平臺保存文件時可選擇的字符編碼類型,你可以指定系統(tǒng)以什么樣的編碼格式來存儲文件,ANSI 是 ISO 8859-1的超集,之所以在 Windows下有 Unicode 編碼這樣一種說法,其實是 Windows 的一種錯誤表示方法,或許是因為歷史原因一直沿用至今,其實它真正表示的是 UTF-16 編碼,更具體一點是 UTF-16小端,什么是大端和小端呢?
大端與小端
大小端是數(shù)據(jù)在存儲器中的存放順序,大端模式,是指數(shù)據(jù)的高字節(jié)在前,保存在內(nèi)存的低地址中,與人類的讀寫法一致,數(shù)據(jù)的低字節(jié)在后,保存在內(nèi)存的高地址中,小端與之相反,小端模式,是指數(shù)據(jù)的高字節(jié)在后,保存在內(nèi)存的高地址中,而數(shù)據(jù)的低字節(jié)在前,保存在內(nèi)存的低地址中例如,十六進制數(shù)值?0x1234567的大端字節(jié)序和小端字節(jié)序的寫法:
至于為什么會有大端和小端之分呢?對于 16 位或者 32 位的處理器,由于寄存器寬度大于一個字節(jié),那么必然存在著一個如何將多個字節(jié)排放的問題,因為不同操作系統(tǒng)讀取多字節(jié)的順序不一樣,,x86和一般的OS(如windows,FreeBSD,Linux)使用的是小端模式。但比如Mac OS是大端模式。因此就導(dǎo)致了大端存儲模式和小端存儲模式的存在,兩者并沒有孰優(yōu)孰劣。
為什么UTF-8不需要考慮大小端問題?
UTF-8 的編碼單元是1個字節(jié),所以就不用考慮字節(jié)序問題。而 UTF-16 是用 2個字節(jié)來編碼 Unicode 字符,編碼單位是兩個字節(jié),因此需要考慮字節(jié)序問題,因為2個字節(jié)哪個存高位哪個存低位需要確定。
Python2 中的字符編碼
現(xiàn)在總算把理論說完了,再來說說 Python 中的編碼問題,也是每個Python開發(fā)者最關(guān)心、最經(jīng)常遇到的問題,Python 的誕生時間比 Unicode 還要早幾年,所以,Python的第一個版本一直延續(xù)到Python2.7,Python 的默認(rèn)編碼都是 ASCII
所以在 Python 源代碼,要能夠正常保存中文字符就必須先指定utf
8 或者 gbk 格式
# coding=utf-8或者是:#!/usr/bin/python # -*- coding: utf-8 -*-str 與 unicode
在前面我們介紹過字符,這里還有必要重復(fù)一下字符和字節(jié)的區(qū)別,字符就是一個符號,比如一個漢字、一個字母、一個數(shù)字、一個標(biāo)點都可以稱為一個字符,而字節(jié)就是字符就是編碼之后轉(zhuǎn)換而成的二進制序列,一個字節(jié)是8個比特位。例如字符 "p" 存儲到硬盤是一串二進制數(shù)據(jù) 01110000,占用一個字節(jié)。字節(jié)方便存儲和網(wǎng)絡(luò)傳輸,而字符用于顯示方便閱讀。
在Python2中,字符與字節(jié)的表示很微妙,兩者的界限很模糊,Python2 中把字符串分為 unicode 和 str 兩種類型。本質(zhì)上 str 類型是二進制字節(jié)序列, unicode 類型的字符串是字符,下面的示例代碼可以看出 str 類型的 "禪" 打印出來是十六進制的 \xec\xf8 ,對應(yīng)的二進制字節(jié)序列就是 '11101100 11111000'。
而 unicode 類型的 u"禪" 對應(yīng)的 unicode 符號是 u'\u7985'
我們要把 unicode 字符保存到文件或者傳輸?shù)骄W(wǎng)絡(luò)就需要經(jīng)過編碼處理轉(zhuǎn)換成二進制形式的 str 類型,于是 python 的字符串提供了 encode 方法,從 unicode 轉(zhuǎn)換到 str,反之亦然。
encode
decode
不少初學(xué)者怎么也記不住 str 與 unicode 之間的轉(zhuǎn)換用 encode 還是 decode,如果你記住了 str 本質(zhì)上其實是一串二進制數(shù)據(jù),而 unicode 是字符(符號),編碼(encode)就是把字符(符號)轉(zhuǎn)換為 二進制數(shù)據(jù)的過程,因此 unicode 到 str 的轉(zhuǎn)換要用 encode 方法,反過來就是用 decode 方法。
encoding always takes a Unicode string and returns a bytes sequence, and decoding always takes a bytes sequence and returns a Unicode string".
清楚了 str 與 unicode 之間的轉(zhuǎn)換關(guān)系之后,我們來看看什么時候會出現(xiàn) UnicodeEncodeError、UnicodeDecodeError 錯誤。
UnicodeEncodeError
UnicodeEncodeError 發(fā)生在 unicode 字符串轉(zhuǎn)換成 str 字節(jié)序列的時候,來看一個例子,把一串 unicode 字符串保存到文件
# -*- coding:utf-8 -*- def main(): name = u'Python之禪' f = open("output.txt", "w") f.write(name)錯誤日志
UnicodeEncodeError: 'ascii' codec can't encode characters in position 6-7: ordinal not in range(128)
為什么會出現(xiàn) UnicodeEncodeError?
因為調(diào)用 write 方法時,程序會把字符經(jīng)過編碼轉(zhuǎn)換成二進制字節(jié)序列,內(nèi)部會有 unicode 到 str 的編碼轉(zhuǎn)換過程,程序會先判斷字符串是什么類型,如果是 str,就直接寫入文件,不需要編碼,因為 str 類型的字符串本身就是一串二進制的字節(jié)序列了。如果字符串是 unicode 類型,那么它會先調(diào)用 encode 方法把 unicode 字符串轉(zhuǎn)換成二進制形式的 str 類型,才保存到文件,而 Python2中,encode 方法默認(rèn)使用 ascii 進行 encde.
相當(dāng)于:
但是,我們知道 ASCII 字符集中只包含了128個拉丁字母,不包括中文字符,因此 出現(xiàn)了 'ascii' codec can't encode characters 的錯誤。要正確地使用 encode ,就必須指定一個包含了中文字符的字符集,比如:UTF-8、GBK。
所以要把 unicode 字符串正確地寫入文件,就應(yīng)該預(yù)先把字符串進行 UTF-8 或 GBK 編碼轉(zhuǎn)換。
def main():name = u'Python之禪' name = name.encode('utf-8') with open("output.txt", "w") as f: f.write(name)或者直接寫str類型的字符串
def main():name = 'Python之禪' with open("output.txt", "w") as f: f.write(name)當(dāng)然,把 unicode 字符串正確地寫入文件不止一種方式,但原理是一樣的,這里不再介紹,把字符串寫入數(shù)據(jù)庫,傳輸?shù)骄W(wǎng)絡(luò)都是同樣的原理
UnicodeDecodeError
UnicodeDecodeError 發(fā)生在 str 類型的字節(jié)序列解碼成 unicode 類型的字符串時
把一個經(jīng)過 UTF-8 編碼后生成的字節(jié)序列 '\xe7\xa6\x85' 再用 GBK 解碼轉(zhuǎn)換成 unicode 字符串時,出現(xiàn) UnicodeDecodeError,因為 (對于中文字符)GBK 編碼只占用兩個字節(jié),而 UTF-8 占用3個字節(jié),用 GBK 轉(zhuǎn)換時,還多出一個字節(jié),因此它沒法解析。避免 UnicodeDecodeError 的關(guān)鍵是保持 編碼和解碼時用的編碼類型一致。
這也回答了文章開頭說的字符 "禪",保存到文件中有可能占3個字節(jié),有可能占2個字節(jié),具體處決于 encode 的時候指定的編碼格式是什么。
再舉一個 UnicodeDecodeError 的例子
str 與 unicode 字符串 執(zhí)行 + 操作時,Python 會把 str 類型的字節(jié)序列隱式地轉(zhuǎn)換成(解碼)成 和 x 一樣的 unicode 類型,但Python是使用默認(rèn)的 ascii 編碼來轉(zhuǎn)換的,而 ASCII字符集中不包含有中文,所以報錯了。相當(dāng)于:
正確地方式應(yīng)該是找到一種包含有中文字符的字符編碼,比如 UTF-8或者 GBK 顯示地把 y 進行解碼轉(zhuǎn)換成 unicode 類型
Python3中的字符串與字節(jié)序列
Python3對字符串和字符編碼進行了很徹底的重構(gòu),完全不兼容Python2,同時也很多想遷移到Python3的項目帶來了很大的麻煩,Python3 把系統(tǒng)默認(rèn)編碼設(shè)置為 UTF-8,字符和二進制字節(jié)序列區(qū)分得更清晰,分別用 str 和 bytes 表示。文本字符全部用 str 類型表示,str 能表示 Unicode 字符集中所有字符,而二進制字節(jié)數(shù)據(jù)用一種全新的數(shù)據(jù)類型,用 bytes 來表示,盡管Python2中也有bytes類型,但那只不過是str的一個別名。
str
bytes
Python3 中,在字符引號前加‘b’,明確表示這是一個 bytes 類型的對象,實際上它就是一組二進制字節(jié)序列組成的數(shù)據(jù),bytes 類型可以是 ASCII范圍內(nèi)的字符和其它十六進制形式的字符數(shù)據(jù),但不能用中文等非ASCII字符表示。
bytes 類型提供的操作和 str 一樣,支持分片、索引、基本數(shù)值運算等操作。但是 str 與 bytes 類型的數(shù)據(jù)不能執(zhí)行?+?操作,盡管在python2中是可行的。
python2 與 python3 字節(jié)與字符對比
| str | bytes | 字節(jié) | encode | 存儲 |
| unicode | str | 字符 | decode | 顯示 |
總結(jié)
字符編碼本質(zhì)上是字符到字節(jié)的轉(zhuǎn)換過程
字符集的演進過程是:ascii、eascii、ios8895-x,gb2312... Unicode
Unicode是字符集,對應(yīng)的編碼格式有UTF-8,UTF-16
字節(jié)序列存儲的時候有大小端之分
python2中字符與字節(jié)分別用unicode和str類型表示
python3中字符與字節(jié)分別用str與bytes表示
參考鏈接
- https://en.wikipedia.org/wiki/Unicode
- https://en.wikipedia.org/wiki/UTF-32
- https://en.wikipedia.org/wiki/UTF-16
- https://zh.wikipedia.org/wiki/%E4%BD%8D%E5%85%83%E7%B5%84%E9%A0%86%E5%BA%8F%E8%A8%98%E8%99%9F
- https://zh.wikipedia.org/wiki/%E9%80%9A%E7%94%A8%E5%AD%97%E7%AC%A6%E9%9B%86
- https://en.wikipedia.org/wiki/Universal_Coded_Character_Set
- http://unicode.org/faq/utf_bom.html
- http://www.fmddlmyy.cn/text6.html
- http://stackoverflow.com/questions/643694/utf-8-vs-unicode
- http://stackoverflow.com/questions/700187/unicode-utf-ascii-ansi-format-differences
- https://www.meridiandiscovery.com/articles/unicode-and-character-encodings/
- https://www.praim.com/character-encodings-linux-ascii-utf-8-iso-8859
- http://stackoverflow.com/questions/4655250/difference-between-utf-8-and-utf-16
- http://www.guokr.com/blog/83367/
轉(zhuǎn)載于:https://www.cnblogs.com/davidwang456/p/7766956.html
總結(jié)
以上是生活随笔為你收集整理的字符编码的前世今生--转的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 当我说要做大数据工程师时他们都笑我,直到
- 下一篇: Kibana查询说明