1. 程式人生 > >關於字元編碼:ascii、unicode與utf-8

關於字元編碼:ascii、unicode與utf-8

轉自:https://foofish.net/unicode_utf-8.html

 

阮一峰老師對普及計算機基礎技術功不可沒,但畢竟老師不是神,因此也避免不了對某些概念有一些錯誤的理解,《字元編碼筆記:ASCII,Unicode 和 UTF-8 》 是阮老師10年前寫的一篇關於字元編碼的科普文章,現在用 Google 搜關鍵字該文章依然名列前茅,可見他的文章有多大影響力,不過這是後話,但裡面的內容是否正確是值得商榷的事。

話說天下大勢,分久必合,合久必分,在字元編碼世界裡也遵循這樣一種歷史規律。從 ASCII 碼到 EASCII(ISO8859),發展到後來的群雄並起,各國家地區擁有自己的編碼格式,中國有 GBK,日本有 JIS,臺灣有 BIG5,然而這是一個混戰的年代,大家各自為政,沒有統一的編碼標準,交流起來極其麻煩。字元編碼成為了程式設計師最頭疼的問題之一

於是在 1991 年,國際標準化組織和統一碼聯盟組織各自開發了 ISO/IEC 10646(USC)和 Unicode 專案。他們兩的野心都不小,一統江湖,千秋萬載。不過很快雙方都意識到世界上並不需要兩個不相容的字符集。於是他們就編碼問題進行了一次非常友好地會晤,決定彼此把工作內容合併,專案還是獨立存在,各自發布各自的標準,前提是兩者必須保持相容。不過由於 Unicode 這一名字比較好記,因而它使用更為廣泛,成為了事實上的統一編碼標準。

以上是對字元編碼歷史的一個簡要回顧,Unicode 究竟是什麼東西,它是一種編碼格式嗎?中文維基百科對 Unicode 的解釋也是讓人一頭霧水,摸不著頭腦。那麼來看看阮老師怎麼說:

可以想象,如果有一種編碼,將世界上所有的符號都納入其中。每一個符號都給予一個獨一無二的編碼,那麼亂碼問題就會消失。這就是Unicode,就像它的名字都表示的,這是一種所有符號的編碼。

這句話讀起來很拗口,有三個地方出現了「編碼」二字。不知阮老師對「編碼」的理解是什麼?但可以肯定的是這三個「編碼」在這句話裡面不是同一個意思。因此有必要先解釋一下「編碼」到底是什麼意思。

「編碼」在漢語裡可以作動詞使用,編碼就是把一個字元(嚴格一點說是字元在字符集中的編號 code point)轉換成一個位元組序列,以便在網路傳輸或者儲存到文字中。比如「好」在 Unicode 中的編號是 U+597d,經過 UTF-8 編碼後會轉換成二進位制序列是 '\xe5\xa5\xbd' 。

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

「編碼」還可以做名詞使用,作為名詞使用時,就是指一種具體的編碼實現方式,比如 ASCII 編碼,GBK 編碼,UTF-8 編碼,都叫做編碼,是一種具體的實實在在的編碼格式。

把編碼概念弄清楚了之後,我們就可以來定義 Unicode 了。其實 Unicode 是一個囊括了世界上所有字元的字符集,其中每一個字元都對應有唯一的編碼值(code point),然而它並不是一種什麼編碼格式,僅僅是字符集而已。 Unicode 字元要儲存要傳輸怎麼辦,它不管,具體怎麼編碼,你們可以自己去實現,可以用 UTF-8、UTF-16、甚至用 GBK 來編碼也是可以的。比如:

>>> a = u""
>>> a
u'\u597d'
>>> b = a.encode("gbk")
>>> b
'\xba\xc3'

「好」用 GBK 編碼轉後就是用兩個位元組表示,用 UTF-8 編碼就是 3 個位元組,同一個字元用不同的編碼方式佔用的位元組長度不一。

阮老師談到 Unicode 的問題 時說:

這裡就有兩個嚴重的問題,第一個問題是,如何才能區別 Unicode 和 ASCII ?

把 Unicode 和 ASCII 碼作為字符集來理解時,需要區分二者嗎?不需要,因為 Unicode 是在 ASCII 的基礎之上建立的,比如 ASCII 碼字符集中的 「A」對應的 code ponit 是 0x41,Unicode 碼是 U+0041,兩者是一樣的。至於中日韓文字用 ASCII 根本沒法表示,所以也不存在混淆的說法。他又接著說:

計算機怎麼知道三個位元組表示一個符號,而不是分別表示三個符號呢?

計算機怎麼不知道是用幾個位元組表示呢?你把字元儲存到文字的時候肯定是要指定一種編碼格式的,既然指定了編碼格式,那麼讀取的時候也按照該種編碼格式反過來讀就行了。老師把這也當做一個問題就有點牽強附會了。

再來看阮老師說 Unicode 的第二個問題:

第二個問題是,我們已經知道,英文字母只用一個位元組表示就夠了,如果Unicode統一規定,每個符號用三個或四個位元組表示,那麼每個英文字母前都必然有二到三個位元組是0,這對於儲存來說是極大的浪費,文字檔案的大小會因此大出二三倍,這是無法接受的。

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

再看來看這張圖:

windows-notepad.jpg

阮老師對 Unicode 編碼的解釋是:

Unicode編碼指的是UCS-2編碼方式,即直接用兩個位元組存入字元的Unicode碼。這個選項用的little endian格式。

阮老師全文並沒有對 UCS-2 做任何解釋,導致很多讀者非常疑惑,因此,我有必要在這裡解釋一下。

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

UCS-2 是使用兩個定長的位元組來表示一個字元,UTF-16 也是使用兩個位元組,不過 UTF-16 是變長的(網上很多錯誤的說法說 UTF-16是定長的),遇到兩個位元組沒法表示時,會用4個位元組來表示,因此 UTF-16 可以看作是在 UCS-2 的基礎上擴充套件而來的。而 UTF-32 與 USC-4 是完全等價的。

再回到這張圖片,之所以在 Windows下有 Unicode 編碼這樣一種說法,其實是 Windows 的一種錯誤表示方法,或許是因為歷史原因還是其他問題一直沿用至今。這因為如此,導致絕大多數初學者誤以為 Unicode 就是一種編碼格式,所以 Windows 有時也是誤人子弟啊。反正你就理解為 UTF-16 編碼就是。

阮老師對什麼是大端和小端的解釋顯得很唐突。為什麼會有大端小端?也解釋的含糊其詞:

上一節已經提到,Unicode碼可以採用UCS-2格式直接儲存。以漢字"嚴"為例,Unicode碼是4E25,需要用兩個位元組儲存,一個位元組是4E,另一個位元組是25。儲存的時候,4E在前,25在後,就是Big endian方式;25在前,4E在後,就是Little endian方式。因此,第一個位元組在前,就是"大頭方式"(Big endian),第二個位元組在前就是"小頭方式"(Little endian)。

小端就是低位位元組放在記憶體的低地址端,高位位元組放在記憶體的高地址端。與之相反,大端就是高位位元組放在記憶體的低地址端,低位位元組放在記憶體的高地址端。舉例來說,位元組序列 '0x12 34 56 78' 在記憶體中的表示形式為:

# 小端模式
低地址 ------------------> 高地址
0x78  |  0x56  |  0x34  |  0x12

# 大端模式
低地址 -----------------> 高地址
0x12  |  0x34  |  0x56  |  0x78

至於為什麼會有大端和小端之分呢?對於 16 位或者 32 位的處理器,由於暫存器寬度大於一個位元組,那麼必然存在著一個如何將多個位元組排放的問題,因為不同機器型別讀取雙位元組的時候方式不一樣,因此就導致了大端儲存模式和小端儲存模式的存在,兩者並沒有孰優孰劣。

前面我已經解釋過了 UCS-2 可以理解為 UTF-16 編碼,漢字「嚴」的 Unicode 編號是 U+4E25,儲存的時候到底需要幾個位元組,跟具體的編碼格式有關,如果是用 UTF-8 編碼,結果為 'e4 b8 a5',需要 3 個位元組來儲存。用 UTF-16 編碼,結果為 '4e 25',用兩個位元組儲存,UTF-32 就要用4個位元組了。因為 UTF-16 和 UTF-32 都是以雙位元組為單位來儲存字元的,雙位元組就存在大小端的問題了。而 UTF-8 的編碼單元是1個位元組,所以就不用考慮位元組序問題。

他又說:

Unicode規範中定義,每一個檔案的最前面分別加入一個表示編碼順序的字元,這個字元的名字叫做"零寬度非換行空格"(ZERO WIDTH NO-BREAK SPACE),用FEFF表示。這正好是兩個位元組,而且FF比FE大1。

如果一個文字檔案的頭兩個位元組是FE FF,就表示該檔案採用大頭方式;如果頭兩個位元組是FF FE,就表示該檔案採用小頭方式

這裡就錯得有點離譜了。在 UTF-16 中,FEFF 是位元組順序標記(byte order mark,BOM),用來標記編碼後的位元組序列高位在前還是低位在前。小端低位在前,高位在後,用 「FFFE」 標識。大端高位在前,低位在後,用 「FEFF」 標識。

yan

上圖就是我分別用 UTF-16LE(小端)和 UTF-16BE(大端)儲存的「嚴」的十進位制顯示方式。而 FEFF 作為零寬度非換行空格僅僅是它出現在位元組序列的中間時的作用,使用者看起來就是一個空格,不過從 Unicode3.2 開始就只能規定 FEFF 只能出現在位元組流的開頭,用於標記位元組序(大小端)。

最後是我自己的補充,UTF-8、UTF-16 各自的應用場景和優缺點。

UTF-8 的優勢是:它以單位元組為單位用 1~4 個位元組來表示一個字元,相容了 ASCII,在資料傳輸和儲存過程中節省了空間,其二是UTF-8 不需要考慮大小端問題。這兩點都是 UTF-16 的劣勢。

不過對於中文字元,用 UTF-8 就要用3個位元組,而 UTF-16 只需2個位元組。而UTF-16 的優點是在計算字串長度,執行索引操作時速度會很快。Java 內部使用 UTF-16 編碼方案。而 Python3 使用 UTF-8。UTF-8 編碼在網際網路領域應用更加廣泛。

以上就是我對字元編碼的一點個人理解,不一定全部正確,但希望同樣能給你帶來一些思考。阮一峰老師的這篇文章雖然有些錯誤的地方,但只要不盲崇,帶著懷疑的態度看待問題,也一定有不一樣的收穫。