深入了解 Unicode 文字與符號的編解碼


(圖片取自維基百科)

1987 年 Unicode 誕生至今已經高達近八成的普及率(若包含 ASCII)。跟文字有關的都得處理編解碼問題,設計 Unicode 的野心就是要能表示含全世界的文字符號。(如:Chinese (中文) 到 Russian (русский) 以及 Arabic (العربية) 與其他表情符號)。

在 Unicode 誕生之前的世界

電腦是在美國被發明的,自然而然最初只需要簡單的符號、數字、以及二十六個英文字母,因此最初被普遍使用的字符集就是 ASCII。而 7-Bit 的 ASCII 隨著電腦的普及逐漸發現有些符號與文字無法表示後,加上電腦並不需要有第八位元的錯誤檢查碼,所以 IBM 將其擴展為 8-Bit 的 EBCDIC

但這始終停留在英美國家,東方世界所使用的漢字系統 CJK(Chinese, Japanese, Korean) 完全只能使用自己定義的字符集,而歐洲國家也同樣如此。每個國家的語言文字都對應到一個以上的編解碼方式,這對寫程式的人是非常痛苦的。試想,如果一篇文章中參雜中文與拉丁文,是無法用同一種編碼方式處理的,

所以在 Unicode 出現之前,不可能寫多語言的文章。瀏覽器也會非常難實作,可能中文網頁跳轉到別的國家網站,就變成亂碼了。為了解決這個問題,微軟與蘋果當初最新跳出來提倡了 ISO-2022 的標準,符合這規範標準的字符集互相可以透過 shift sequeences 結合使用,但直到 Unicode 的出現,電腦上的語言隔閡才真正的被消弭。

Unicode 的編碼

大多人都會以為 Unicode 是種編碼,其實不然,Unicode 代表著文字符號的集合,每個文字符號當然都會依序有對應的編號,依重要性高至低,給予編號

為什麼需要編碼?

那麼既然有了編號,不也是一種編碼嗎?為什麼還需要額外的編碼?理論上來說,我們的確是不需要 UTF (Unicode Transformation Format) 或是 UCS(Universal Character Set)這類的 Codec。不過因為 Unicode 無所不在(如:輸入法、郵件、網路、作業系統、字型),編解碼的速度以及空間使用的效率就非常重要了!

也因為講究效能,在 Unicode 誕生後,不同的機器上大家採用的最佳化方式也不同(考慮 Little/Big Endian),造成編碼方式的混亂。可知 Unicode 本身就算有基本的編號,也因大家的實作不同,必須要明確指出使用的編碼方式(即使你不想要最佳化)。

UTF 編碼中的數字代表 Bit,UCS 中的數字代表 Byte。

    >>> h = '哈囉'     '\xe5\x93\x88\xe5\x9b\x89'

檔案中存放的二進位碼為 0000000: e593 88e5 9b89 0a                        .......

>>> h.decode('utf8')
    u'\u54c8\u56c9'

當我們提到 Unicode 編碼時,通常指的會是以下幾種編碼方式

UTF-8

  • 最普遍被使用的編碼方式之一(請參考經典的UTF-8)
  • 可變長度,大部分字符可以 3-Byte 表示
  • 相容 ASCII
  • Endian-Neutral,在不同機器與 OS 間不需要特別處理
  • 外部編碼需要訊息交換的首選

UTF-16

  • 最普遍被使用的編碼方式之一
  • 可變長度,大部分字符可以 2-Byte 表示
  • 和 ASCII 和 ISO-8859-1 不相容
  • 需要考慮 Little/Big Endian,所以需要 BOM 的存在
  • 空間使用效率高,被被廣泛用於程式的內部編碼,如 Java。

UTF-32:同 UCS-4

  • 固定長度
  • 簡單,不需要處理字符定位問題
  • 需要考慮 Little/Big Endian,所以需要 BOM 的存在
  • 浪費記憶體空間

UCS-4:功能上與 UTF-32 相同
UCS-2:已過時的 UTF-16 子集

常見名詞解釋

  • Code Page: 含義同於字符編碼集(Character Encoding),Windows 中 UTF-8 對應的 Code Page65001,Big5 為 950。
  • Code Space: 該編碼所有會用到的數字範圍,Unicode UTF-8 為 1,114,112
  • Code Point:與字元間有一對一的關係,例如:「哈」對應到 UTF-8 編碼為
    '\xe5\x93\x88'
    ,而
'\xe5\x93\x88'
就是一個 Code Point。 - Code Unit:指的是最小的編碼單位,舉例來說 UTF-8 因為每個 Code Point 一定至少由 8 Bits 以上組成,因此 UTF-8 的 Code Unit 就是 8 Bits。依此類推,UTF-16 的 Code Unit 為 16 Bits。

舉例來說:

  • With US-ASCII, code unit is 7 bits.
  • With UTF-8, code unit is 8 bits.
  • With EBCDIC, code unit is 8 bits.
  • With UTF-16, code unit is 16 bits.
  • With UTF-32, code unit is 32 bits.

Unicode 編解碼無所不在

因為編碼到處都會用到,光是在網頁輸入文字,就會牽扯到「輸入法->作業系統->瀏覽器->網頁」間的編解碼轉換,因此得特別留意設定是否正確才可以避免編解碼錯誤產生的亂碼。(不知道程式採用何種編碼時,可以試試用 Universal Encoding Detector 來檢查。)

VIM 編輯器

將 VIM、終端機、以及檔案使用的編碼方式都改為 UTF-8:

set encoding=utf-8 set termencoding=utf-8 set fileencoding=utf-8
### Python

Python 可以透過直譯器或是 Script 兩種方式執行,如果透過直譯器執行的話,會被終端機與 Shell 的編碼方式影響,因此我們需要設定以下環境變數:

LANG=zh_TW.UTF-8
若是透過 Script 執行,則要記得在檔案開頭加入以下標頭:
# -*- coding: utf8 -*-
或是強制更改編碼方式:
importsys ifsys.getdefaultencoding()!='utf-8':     reload(sys)     sys.setdefaultencoding('utf-8')
Python 相關的 Unicode 處理可以參考 [All About Python and Unicode](http://www.talisman.org/~erlkonig/misc/python-and-unicode.html#PLAT_WIN)。

Windows

因為 Windows 在 Unicode 誕生前就存在,因此處理 Unicode 相對來說沒有這麼直覺。

  • MBCS (Multi Byte Char System) 指的是繁體中文(BIG5),簡體中文(GBK)這類的字符集。
  • WideChar 指的是 Unicode。
  • 這些字元的編碼與 Unicode 轉換可以使用 MultiByteToWideChar/WideCharToMultiByte。
  • _stprintf 就是 Unicode 的 sprintf,因為 sprintf 就只是給 MBCS 使用。
  • LPSTR, pointer to 32 bits string
  • LPWSTR, pointer to 32 bits unicode string
  • LPCWSTR, pointer to 32 bits unicode constant string
  • 要在 C 語言中使用 Unicode 字串,要使用 wchar_t 取代 char,然後用對應的函數取代原本的字串操作函數。
  • 沒特別說明,Unicode 在 Windows 指的是 UTF-16 Little Endian。