April 20, 2015

淺談電腦編碼與 Unicode (一) 基礎概念篇

有鑑於最近在寫 Python 時常常遇到編碼問題,於是重新整理以前記錄在 Evernote 內的編碼相關資料,用自己理解的方式寫出來,如果有錯誤還請留言指教。


目錄


(一) 編碼基本概念

人腦能理解各種文字、符號,而電腦只懂得 0 與 1,因此科學家制定了一些規則,將各種文字、符號用 0 與 1 的組合表示,例如 ‘0100 0001’ 代表 ‘A’,’0100 0010’ 代表 ‘B’。

這種 將字元對應到 0 與 1 的組合 的規則就是編碼的基礎。


(二) 常見的編碼與其歷史淵源

1. ASCII 編碼

電腦開始發展的時候,因為各種原因(語言、習慣、版權等等),不同系統可能使用不同的編碼。在百家爭鳴當中,一款稱為 ASCII 的編碼逐步提升其佔有率。ASCII 編碼僅用 7 bits,故最多只能表示 2 的 7 次方也就是 128 種文字。換句話說,ASCII 編碼基本上只能表達英文字母 A-Z、a-z、數字 0-9 以及一些標點符號。雖然不夠完善,但是對於英語系國家來說 ASCII 編碼已經十分夠用了。

2. Extended ASCII

ASCII 編碼對於英語系國家來說已經足夠,但歐洲許多國家有不能用純英文表示的字元,譬如 a 上面加一個圈,u 上面加一個點等等,又或著像希臘符號 alpha, beta 等等都無法用 ASCII 表示。為了解決這個問題,於是出現了 Extended ASCII (或稱 EASCII)。

EASCII 就如同其名字一樣,是 ASCII 的 extend 延伸版,EASCII 使用 8 bits 來編碼,比 ASCII 多了 1 個 bit,因此最多能表示 256 種字元,256 雖然比 ASCII 的 128 種要多上一倍,但小於 128 的字元定義基本上和 ASCII 一致,而剩下的 128 字元還是無法容納整個歐洲的文字,編碼表仍然必須因地制宜,故 EASCII 依然會依照國家不同,而有不同的字元編碼,不同地區的編碼仍舊無法統一。

Many extensions use the additional 128 codes available by using all eight bits of each byte. This helps include many languages otherwise not easily representable in ASCII, but is still not enough to cover all languages of countries in which computers are sold, so even these eight-bit extensions had to have local variants. – Wikipedia

3. ISO/IEC 8859

此時國際標準組織跳出來訂製 EASCII 編碼的標準(ISO/IEC 8859),其中最廣泛被運用的就是 ISO/IEC_8859-1,ISO 8859-1 的 256 個字元主要涵蓋了西歐文字,而歐洲與北美一直都是電腦科學發展的重心,故間接更加促進 ISO 8859-1 的接受度。

4. Windows-1252

在國際組織發表 ISO 8859 編碼標準後,微軟制定了一套叫做 Windows-1252 (或稱之為 CP-1252, cp1252) 的字元編碼表,這套編碼標準大部分遵守 ISO 8859-1 制定的編碼方式,但仍有小部分與其有所落差。例如 Windows-1252 的雙引號和撇號與 ISO 8859-1 有所差異,因此在非 Windows 操作系統都變成問號或方格。

這裡要注意的是,雖然有些人會將 Windows-1252 稱作 ANSI(American National Standards Institute,美國國家標準協會)編碼,但這個名稱並不正確。實際上 ANSI 編碼指的應該是 ISO 8859 家族,而非 Windows-1252,但大家一直這樣以訛傳訛,日子久了人們也漸漸接受這樣的叫法。

Microsoft explains, “The term ANSI as used to signify Windows code pages is a historical reference, but is nowadays a misnomer that continues to persist in the Windows community.” – Wikipedia

Windows-1251 被用於英文與西歐版本的 Windows 系統,成為目前最常見的編碼之一。

5. CJK 編碼

而大約在同一個時期,亞洲地區的編碼也是一樣的混亂,中文、日文、韓文、印度文、阿拉伯文..每個國家各有自己的編碼。而亞洲語言的字元太多,光是中文常用字可能就有上千字,僅用 1 byte = 8 bits = 256 種字元無法完全表達,故亞洲文字通常需要用 2 個以上的 byte 來表示(例如 DBCS 系統),因此亞洲整體而言又較歐美更為複雜。

亞洲文字當中,中文、日文、韓文(及東南亞部分地區)擁有相似的背景,這些文字都受過「漢字」相當程度的影響,因此這些地區的文字有著相似的特性。歪國人將這些文字稱之為 中日韓統一表意文字(CJK Unified Ideographs)。其中 C 代表 Chinese,J 代表 Japan,K 代表 Korea。

6. 在網際網路成熟前

總而言之,在離現在沒多久之前的那個年代,不同地區使用不同的編碼,甚至同一個國家內有時也沒有一個統一的編碼。張三都用張三的編碼,李四都用李四的編碼,大家各自用的很開心。

這在當時並不是一個很大的問題,因為在那個時空背景下,能夠彼此相連的機器其物理位置也都很相近,要連線的兩方只要互相講好要使用的編碼就沒問題了。

7. 網際網路發展後

但隨著網際網路的發展,電腦與電腦間交換訊息變成非常普遍的事,亞洲的客戶可以透過網路直接連上歐洲的伺服器,此時如果從 A 電腦輸入的字串在 B 電腦不能正確顯示,那麼對雙方使用者都是很大的困擾。

這時候,因地制宜的區域編碼就無法解決這個問題,這段時間出現了許多 workaround solution,有些可以暫時解決問題,有些則衍伸出更多的麻煩。但可以確定的是,為了在網際網路上流通資訊,我們必須逐漸捨棄舊有編碼方式,並建立一套新的機制。這就是 Unicode 的由來。


(三) Unicode

1. Code point

科學家將全世界所有的文字與符號都收錄進一個巨大的表格內,並給予每一個字元一個獨一無二的「代號」(碼位、Code point)。而這個概念就稱為萬國碼(Unicode)。

在 Unicode 內,字元的 Code point 通常用「U+」然後緊接著一組十六進位的數字來表示,例如 U+5566 代表 “啦” 這個字。

有個叫 Ian Albert 的人將 Unicode 一百多萬個 code points 製作成海報,相當壯觀。參見 Albert’s Unicode chart

2. Unicode Transformation Format (UTF) 與 Universal Character Set (UCS)

但是如同文章一開始提到的,電腦只看得懂 0 與 1 的組合,我們勢必得將 Unicode 的字元轉換成 0 與 1 才能在電腦上表達,這個轉換的過程我們稱之為 mapping。

Unicode 定義了兩種 mapping methods,分別為 Unicode Transformation Format (UTF) encodings 與 Universal Character Set (UCS) encodings.

UTF 與 UCS 家族各自有不同的 encoding 實作方式,例如 UTF-7, UTF-8, UTF-16, UTF-32; UCS-2, UCS-4 等,其中 UCS-2 是 UTF-16 已經 obsolete 的 subset,而 UCS-4 與 UTF-32 功能上是相同的。

Unicode 有這麼多種編碼方式,看起來很複雜,但基本上一般使用者只需認識 UTF 家族即可,其中又以 UTF-8 最為常見。

3. UTF-8

Unicode 記錄了全世界所有的文字與符號,全世界的文字符號非常非常 der 多,如果要統一用一固定長度的編碼來表達全世界的文字,勢必得用多個位元組來表達一個字元,這將會浪費儲存空間與傳輸頻寬。

還記得文章一開始提到的 ASCII 編碼嗎?對於英語系國家而言,使用 ASCII 編碼僅需 1 個 byte 就能表達一個字元,若 Unicode 反而需要用多個 bytes 才能表達一個字元,那大家肯定不想用 Unicode。

UTF-8 可以解決這個問題,UTF-8 是個可變長度的編碼,它使用 1 ~ 4 bytes 來表達一個字元,並向下相容 ASCII,也就是說,UTF-8 的前 128 個字元編碼與 ASCII 完全相同,如此一來處理英文字時就不會有上述提到浪費空間的問題。而當面對 ASCII 無法正確表示的字元(如中文、日文)時,UTF-8 則使用多個 bytes 來表達該字元。

A UTF-8 file that contains only ASCII characters is identical to an ASCII file. Legacy programs can generally handle UTF-8 encoded files, even if they contain non-ASCII characters. – Wikipedia

由於 UTF-8 具有可變長度以不需要 BOM的優勢(等等會解釋 BOM),UTF-8 很快地便成為了全世界最廣泛被使用的編碼方式。根據 w3techs 於 2015 年 3 月的 調查 顯示,全世界的網頁有 83.9% 使用 UTF-8 編碼,獲得壓倒性的佔有率。


UTF-8 近年來快速成長,根據 2015 年最新調查,其佔有率已達 83.9%。 (圖片來源: Wikipedia)

4. UTF 家族之間的比較

UTF 家族有多種實作方式,如 UTF-8, UTF-16, UTF-32 等等,我們在這邊只簡單介紹了 UTF-8,而維基百科的這個頁面:Comparison of Unicode encodings 則詳細列舉了 UTF 家族之間的不同處,有興趣的話可以看一看。

UTF-16 and UTF-32 are incompatible with ASCII files, and thus require Unicode-aware programs to display, print and manipulate them, even if the file is known to contain only characters in the ASCII subset. Because they contain many zero bytes, the strings cannot be manipulated by normal null-terminated string handling for even simple operations such as copy. –Wikipedia

懶得看整篇文章的話,只要記得以下兩點即可:

  • 在大部分情形下,UTF-8 與舊編碼(ASCII) 的相容性比其他 UTF 家族要來的好很多。
  • UTF-8 是 endian-neutral,不需要 BOM

接下來的問題是,什麼是 endian-neutral 與 BOM ?

5. Endianness

讀過資工的應該都上過計算機概論,裡面就會提到 Endianness 的概念。如果您跟我一樣年輕時不懂事都沒去上課的話也沒關係,我也是看報紙才知道..我是說我也是上網查資料才重新複習了這些概念。

下面這三篇文章應該可以讓您重拾對 Endianness 的觀念:

簡單來說,電腦內的資料都是以 11010101 01100010 這樣的方式儲存著,每 8 個 bits 為 1 個 byte,不同廠商製作的硬體在解釋一連串 0 與 1 的組合時,可能會以不同的方式(MSB, LSB..)去解釋位元組,解釋的方式不同,所 mapping 出來的字元就不同。

例如:
在 Unicode 中,Hello 這個單字會對映到五個 code point:
U+0048 U+0065 U+006C U+006C U+006F

UCS-2 編碼下,每個字元佔 2 byte,依照 Big endian 或 Little endian 的不同,又可以分別儲存為
00 48 00 65 00 6C 00 6C 00 6F
6F 00 6C 00 6C 00 65 00 48 00

不同的系統可能會使用不同的 Endianness (例如Intel CPU 通常是 little Endian,而網路上傳輸慣例通常是 Big Endian),不同 Endianness 間需要做轉換才不會在傳輸過程產生誤會。

隨著科技的發展,科學家採用了一些方法來簡化 Endianness 造成的困擾,例如 Unicode Byte Order Mask (BOM) 就是其中一個。

6. Byte order mark, BOM

UTF-16 與 UTF-32 也有上述 Endianness 的問題,解決方法就是:主動在資料最前面加上一些特殊符號。

而這些符號就稱之為 Unicode 的 Byte order mark (BOM)。當程式開啟這些檔案時,一看到檔案開頭的 BOM 就立刻知道「喔喔,這個檔案是用 UTF-16 編碼的,且要用 Big-Endian 去解釋其位元組排列」。

(上述這些描述有點過於簡化了,詳細請參考 Wikipedia。)

On the other hand, UTF-8 is endian-neutral, while UTF-16 and UTF-32 are not. This means that when character sequences in one endian order are loaded onto a machine with a different endian order, the characters need to be converted before they can be processed efficiently. This is more of a communication problem than a computation one. – Wikipedia

舉例來說,

若檔案的 BOM 為 FE FF,代表這個檔案是 UTF-16 (Big-Endian)
若檔案的 BOM 為 FF FE,代表這個檔案是 UTF-16 (Little-Endian)
若檔案的 BOM 為 EF BB BF,代表這個檔案是 UTF-8

等等,剛剛不是說 UTF-8 是 endian-neutral,不需要 BOM 嗎?

The Unicode Standard permits the BOM in UTF-8, but does not require or recommend its use. Not using a BOM lets the text be backwards-compatible with software that is not Unicode-aware. However, without a BOM, heuristic analysis is required to determine what character encoding a file is using.

UTF-8 是 endian-neutral,基本上不需要 BOM,這個 EF BB BF 只是用來告訴開啟這個檔案的程式說「安安你好,我是用 UTF-8 編碼 der 喔 ㄏㄏ」

UTF-8 可以有 BOM,也可以沒有,Unicode 標準並沒有強制規定。

  • UTF-8 檔案有 BOM 的壞處在於:非 Unicode 的老程式用 ASCII 的方式去解讀 UTF-8 文件時,開頭會遇到 EF BB BF 由於 ASCII 不知道那是什麼,故可能直接印出來(亂碼)或產生其他非預期的結果。

  • UTF-8 檔案沒有 BOM 的壞處:程式不能直接透過 BOM 得知要用什麼編碼方式來解讀這個文件,必須透過 heuristic analysis 猜測這個文件是 UTF-8, UTF-16, ASCII 還是其他編碼。理論上這並不是一個太過困難的問題,但是 Microsoft 的系統與軟體在沒有 BOM 的輔助下,常常無法正常辨認出 UTF-8 編碼的檔案,進而導致使用者看見亂碼,這部分等等會再補充說明。

對 BOM 有興趣的話,可以用 HxDHex Fiend 等 Hex Editor 來觀看檔案是否含有 BOM 開頭。

圖1 含有 UTF-8 BOM 開頭(EF BB BF)的檔案
含有 UTF-8 BOM 開頭(EF BB BF)的檔案

圖2 同一個檔案,沒有 UTF-8 BOM 開頭
同一個檔案,沒有 UTF-8 BOM 開頭

7. Unicode 與 UTF 之間的關係

繞了一大圈,接下來回來談 Unicode 與 UTF 之間的關係,直接以例子來說明:

「買啦~你買啦~買一下嘛~買啦買啦」這句子裡面的「啦」這個字

  • 對注音輸入法來說是 ㄌㄚ˙
  • 對拼音輸入法來說是 LA
  • 在 Unicode 表內是 U+5566

歪國人看不懂注音,也不太了解拼音。但是沒關係,「啦」這個字在 Unicode 的 code point 是 U+5566,因此不管是哪一國人,只要講到 U+5566 指的就是唯一的這個「啦」字。

實際在 encoding 時,會依照使用的編碼不同,將「啦」這個字編成不同的 0 與 1 組合,「啦」這個字
- 在 Big5 編碼是 B0D5
- 在 UTF-8 編碼是 0xE5 0x95 0xA6,也就是 11100101:10010101:10100110
- 在 UTF-16 編碼是 0x5566,也就是 56 不能亡

最後再視需求在檔案前加上 BOM 即可。

那要怎麼知道「啦」這個字的 Unicode code print 是 U+5566 的呢?

上網查就可以查到惹,許多網站可以查詢字元的 code print 與編碼方式,例如

等等。


(四) Reference

主要資料來源:

註: 本文整理自我在 2011 ~ 2015 記錄在 Evernote 內的編碼相關資料,用自己理解的方式寫出來,不保證完全正確,如果有錯誤還請留言指教,謝謝。


(五) 延伸閱讀