1. 程式人生 > 實用技巧 >字符集與編碼

字符集與編碼

2020年9月16日11:33:10

轉自:https://deerchao.cn/blog/posts/unicode.html

字符集和編碼

字符集(Character Set)是字元的集合,定義系統能處理哪些字元;編碼(Encoding)則規定這些字元在計算機內部的表示方式。

這裡字元是抽象的概念,編碼將其與二進位制資料進行對映。由於編碼通常依賴於字符集,實踐中兩者經常是繫結或互指的。常見的漢字編碼方案GB2312,其全名為《資訊交換用漢字編碼字符集·基本集》;而 HTML 中的<meta charset="encoding">標籤也混用了字符集(charset)和編碼的概念。

既然是集合,字符集就會有超集和子集。如 1995 年釋出的GBK(《漢字內碼擴充套件規範》),就是GB2312(釋出於 1980 年)的超集;而 2000 年釋出的GB18030(《資訊科技 中文編碼字符集》)又是GBK的超集。

內碼表

內碼表就是字符集加上編碼。

Windows 提供了很多內碼表選項,用於支援不同的語言文字。每個內碼表有一個編號,簡體中文對應的編號為936,其對應的字符集為GBK。你可以在命令列中執行命令reg query HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Nls\CodePage /f ACP,檢視系統當前設定的內碼表編號:

C:\>reg query HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Nls\CodePage /f ACP

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Nls\CodePage
    ACP    REG_SZ    936

搜尋結束: 找到 1 匹配。

C:>

內碼表是 Windows 系統的術語。在 Linux 系統中,類似的概念是locale,不過當前 Linux 一般預設編碼方式都是utf-8,對應的字符集是 Unicode。

ANSI 編碼

ANSI 編碼代表 Windows 系統中當前內碼表對應的編碼,亦即系統預設編碼。

這個術語經常在 Windows 平臺使用,但實際上是被誤用的。理論上,它應該等同於ANSI在 1986 年釋出的US-ASCII。現實中,它可以代表任意編碼,甚至可以與ASCII完全不相容,只要設定為系統預設編碼。

Unicode

Unicode 是一套標準,包含多語言統一的字符集及其相關編碼,以及在這個字符集上進行文字處理的相關規則。

Unicode 當前版本(12.1)共規定了 137,929 個字元。它們不僅囊括了當前全球使用的主要語言文字(如拉丁字母,阿拉伯文字,簡繁漢字等),還包含了很多符號(貨幣符號,標點符號,數學符號,幾何圖形,emoji等),甚至還有僅在史料上使用的文字(如楔形文字,埃及象形文字等)。

在 Unicode 中,每個字元被分配了一個數值(Code Point,程式碼點)和一個名稱。比如字母A的名稱是LATIN CAPITAL LETTER A(大寫拉丁字母A)。它對應的數值是 65,通常寫作U+0041(41是十六進位制數,等於10進位制的65)。除此之外,Unicode 還定義了各個字元的一系列屬性,比如是否是大寫字母,是否代表數字,書寫方向(左到右還是右到左),寬度(半形還是全形)等。基於這些屬性,Unicode 提供了大小寫轉換,文字換行,雙向書寫顯示等相關演算法。

Unicode 字符集被分為十七個子集(Plane,平面或位面),每個子集最多可包含 65536 個字元,因此總共可以有 1,114,112 個字元。其中第一個子集(Plane 0)包含最常用的字元,被稱為BMP(Basic Multilingual Plane, 基本多文種平面)。 BMP 中為 UTF-16 中的代理對(Surrogate Pair)保留了 2048 個位置,只剩下 63488 個有效字元空間(因此 Unicode 中實際最多有1,112,064個字元)。BMP 中的字元可以用四位十六進位制數(U+xxxx)表示,其它的字元需要五位或更多。

UTF-8, UTF-16, UTF-32

這些是 Unicode 標準中規定的幾種編碼方式。UTF 是 Unicode Transformation Format 的縮寫。

UTF-8是一種變長編碼,以位元組為基本單位,單個字元佔用的位元組數可能是 1 (U+0000 ~ U+007F,128個位置,所有的 ASCII 字元),2(U+0080 ~ U+07FF, 1920個位置,主要是各種字母和符號),3(U+0800 ~ U+FFFF,63488個位置,BMP中所有其它字元,包括絕大部分常用漢字)或4(U+10000 ~ U+10FFFF,1048576個位置,所有其他字元)。 UTF-8 的主要優勢在於相容 ASCII,面向位元組因而無需考慮位元組順序。現在 UTF-8 是網際網路上使用率最高的編碼方式。

UTF-16是另一種變長編碼,以 16 位(bit)為基本單位,單個字元可佔用單位數可能為 1 (BMP 中的所有字元)或 2(所有其它字元,這兩個單位被稱為代理對)。相對於另外兩種編碼,它最大的優勢是儲存大量東亞文字(中日韓)時佔用空間較少。由於基本單位不是位元組,而是 16 位,不同的系統通訊時需要考慮位元組序(Byte Order)問題。Windows, JavaScript, Java 等內部使用了 UTF-16。

UTF-32(又稱UCS-4) 是一種定長編碼,每個字元使用 32 位來表示。它的優勢在於實現和處理簡單,劣勢在於空間效率低。它同樣需要考慮位元組序問題。

在 Unicode 標準早期版本中,唯一的編碼方式是 16 位定長編碼(UCS-2),Windows 和 JavaScript 等採用了這種簡單高效的標準。後來 Unicode 字符集擴充,16 位空間不足以容納所有的字元,不得不產生了 UTF-16 的代理對機制,定長編碼尷尬地轉變成了變長編碼。而 Linux 由於反應較慢,躲過一劫,後來逐漸轉向了 UTF-8 編碼。

位元組序 (Byte Order)

不同系統中處理數值時可能採用的大小端序(Endianness)不同。比如在小端序(Little-endian)的機器上,32 位數字 1 在記憶體中表示為 4 個位元組:01 00 00 00;而在大端序(Big-endian)的機器上則表示為00 00 00 01。常見的 x86/x64, ARM 等處理器架構使用小端序,OpenRISC, SPARC等則使用大端序。不同系統間通訊時,或讀取其它系統儲存的檔案時,需要考慮位元組順序問題。

Unicode 編碼中可以使用字元U+FEFF來作為位元組序標記(Byte Order Mark),需要時在字元序列前新增該字元。該字元本身不被視為文字的一部分,但通過分析它的表示方式,處理程式可以判斷編碼時的位元組序,進而做出相應的處理。U+FEFF名稱為ZERO WIDTH NO-BREAK SPACE(零寬度非間斷空白),原本是有實際意義的。後來這個意義被新字元U+2060(WORD JOINER) 取代,U+FEFF現在僅用於標明位元組序。

編碼大端位元組序標記小端位元組序標記
UTF-8 EF BB BF EF BB BF
UTF-16 FE FF FF FE
UTF-32 00 00 FE FF FF FE 00 00

UTF-8 編碼的基本單位是位元組,無需考慮位元組序問題。儘管可以新增位元組序標記EF BB BF(字元U+FEFF在 UTF-8 中的表示方式),但標準推薦僅在特殊情況下使用。Windows 下記事本儲存為 UTF-8 格式時,會自動新增 BOM,用以與系統預設編碼(即記事本所稱的 ANSI編碼)區分。而 Linux/macOS 等其它作業系統上的軟體,一般只支援無位元組序標記的 UTF-8 編碼檔案,這樣導致檔案交換出現不少問題。 從 Windows 10 v1903 起,記事本也在儲存為 UTF-8 編碼時預設不附加位元組序標記了。

字素(Grapheme)與字素簇(Grapheme Cluster)

字素是文字在書寫時最小的單位,可以被理解為單獨的“字”。

在 Unicode 標準中,字元(Character)一般指程式碼點(Code Point)。通常,一個字素就是一個字元。但是,也有些字素是由多個字元序列組合而成的,這樣的字元序列被稱為字素簇。比如字母 é 可以用字母 e (U+0065) 加上重音符(U+0301) 組合而成。像重音符這樣用於修飾前一個字元的字元,被稱為組合字元(Combining Character)。可以使用多個組合字元來修飾同一個字元,這就是有一段時間內各個社群很流行的越界文字的技術根源。

超̷̪͓̫͕̳̝̔͐̋͌͑͗́̒̕͟͞越͓̻̗̙̙̠͖̔̆̌͑͐̽̊代̷͉̘̲̺̤͈̀͑̒͗̄͘̕͜碼̵̨̟͖͎̉̿͌͜͞͞͞,越界文字̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗̗

如同前面的例子所示,含重音符的拉丁字母可以使用基本字元加上重音修飾字符來表示。為保持與舊軟體系統的相容性,這種情況下 Unicode 中實際還包含了預先組合好的單個字元。即,某些字素可以有多個表示方式。上面的 é 既可以用字元序列U+0065 U+0301表示,也可以用單個U+00E9表示。這樣也帶來了新的問題,在字串比較,排序等操作前需要首先進行正規化(Normalization)。正規化即把所有可用單個字元表示的字元序列替換為對應的單個字元。

Collation

Collation 是字元間的排序規則。在一個 Collation 中,對應字符集裡所有的字元都有確定的排序先後關係,因而構成了全序關係。

文字排序的規則可以有很多,如字母表順序(Alphabetical order),數值順序等;針對漢字還有部首筆畫順序等。同一字元,在不同的語言中,所處的排序位置也有可能不同。排序時,還要考慮是否忽略大小寫,是否忽略重音/音調等,因此排序規則是很複雜的。Unicode 標準中提供了所有字元的預設的排序規則(Default Unicode Collation Element Table, DUCET),該規則也可根據不同的情況進行定製。開源專案ICU(International Components for Unicode)裡提供了各種語言的各種排序規則。

MySQL 與 UTF-8

在 MySQL 資料庫中,存在多個與 Unicode 相關的字符集和排序規則。

一般情況下應該使用utf8mb4字符集,這才是 MySQL 中真正的UTF-8編碼(如第一節所言,這裡字符集和編碼又一次互指了)。相應的所謂utf8字符集是非標準的,其中單個字元最多隻能編碼為 3 個位元組,因此很多字元無法儲存。

MySQL 中排序規則應該使用utf8mb4_unicode_ci,而不是utf8mb4_general_ci。後者效能稍微高於前者,但演算法不符合 Unicode 標準,可能在處理某些語言文字時出現錯誤的結果。utf8mb4_unicode_ci對應 Unicode v4.0,較新版本的 MySQL 還同時支援 v5.2(utf8mb4_unicode_520_ci),v9.0(utf8mb4_0900_ai_ci)。