1. 程式人生 > >字符編碼的前世今生--轉

字符編碼的前世今生--轉

tab 當前 小端模式 成本 這一 mat port discover 演進

原文地址:http://gitbook.cn/books/599d075614d1bc13375caeaf/index.html

前言

很多程序員對字符編碼不太理解,雖然他們大概知道 ASCII、UTF8、GBK、Unicode 等術語概念,但在寫代碼過程中還是會遇到各種奇怪的編碼問題,在 Java 中最常見的是亂碼,而 Python 開發中遇到最多的是編碼錯誤,如:UnicodeDecodeError、UnicodeEncodeError,幾乎每個 Python 開發者都會碰到這種問題,對此都是一籌莫展,這篇文章從字符編碼的起源開始,講述了編程中應該如何應對編碼的問題,通過理解本文,你可以從容地定位、分析、解決字符編碼相關的問題。

說到「字符編碼」我們先要理解什麽是編碼以及為什麽要編碼。

什麽是編碼

但凡學過計算機的同學都知道,計算機只能處理0和1組成的二進制數據,人類借助計算機所看到的、聽到的任何信息,包括:文本、視頻、音頻、圖片在計算機中都是以二進制形式進行存儲和運算。計算機善於處理二進制數據,但是人類對於二進制數據顯得捉襟見肘,為了降低人與計算機的交流成本,人們決定把每個字符進行編號,比如給字母 A 的編號是 65,對應的二進制數是「01000001」,當把 A 存到計算機中時就用 01000001 來代替,當要加載顯示在文件中或網頁中用來閱覽時,就把二進制數轉換成字符 A,這個過程中就會涉及到不同格式數據之間的轉換。

編碼(encode)是把數據從一種形式轉換為另外一種形式的過程,它是一套算法,比如這裏的字符 A 轉換成 01000001 就是一次編碼的過程,解碼(decode)就是編碼的逆過程。今天我們討論的是關於字符的編碼,是字符和二進制數據之間轉換的算法。密碼學中的加密解密有時也稱為編碼與解碼,不過它不在本文討論範圍內。

技術分享

什麽是字符集

字符集是一個系統支持的所有抽象字符的集合。它是各種文字和符號的總稱,常見的字符集種類包括 ASCII 字符集、GBK 字符集、Unicode字符集等。不同的字符集規定了有限個字符,比如:ASCII 字符集只含有拉丁文字字母,GBK 包含了漢字,而 Unicode 字符集包含了世界上所有的文字符號。

有人不禁要問,字符集與字符編碼是什麽關系?別急,先往下面

ASCII:字符集與字符編碼的起源

世界上第一臺計算機,1945年由美國賓夕法尼亞大學的兩位教授-莫奇利和埃克特設計和研制出來,美國人起草了計算機的第一份字符集和編碼標準,叫 ASCII(American Standard Code for Information Interchange,美國信息交換標準代碼),一共規定了 128 個字符及對應的二進制轉換關系,128 個字符包括了可顯示的26個字母(大小寫)、10個數字、標點符號以及特殊的控制符,也就是英語與西歐語言中常見的字符,這128個字符用一個字節來表示綽綽有余,因為一個字節可以表示256個字符,所以當前只利用了字節的7位,最高位用來當作奇偶校驗。如下圖所以,字符小寫 a 對應 01100001,大寫 A 對應 01000001。

技術分享

ASCII 字符集是字母、數字、標點符號以及控制符(回車、換行、退格)等組成的128個字符。ASCII 字符編碼是將這128個字符轉換為計算機可識別的二進制數據的一套規則(算法)。現在可以回答前面的那個問題了,通常來說,字符集同時定義了一套同名的字符編碼規則,例如 ASCII 就定義了字符集以及字符編碼,當然這不是絕對的,比如 Unicode 就只定義了字符集,而對應的字符編碼是 UTF-8,UTF-16。

ASCII 由美國國家標準學會制定,1967年定案,最初是美國國家標準,後來被國際標準化組織(International Organization for Standardization, ISO)定為國際標準,稱為ISO 646標準,適用於所有拉丁文字字母。

EASCII:擴展的ASCII

隨著計算機的不斷普及,計算機開始被西歐等國家使用,然後西歐語言中還有很多字符不在 ASCII 字符集中,這給他們使用計算機造成了很大的限制,就好比在中國,你只能用英語跟人家交流一樣。於是乎,他們想著法子把 ASCII 字符集進行擴充,以為 ASCII 只使用了字節的前 7 位,如果把第八位也利用起來,那麽可表示的字符個數就是 256。這就是後來的 EASCII(Extended ASCII,延伸美國標準信息交換碼)EASCII 碼比 ASCII 碼擴充出來的符號包括表格符號、計算符號、希臘字母和特殊的拉丁符號。

然後 EASCII 並沒有形成統一的標準,各國個商家都有自己的小算盤,都想在字節的高位做文章,比如 MS-DOS, IBM PC上使用了各自定義的編碼字符集,為了結束這種混亂的局面,國際標準化組織(ISO)及國際電工委員會(IEC)聯合制定的一系列8位元字符集的標準,叫 ISO 8859,全稱ISO/IEC 8859,它在 ASCII 基礎之上擴展而來,所以完全 ASCII,ISO 8859 字符編碼方案所擴展的這128個編碼中,只有0xA0~0xFF(十進制為160~255)被使用,其實 ISO 8859是一組字符集的總稱,旗下共包含了15個字符集,分別是 ISO 8859-1 ~ ISO 8859-15,ISO 8859-1 又稱之為 Latin-1,它是西歐語言,其它的分別代表 中歐、南歐、北歐等字符集。

技術分享

GB2312:滿足國人需求的字符集

後來,計算機開始普及到了中國,但面臨的一個問題就是字符,漢字博大精深,常用漢字有3500個,已經大大超出了 ASCII 字符集所能表示的字符範圍了,即使是 EASCII 也顯得杯水車薪,1981 年國家標準化管理委員會定了一套字符集叫 GB2312,每個漢字符號由兩個字節組成,理論上它可以表示65536個字符,不過它只收錄了7445個字符,6763個漢字和682個其他字符,同時它能夠兼容 ASCII,ASCII 中定義的字符只占用一個字節的空間。

GB2312 所收錄的漢字已經覆蓋中國大陸99.75%的使用頻率,但是對一些罕見的字和繁體字還有很多少數民族使用的字符都沒法處理,於是後來就在 GB2312 的基礎上創建了一種叫 GBK 的字符編碼,GBK 不僅收錄了27484 個漢字,同時還收錄了藏文、蒙文、維吾爾文等主要的少數民族文字。GBK 是利用了 GB2312 中未被使用的編碼空間上進行擴充,所以它能完全兼容 GB2312和 ASCII。而 GB 18030 是現時最新的字符集,兼容 GB 2312-1980 和 GBK, 共收錄漢字70244個,采用多字節編碼,每個字符可以有1、2、4個字節組成,某種意義上它能容納161 萬個字符,包含繁體漢字以及日韓漢字,單字節與ASCII兼容,雙字節與GBK標準兼容。

Unicode :統一江湖的字符集

盡管我們有了屬於自己的字符集和字符編碼 GBK,可世界上還有很多國家擁有自己的語言和文字,比如日本用 JIS,臺灣用 BIG5,不同國家之間交流起來就很困難,因為沒有統一的編碼標準,可能同一個字符,在A國家用兩字字節存儲,而到了B國家是3個字節,這樣很容易出現編碼問題,於是在 1991 年,國際標準化組織和統一碼聯盟組織各自開發了 ISO/IEC 10646(USC)和 Unicode 項目,這兩個項目的目的都是希望用一種字符集來統一全世界所有字符,不過很快雙方都意識到世界上並不需要兩個不兼容的字符集。於是他們就編碼問題進行了非常友好地會晤,決定彼此把工作內容合並,雖然項目還是獨立存在,各自發布各自的標準,但前提是兩者必須保持兼容。不過由於 Unicode 這一名字比較好記,因而它使用更為廣泛,成為了事實上的統一編碼標準。

以上是對字符集歷史的一個簡要回顧,現在重點來說說Unicode,Unicode 是一個囊括了世界上所有字符的字符集,其中每一個字符都對應有唯一的編碼值(code point),註意了!它不是字符編碼,僅僅是字符集而已,Unicode 字符如何進行編碼,可以是 UTF-8、UTF-16、甚至用 GBK 來編碼。例如:

>>> a = u"好"
>>> a
u‘\u597d‘
>>> b = a.encode("utf-8")
>>> b
‘\xe5\xa5\xbd‘
>>>

>>> b = a.encode("gbk")
>>> b
‘\xba\xc3‘

Unicode 本身並沒有規定一個字符究竟是用一個還是三個或者四個字節表示。Unicode 只規定了每個字符對應到唯一的代碼值(code point),代碼值 從 0000 ~ 10FFFF 共 1114112 個值 ,真正存儲的時候需要多少個字節是由具體的編碼格式決定的。比如:字符 「A」用 UTF-8 的格式編碼來存儲就只占用1個字節,用 UTF-16 就占用2個字節,而用 UTF-32 存儲就占用4個字節。

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 兩種字符集是相互兼容的,所以這幾種編碼格式也有著對應的等值關系

UCS-2 使用兩個定長的字節來表示一個字符,UTF-16 也是使用兩個字節,不過 UTF-16 是變長的(網上很多錯誤的說法說 UTF-16是定長的),遇到兩個字節沒法表示時,會用4個字節來表示,因此 UTF-16 可以看作是在 UCS-2 的基礎上擴展而來的。而 UTF-32 與 USC-4 是完全等價的,使用4個字節表示,顯然,這種方式浪費的空間比較多。

UTF-8 的優勢是:它以單字節為單位用 1~4 個字節來表示一個字符,從首字節就可以判斷一個字符的UTF-8編碼有幾個字節。如果首字節以0開頭,肯定是單字節編碼,如果以110開頭,肯定是雙字節編碼,如果是1110開頭,肯定是三字節編碼,以此類推。除了單字節外,多字節UTF-8碼的後續字節均以10開頭。

1~4 字節的 UTF-8 編碼看起來是這樣的:

0xxxxxxx
110xxxxx 10xxxxxx
1110xxxx 10xxxxxx 10xxxxxx
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
  • 單字節可編碼的 Unicode 範圍:\u0000~\u007F(0~127)

  • 雙字節可編碼的 Unicode 範圍:\u0080~\u07FF(128~2047)

  • 三字節可編碼的 Unicode 範圍:\u0800~\uFFFF(2048~65535)

  • 四字節可編碼的 Unicode 範圍:\u10000~\u1FFFFF(65536~2097151)

UTF-8 兼容了 ASCII,在數據傳輸和存儲過程中節省了空間,其二是UTF-8 不需要考慮大小端問題。這兩點都是 UTF-16 的劣勢。不過對於中文字符,用 UTF-8 就要用3個字節,而 UTF-16 只需2個字節。而UTF-16 的優點是在計算字符串長度,執行索引操作時速度會很快。Java 內部使用 UTF-16 編碼方案。而 Python3 使用 UTF-8。UTF-8 編碼在互聯網領域應用更加廣泛。

來看一張圖,下圖是Windows平臺保存文件時可選擇的字符編碼類型,你可以指定系統以什麽樣的編碼格式來存儲文件,ANSI 是 ISO 8859-1的超集,之所以在 Windows下有 Unicode 編碼這樣一種說法,其實是 Windows 的一種錯誤表示方法,或許是因為歷史原因一直沿用至今,其實它真正表示的是 UTF-16 編碼,更具體一點是 UTF-16小端,什麽是大端和小端呢?

技術分享

大端與小端

大小端是數據在存儲器中的存放順序,大端模式,是指數據的高字節在前,保存在內存的低地址中,與人類的讀寫法一致,數據的低字節在後,保存在內存的高地址中,小端與之相反,小端模式,是指數據的高字節在後,保存在內存的高地址中,而數據的低字節在前,保存在內存的低地址中例如,十六進制數值 0x1234567的大端字節序和小端字節序的寫法:

技術分享

至於為什麽會有大端和小端之分呢?對於 16 位或者 32 位的處理器,由於寄存器寬度大於一個字節,那麽必然存在著一個如何將多個字節排放的問題,因為不同操作系統讀取多字節的順序不一樣,,x86和一般的OS(如windows,FreeBSD,Linux)使用的是小端模式。但比如Mac OS是大端模式。因此就導致了大端存儲模式和小端存儲模式的存在,兩者並沒有孰優孰劣。

為什麽UTF-8不需要考慮大小端問題?

UTF-8 的編碼單元是1個字節,所以就不用考慮字節序問題。而 UTF-16 是用 2個字節來編碼 Unicode 字符,編碼單位是兩個字節,因此需要考慮字節序問題,因為2個字節哪個存高位哪個存低位需要確定。

Python2 中的字符編碼

現在總算把理論說完了,再來說說 Python 中的編碼問題,也是每個Python開發者最關心、最經常遇到的問題,Python 的誕生時間比 Unicode 還要早幾年,所以,Python的第一個版本一直延續到Python2.7,Python 的默認編碼都是 ASCII

>>> import sys
>>> sys.getdefaultencoding()
‘ascii‘

所以在 Python 源代碼,要能夠正常保存中文字符就必須先指定utf

8 或者 gbk 格式

# coding=utf-8

或者是:

#!/usr/bin/python
# -*- coding: utf-8 -*-

str 與 unicode

在前面我們介紹過字符,這裏還有必要重復一下字符和字節的區別,字符就是一個符號,比如一個漢字、一個字母、一個數字、一個標點都可以稱為一個字符,而字節就是字符就是編碼之後轉換而成的二進制序列,一個字節是8個比特位。例如字符 "p" 存儲到硬盤是一串二進制數據 01110000,占用一個字節。字節方便存儲和網絡傳輸,而字符用於顯示方便閱讀。

在Python2中,字符與字節的表示很微妙,兩者的界限很模糊,Python2 中把字符串分為 unicode 和 str 兩種類型。本質上 str 類型是二進制字節序列, unicode 類型的字符串是字符,下面的示例代碼可以看出 str 類型的 "禪" 打印出來是十六進制的 \xec\xf8 ,對應的二進制字節序列就是 ‘11101100 11111000‘。

>>> s = ‘禪‘
>>> s
‘\xec\xf8‘
>>> type(s)
<type ‘str‘>

而 unicode 類型的 u"禪" 對應的 unicode 符號是 u‘\u7985‘

>>> u = u"禪"
>>> u
u‘\u7985‘
>>> type(u)
<type ‘unicode‘>

我們要把 unicode 字符保存到文件或者傳輸到網絡就需要經過編碼處理轉換成二進制形式的 str 類型,於是 python 的字符串提供了 encode 方法,從 unicode 轉換到 str,反之亦然。

技術分享

encode

>>> u = u"禪"
>>> u
u‘\u7985‘
>>> u.encode("utf-8")
‘\xe7\xa6\x85‘

decode

>>> s = "禪"
>>> s.decode("utf-8")
u‘\u7985‘
>>>

不少初學者怎麽也記不住 str 與 unicode 之間的轉換用 encode 還是 decode,如果你記住了 str 本質上其實是一串二進制數據,而 unicode 是字符(符號),編碼(encode)就是把字符(符號)轉換為 二進制數據的過程,因此 unicode 到 str 的轉換要用 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 之間的轉換關系之後,我們來看看什麽時候會出現 UnicodeEncodeError、UnicodeDecodeError 錯誤。

UnicodeEncodeError

UnicodeEncodeError 發生在 unicode 字符串轉換成 str 字節序列的時候,來看一個例子,把一串 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)

為什麽會出現 UnicodeEncodeError?

因為調用 write 方法時,程序會把字符經過編碼轉換成二進制字節序列,內部會有 unicode 到 str 的編碼轉換過程,程序會先判斷字符串是什麽類型,如果是 str,就直接寫入文件,不需要編碼,因為 str 類型的字符串本身就是一串二進制的字節序列了。如果字符串是 unicode 類型,那麽它會先調用 encode 方法把 unicode 字符串轉換成二進制形式的 str 類型,才保存到文件,而 Python2中,encode 方法默認使用 ascii 進行 encde.

相當於:

>>> u"Python之禪".encode("ascii")

但是,我們知道 ASCII 字符集中只包含了128個拉丁字母,不包括中文字符,因此 出現了 ‘ascii‘ codec can‘t encode characters 的錯誤。要正確地使用 encode ,就必須指定一個包含了中文字符的字符集,比如:UTF-8、GBK。

>>> u"Python之禪".encode("utf-8")
‘Python\xe4\xb9\x8b\xe7\xa6\x85‘

>>> u"Python之禪".encode("gbk")
‘Python\xd6\xae\xec\xf8‘

所以要把 unicode 字符串正確地寫入文件,就應該預先把字符串進行 UTF-8 或 GBK 編碼轉換。

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)

當然,把 unicode 字符串正確地寫入文件不止一種方式,但原理是一樣的,這裏不再介紹,把字符串寫入數據庫,傳輸到網絡都是同樣的原理

UnicodeDecodeError

UnicodeDecodeError 發生在 str 類型的字節序列解碼成 unicode 類型的字符串時

>>> a = u"禪"
>>> a
u‘\u7985‘
>>> b = a.encode("utf-8")
>>> b
‘\xe7\xa6\x85‘
>>> b.decode("gbk")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: ‘gbk‘ codec can‘t decode byte 0x85 in position 2: incomplete multibyte sequence

把一個經過 UTF-8 編碼後生成的字節序列 ‘\xe7\xa6\x85‘ 再用 GBK 解碼轉換成 unicode 字符串時,出現 UnicodeDecodeError,因為 (對於中文字符)GBK 編碼只占用兩個字節,而 UTF-8 占用3個字節,用 GBK 轉換時,還多出一個字節,因此它沒法解析。避免 UnicodeDecodeError 的關鍵是保持 編碼和解碼時用的編碼類型一致。

這也回答了文章開頭說的字符 "禪",保存到文件中有可能占3個字節,有可能占2個字節,具體處決於 encode 的時候指定的編碼格式是什麽。

再舉一個 UnicodeDecodeError 的例子

>>> x = u"Python"
>>> y = "之禪"
>>> x + y
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: ‘ascii‘ codec can‘t decode byte 0xe4 in position 0: ordinal not in range(128)
>>>

str 與 unicode 字符串 執行 + 操作時,Python 會把 str 類型的字節序列隱式地轉換成(解碼)成 和 x 一樣的 unicode 類型,但Python是使用默認的 ascii 編碼來轉換的,而 ASCII字符集中不包含有中文,所以報錯了。相當於:

>>> y.decode(‘ascii‘)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: ‘ascii‘ codec can‘t decode byte 0xe4 in position 0: ordinal not in range(128)

正確地方式應該是找到一種包含有中文字符的字符編碼,比如 UTF-8或者 GBK 顯示地把 y 進行解碼轉換成 unicode 類型

>>> x = u"Python"
>>> y = "之禪"
>>> y = y.decode("utf-8")
>>> x + y
u‘Python\u4e4b\u7985‘

Python3中的字符串與字節序列

Python3對字符串和字符編碼進行了很徹底的重構,完全不兼容Python2,同時也很多想遷移到Python3的項目帶來了很大的麻煩,Python3 把系統默認編碼設置為 UTF-8,字符和二進制字節序列區分得更清晰,分別用 str 和 bytes 表示。文本字符全部用 str 類型表示,str 能表示 Unicode 字符集中所有字符,而二進制字節數據用一種全新的數據類型,用 bytes 來表示,盡管Python2中也有bytes類型,但那只不過是str的一個別名。

str

>>> a = "a"
>>> a
‘a‘
>>> type(a)
<class ‘str‘>

>>> b = "禪"
>>> b
‘禪‘
>>> type(b)
<class ‘str‘>

bytes

Python3 中,在字符引號前加‘b’,明確表示這是一個 bytes 類型的對象,實際上它就是一組二進制字節序列組成的數據,bytes 類型可以是 ASCII範圍內的字符和其它十六進制形式的字符數據,但不能用中文等非ASCII字符表示。

>>> c = b‘a‘
>>> c
b‘a‘
>>> type(c)
<class ‘bytes‘>

>>> d = b‘\xe7\xa6\x85‘
>>> d
b‘\xe7\xa6\x85‘
>>> type(d)
<class ‘bytes‘>
>>>

>>> e = b‘禪‘
  File "<stdin>", line 1
SyntaxError: bytes can only contain ASCII literal characters.

bytes 類型提供的操作和 str 一樣,支持分片、索引、基本數值運算等操作。但是 str 與 bytes 類型的數據不能執行 + 操作,盡管在python2中是可行的。

>>> b"a"+b"c"
b‘ac‘
>>> b"a"*2
b‘aa‘
>>> b"abcdef\xd6"[1:]
b‘bcdef\xd6‘
>>> b"abcdef\xd6"[-1]
214

>>> b"a" + "b"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can‘t concat bytes to str

python2 與 python3 字節與字符對比

python2python3表現轉換作用
str bytes 字節 encode 存儲
unicode str 字符 decode 顯示

總結

  1. 字符編碼本質上是字符到字節的轉換過程

  2. 字符集的演進過程是:ascii、eascii、ios8895-x,gb2312... Unicode

  3. Unicode是字符集,對應的編碼格式有UTF-8,UTF-16

  4. 字節序列存儲的時候有大小端之分

  5. python2中字符與字節分別用unicode和str類型表示

  6. python3中字符與字節分別用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/

字符編碼的前世今生--轉