字符集編碼(三):Unicode

前面《字符集編碼(上):Unicode 之前》我們講了在二十世紀九十年代 Unicode 出現之前各廠商和標準化組織爲了應對不同語言文字的編碼需求而設計了各種互不兼容的字符集編碼標準,這使得軟硬件開發商在處理多語言環境時相當棘手。爲了解決字符集編碼各自爲政的亂象,一些利益相關公司開始湊到一起試圖設計一種新型的、可囊括全世界所有字符的統一編碼標準。


開端

1987 年,蘋果(Apple)和施樂(Xerox)兩家公司的三個技術人員湊到一起,着手研究開發通用字符集的可行性。在 1987 年末到 1988 年初的幾個月裏,他們進行了一系列的調查統計,主要弄清這麼幾個事情:

  • 該字符集大約需要包含多少個字符?
  • 目前世界上使用雙字節編碼的字符數量?
  • 應該採用固定寬度(定長編碼)還是混合寬度(變長編碼)的編碼方式?
  • 中、日、韓的表意文字可以統一嗎(即同樣的字符同時出現在中日韓三種語言中,是否可以只編碼一次)?

在設計統一碼的時候,世界上已經存在大量的雙字節編碼標準(如 GB 2312、Shift JIS、Big5 等東亞編碼標準),而且當時施樂也設計了一套雙字節的多語言編碼標準,所以最初設計統一碼時,大家是傾向於使用雙字節的,而雙字節最多能表示的字符數量是 2^16 = 65536 個,所以接下來的重點就是驗證全世界的字符總數是否大於這個數。

工作小組的驗證結果是肯定的(雖然在我們看來有點出乎意料,因爲光漢字就不止這個數目了)。當時工作組的原則是只考慮現代字符(也就是現代語言文字中在使用的,不考慮古埃及、古巴比倫、古漢語等現代語言不再使用的字符),而且傾向於使用字符組合(而不是對複雜字符單獨編碼),比如西班牙語的 ñ 由 n 和 ~ 兩個字符組成,大量的韓語也是組合出來的(其實大家還考慮過通過偏旁部首組合漢字,但發現漢字構造過於複雜,就放棄了)。在這些原則下,工作組統計了當時全世界的報紙等刊物,得出了一個結論:兩個字節足以囊括全世界有實用意義的字符。

早期工作組的另一個設計傾向是採用定長編碼形式

決定採用雙字節編碼後,面臨兩個選擇:一是採用變長編碼形式(類似 GB 2312),對於 ASCII 字符使用一個字節,其他字符使用兩個字節;另一種是採用定長編碼形式,不管是不是 ASCII 字符統一使用兩個字節。

工作組主要從存儲和處理效率的角度驗證兩種方案的優劣,其驗證結論是雙字節定長編碼所帶來的文本尺寸增長是可接受的(和其它信息如文本格式信息、圖片、視頻等比較起來,字符本身的編碼信息佔用的空間小的可憐),而定長編碼形式在各方面的處理效率都優於變長編碼形式,所以早期 Unicode 採用了定長編碼形式。

工作組面臨的第三個問題是漢字

東亞國家由於受到中化文化的影響,它們的語言中包含了一些漢字字符(如日語、韓語),這些(不同語言)中的相同字符(漢字)到底是算作同一個字符呢還是不同字符呢?

在上一篇討論字符編碼模型的文章中我們說過,字符集編碼(特別是 Unicode)是對抽象字符編碼,而不是對字形或者字意編碼。按照這個原則,這些漢字應該算同一個字符,雖然它們在含義上不同(可能在字形上也存在些許差異)。

所以工作組決定將中日韓語言中的漢字部分合並叫中日韓統一表意文字(CJK,中日韓三種語言的首字母)——所以你在 Unicode 區塊表中是搜不到諸如 “Han”、“Chinese” 的,漢語是在 CJK 和 CJK 擴展塊中。

Unicode 將中日韓漢字合併這點是有爭議的。因爲 Unicode 是針對抽象字符而非字形編碼,而同一個漢字在不同語言中寫法(字形)可能不同(比如中國漢字“帶”在日語中寫作“帯”,以及同一個字的古今寫法可能也不同),對於一些寫法不同的漢字 Unicode 也認爲是同一個字符。所以有些人認爲這種合併會讓人覺得語言本身失去了獨立性。

最初的調研成果總結起來就是:

  1. 使用雙字節定長編碼;
  2. 中日韓漢字合併編碼;

這些研究成果於 1988 年 8 月以草案的形式發佈(後稱爲 Unicode 88),在該草案中正式使用 “Unicode” 一詞,中文翻譯爲“統一碼“。

後來,越來越多的公司加入到這個工作組中,包括 Sun、微軟、NeXT 等。這些公司於 1991 年 1月決定在加州成立一個正式的聯盟,就叫Unicode 聯盟(Unicode Consortium),並於同年 10 月發佈了 Unicode 的第一版(Unicode 1.0.0。注意 88 年的那個叫草案,只是公佈了最初研究成果,很多工作還沒完成呢)。該版僅包含 24 種語言文字共 7163 個字符。

注意第一版 Unicode 標準中並不包含 CJK 字符——此時 CJK 部分的工作尚未完成。CJK 部分是在第二年(1992 年 6 月)的 Unicode 1.0.1 中加入的(此版本包含 20902 個漢字)。

如今(2022 年 2 月)最新版 Unicode 已經到了 14.0.0(於 2021 年 9 月發佈),支持 159 種文字,共包含 144,697 個字符(包括控制字符、文字符號、表情符號等)。


另一個兄弟

Unicode 聯盟並不孤單,當這幫人在設計 Unicode 標準時,另一個組織也在幹同樣的事情。

ISO 和 IEC 兩家組織在 1984 年就成立了一個聯合工作組來設計一套新的統一字符集——也就是後來的 UCS(Universal Character Set),於 1989 年公佈了 UCS 草案(Unicode 工作小組在 1988 年發佈了 Unicode 草案)。

這兩個工作組起初誰也不知道對方的存在,直到雙方公佈了各自的設計草案並被傳閱後,大家才逐漸意識到兩者乾的是同樣的事情,這會導致世界上存在兩套統一編碼字符集標準,對雙方以及對世人都沒有好處。

於是這兩個工作組於 1991 年開始謀求合併方案。

所謂合併,並不是簡單地一個工作組完全拋棄自己的東西去擁抱另一個工作組的標準。如果你花費大量人力物力搞了個東西,然後發現別人也搞了個幾乎一樣的,你願意完全放棄自己的這套?另外雖然兩個組織都奔着相同的目標(統一字符集),但在設計原則和一些細節上還是有很多不同的,更麻煩的是,當大家坐下來談合併事項時,Unicode 1.0 已經發布了,UCS 還處於草案階段,但也離發佈不遠了。出於各方面的原因,合併的前提是仍然保持兩個標準都獨立存在和發展,在此基礎上保持兩個標準的兼容和同步(或者其中一個是另一個的子集,或者兩個在編碼字符集 CCS 層面完全一致)。

和 Unicode 使用 16 位編碼空間不同,UCS 一開始就選擇使用 31 位編碼空間,也就是說,UCS 最多可以容納 2^31 約 21 億個字符。最開始,Unicode 打算作爲 UCS 的真子集,即 Unicode 中的每個字符都存在於 UCS 中,而且兩者的碼點相同,但 UCS 中的字符(編碼超過 64K 的)則不一定存在於 Unicode 中。

經過多次的爭論、投票,最終雙方都作出了一些妥協,在字符集層面達成了一致,即兩個標準中相同字符的編碼(碼點)必須是一樣的,Unicode 針對 1.0 版本做了一些調整(如調整一些字符的編碼,調整某些語言文字的區塊),最終 ISO/IEC 在 1993 年發佈了 UCS 的第一個版本 ISO/IEC 10646-1:1993,Unicode 聯盟也在同一年發佈了兼容版本 Unicode 1.1。

當然,合併工作是分步進行的,合併工作成果也是分版本發佈的,1993 年的版本發佈只是雙方初步的(也是最重要的)合併成果發佈。


Unicode 概覽

網上有一種提問:“Unicode 和 UTF-8 是什麼關係?”

很多回答說:“Unicode 是字符編碼,UTF-8 是編碼實現。”

這種回答並不準確。Unicode 是字符編碼不錯,但一般人理解的字符編碼僅僅是指給字符編號(字符編碼模型中的第二層),這給人感覺好像 Unicode 和 UTF-8 是兩個平行的、獨立的東西。實際情況是 Unicode 標準囊括了字符編碼模型的所有層次,從抽象字符集的定義到計算機編碼方案,UTF-8 屬於 Unicode 標準中編碼實現部分。

Unicode 設計之初只打算對人類正在使用的字符進行編碼,而不考慮曾經使用但現在已經廢棄的古文字,因而起初覺得雙字節已經足夠。不過很快大家就發現該思路走不通,因爲一些文字符號雖然在日常生活中用不到,但在特殊領域會用到(比如歷史、考古、語言學等),如果 Unicode 不包含這些字符,則這些特殊領域就必須設計另外的字符集標準,這有違 Unicode 初衷。於是很快 Unicode 聯盟就決定拓寬 Unicode 編碼空間,從 16 位拓寬到 21 位,共可表示一百多萬的字符(其中有些空間屬於保留空間或私人空間,不可分配碼點)。

Unicode 不對非人類語言文字編碼,比如騰格瓦語(Tengwar,《魔戒》作者托爾金創造的精靈語文字)、克林貢語(Klingons,《星際迷航》中克林貢人使用的語言)。

一個抽象字符可能有多種形狀(字形),但由於都具有相同的含義,在 Unicode 中被視爲同一個字符,只會分配一個碼點,比如漢字有楷、行、草、隸等寫法,這些屬於同一個字符的不同字形。需要注意,這裏所說的“字形”是指不同的書寫方式導致的字形差異,而非結構性差異,比如繁體中文和簡體中文,從字源來說,簡體中文是對繁體中文的簡化寫法,屬於同意不同字,但這兩者的差距是體現在結構上而非書寫形式上,所以要視爲不同的字符。

一個字形到底是由一個字符構成的,還是由多個字符組合成的,是一件見仁見智的事情。比如字符 é,你可以認爲它是一個獨立的字符,也可以認爲是由拉丁字母 e 和音調字符 ́ 構成。傳統編碼傾向於作爲獨立字符看待,而 Unicode 傾向於作爲組合字符——Unicode 傾向於使用簡單字符組合複雜字符

另外,抽象字符不一定就存在可視化的字形,比如控制字符。

在 Unicode 字符集中,每個抽象字符都有唯一的名稱,使用大寫 ASCII 字符表示,比如拉丁字母 a 在 Unicode 中的名稱是“LATIN SMALL LETTER A”。可在 http://www.unicode.org/Public/UNIDATA/NamesList.txt 查看所有字符的名稱。

Unicode 使用整型數值對這些抽象字符編碼,在書寫上,用數值的十六進制表示,且至少是 4 位,少於 4 位的使用前導 0 填充,比如 61 要寫成 0061。另外要在數值前面加上 U+ 表示是 Unicode 碼點,因而拉丁字母 a 的 Unicode 碼點寫作 U+0061。

數值編碼(碼點)可能的範圍叫編碼空間(codespace)。起初 Unicode 的編碼空間是 U+0000 ~ U+FFFF,大家很快發現 64K 的編碼空間根本不夠用,所以後來將編碼空間擴大到了 U+0000 ~ U+10FFFF,可容納一百多萬的字符。

這一百多萬的編碼空間被劃分成 17 個平面(planes,17 個大小相同的區域,編號 0 ~ 16),每個平面可容納 2^16 即 65,536 個碼點。每個平面的作用不一樣:

image-20220225153242675

其中最重要的平面是基本平面 Plane 0,也叫 BMP,全世界日常使用的字符都在該平面中(早期 Unicode 就是以該平面作爲整個編碼空間);Plane 2 和 Plane 3 是給漢字擴展用的;最後兩個平面是私有編碼空間(PUA),不會分配字符碼點,專門給軟件自定義用的。

這些平面又進一步劃分成(block),每個塊放一組特定的字符,如 0000~007F 放基本拉丁字母(ASCII 字母),0590~05FF 放希伯來文,4E00~9FFF 放中日韓統一表意文字(我們最常用的漢字就是在這個塊)

# Unicode 字符塊(部分)

0000—007F 基本拉丁字母
0080—00FF 拉丁文補充1
0100—017F 拉丁文擴展A
0180—024F 拉丁文擴展B
...
0370—03FF 希臘字母及科普特字母
0400—04FF 西裏爾字母
0500—052F 西裏爾字母補充
0530—058F 亞美尼亞字母
0590—05FF 希伯來文
0600—06FF 阿拉伯文
...
0E00—0E7F 泰文
0E80—0EFF 老撾文
0F00—0FFF 藏文
1000—109F 緬甸文
...
2200—22FF 數學運算符
...
2E80—2EFF 中日韓部首補充
2F00—2FDF 康熙部首
2FF0—2FFF 表意文字描述符
...
4DC0—4DFF 易經六十四卦符號
4E00—9FFF 中日韓統一表意文字
A000—A48F 彝文音節
A490—A4CF 彝文字根
...

一種語言文字可能分散在多個塊中,如漢字就存在很多擴展塊,不過最常用的漢字都是在 4E00~9FFF 中。

image-20220225161242070

其中一個擴展塊中的漢字,都是我們沒見過的,平時根本用不到


Unicode 的三種表示形式

Unicode 設計之初是採用雙字節定長編碼的,其碼點和計算機層面表示形式(編碼模式中的第三層 CEF)是一致的。比如漢字的“漢”的 Unicode 碼點是 U+6C49,其計算機編碼表示就是 6C49——這就是 UTF-16 的早期樣子。這種編碼方式的優點是高效,不需要檢查標誌位。

後來大家發現 16 位編碼空間根本不夠用,於是將編碼空間拓展到 21 位。由於原始的 UTF-16 編碼形式無法表示大於 FFFF 的碼點,於是對 UTF-16 也進行了拓展,使其既能用 1 個碼元表達 BMP 中的字符,也能用 2 個碼元表示補充平面的字符——這就是現代版本的 UTF-16。

在 Unicode 認爲自己的 16 位編碼空間太小的同時,ISO/IEC 也覺得 UCS 的 31 位編碼空間太多了,實際中根本沒有幾十億字符。所以最終 Unicode 聯盟和 ISO/IEC 工作組達成一致:兩者使用統一的編碼空間 0000 ~ 10FFFF(即 UCS 保證永遠不分配大於 10FFFF 的字符碼點),而且雙方在字符編碼上保持同步,即一方標準中增加了字符,也要通知另一方同步。

使用雙字節定長編碼還存在另一個——可能是更要命的——問題:它無法在編碼形式層面兼容 ASCII 碼。雖然 Unicode 在碼點層面(第二層)兼容 ASCII(U+0000~U+007E 的碼點分配和 ASCII 一致),但由於在計算機編碼層面,Unicode 使用兩個字節,而 ASCII 使用一個字節,這導致採用 Unicode 編碼標準的軟件無法正確處理現有的 ASCII 編碼文件。

Unicode 1991 年才發佈,ASCII 在 1968 年就發佈了,這二十多年間產生了大量的 ASCII 文件和使用 ASCII 標準的軟件,Unicode 置這些現存文件和軟件不顧的後果就是新興的 Unicode 標準很難被全世界(特別是計算機重度使用區歐美)廣泛接受。Unicode 要想快速普及,就必須完全兼容 ASCII,因而 Unicode 聯盟很快推出了 8 bit 碼元編碼方案:UTF-8。UTF-8 和改進後的 UTF-16 一樣是變長編碼方式,ASCII 字符采用單字節編碼(最高位是 0),其它字符可能採用 2~4 字節,比如常用漢字用 3 字節(一些不常用漢字會用到 4 字節)。

關於爲何現在 UTF-8 成爲 Unicode 標準中最廣泛使用的編碼方式,網上大多數的回答都是說因爲 UTF-8 在編碼拉丁字母時更節約空間,所以歐美公司傾向於使用 UTF-8。

在我看來,這可能是原因之一,但並非主因。

節約空間並不是導致 UTF-8 被廣泛使用的主因。想一想當時設計 Unicode 的都是誰,蘋果、施樂、微軟等等,這些都是當時或未來計算機行業的代表,他們肯定是按照自己的實際訴求來設計 Unicode 的,是在做了充分調研、實際測試驗證後,得出雙字節編碼並不會造成拉丁語系文本空間顯著增長的結論,才決定用雙字節定長編碼。

真正讓 UTF-8 廣爲接受的恰恰就是歷史遺留下來的那些 ASCII 文本和程序(以及操作系統、編程語言)。

這個論點從直覺上可能覺得不可思議——因爲人們直覺總是覺得過去無關緊要,現在和未來纔是重要的(比如 Unicode 設計之初就不打算考慮古文字,選用雙字節編碼也是不打算徹底兼容 ASCII),然而真正決定未來的往往就是歷史——人類文明如此,項目的成敗亦是如此。

我們必須正視兩個事實:1. Unicode 比 ASCII 晚了二十多年;2. 其他傳統編碼基本上都兼容 ASCII。這兩點導致在 Unicode 發佈的時候,世界上必然存在大量的 ASCII 文本和使用或兼容 ASCII 的軟件。

於是 Unicode 出來後,各大軟硬件廠商面臨幾個選擇:

  • 完全擁抱 Unicode,這將造成新舊不兼容(比如文字處理軟件 2.0 無法處理 1.0 生成的文件),這很可能導致新產品賣不出去;
  • 完全不用 Unicode,以前該怎麼苦逼繼續怎麼苦逼;
  • 開發轉換工具,爲新老文本和工具做雙向轉換(但這種方式無疑是彆扭的);

如果是你,你會怎麼選呢?有可能是新產品用 Unicode,老產品繼續用老編碼標準,也有可能直接不用 Unicode——因爲你還要考慮公司產品之間的兼容性問題。

所以當各大公司發現 UTF-8 能完美解決兼容性問題,自然都跑去用 UTF-8 了。大家都用,那 UTF-8 自然就被推廣開了,而且人家都用,你不用,你的產品在跟別家互操作時就會出現兼容性問題,進而就會被市場淘汰——所以你也必須用。

我們用上帝視角設想,假如 1968 年的那個標準不是單字節編碼的 ASCII 而是雙字節編碼的 Unicode(雖然從歷史環境來說不太可能),那麼很可能今天就壓根沒有 UTF-8 編碼。UTF-8 編碼也僅僅是在 ASCII 字符區域節約空間,在拉丁擴展區域(也就是歐洲拉丁字母)和 UTF-16 一樣佔兩個字節,而在常用漢字區域則比 UTF-16 多使用一個字節。全世界最常用的字符都是在基本平面 BMP 中,UTF-16 在該平面恆定使用兩個字節編碼,其效率近似於定長編碼方式,而 UTF-8 則使用 1~3 個字節編碼,是真正的變長編碼——文字處理軟件可以針對 UTF-16 做定長假定優化,對 UTF-8 則不行(也即是說,文字處理軟件可以假定 UTF-16 文本都是雙字節編碼,當真的遇到四字節時再做特殊處理,但不能對 UTF-8 這麼做)。

所以,UTF-8 之所以會勝出,不是因爲 UTF-8 在技術上比 UTF-16 有多大優勢(雖然 UTF-8 設計得很巧妙),而是因爲 UTF-8 在那時解決了各大公司的痛點——更準確地說,UTF-8 是爲了解決大家的痛點纔出現的。

參考資料:UTF-8 history; Early Years of Unicode;

另外,UCS 一開始就是支持 32 位碼元的(人家從一開始就是 31 位編碼空間),爲了和 UCS 保持一致,Unicode 也支持 32 位碼元:UTF-32。

UTF-X 中的 X 表示碼元位數。

Unicode 在 2.0(1996 年) 中正式引入了 UTF-8 和改進後的 UTF-16,在 3.1(2001 年) 版本中引入了 UTF-32。

我們在下一篇將詳細介紹三種編碼方式的實現細節,此處僅做概要介紹。


妥協

Unicode 有兩個設計原則:

  1. 抽象字符原則:面向抽象字符而不是字形或字意編碼;
  2. 動態組合原則:使用簡單的字符組合複雜字符,而不是爲複雜字符單獨編碼;

爲了讓 Unicode 能夠被廣泛地接受,Unicode 聯盟在設計之初做了一項重要決定:Unicode 必須完全兼容現有的所有字符集編碼標準。這個兼容是“雙程”的:任何現有字符集中的任何一個字符,可以轉換成 Unicode 字符集中的字符,並且從 Unicode 中的這個字符再轉換回去後還是原來那個字符,這個規則稱爲 round-trip rule。

這個規則讓 Unicode 在實現上做了很多妥協。

我們在上篇文章中舉過拉丁字母 K 的例子。在一些傳統的編碼標準中,拉丁字母 K 和熱力學單位 K(開爾文)被當做兩個不同的字符,

爲了實現 round-trip 規則,Unicode 中也必須編碼兩個 K(分別是 Latin Capital Letter K U+004B 和 KELVIN SIGN U+212A)——否則那個傳統編碼中的兩個 K(在那邊的碼點是不一樣的)轉換成 Unicode 編碼後變成同一個 K 了,再轉回去就不知道對應誰了。

類似的情況在漢語中也有很多。比如 U+2F08 和 U+319F 都是漢字“人”,U+2F17 和 U+3038 都是漢字“十”,U+03A9 和 U+2126 都是 Ω(一個是希臘字母,一個是電阻符號)。

中國的 GB 編碼和日本的 JIS 編碼在兼容 ASCII 的同時,又給 ASCII 中的可見字符做了個“全角”編碼(原 ASCII 中的字符被稱爲“半角”字符)。所謂全角和半角字符,在字形和字意上都完全相同,只是全角字符佔用寬度(注意不是字形本身的寬度)是半角字符的兩倍(據說是爲了中英文混排時的美觀效果),按照 Unicode 的設計原則,這種問題應該交由文字渲染程序去處理,但由於傳統編碼標準中做了獨立編碼,所以 Unicode 中也必須支持,在 Unicode 編碼表中也能看到一系列 Full Width 的拉丁字母。

另外有些傳統編碼標準中對不同書寫體的拉丁字母也做了不同編碼,Unicode 同樣需要兼容之。

注意:一個抽象字符在傳統編碼中之所以爲不同的書寫體編碼,肯定是有原因的(一般是不同的書寫體表達不同的含義,比如數學符號;或者表達不同時期的寫法,如中世紀裝飾體)。比如拉丁字母 C 的變體 ℭ,A 的變體 𝒜、𝑨,它們在 Unicode 中擁有不同的碼點。具體可在 NameList 中搜 font。

當然另外一點是,Unicode 選擇爲不同的書寫體分配不同的碼點,不完全是爲了兼容的考慮,很多(特別是數學符號)是爲了現實要求(比如數學領域的一致要求)而有意違背設計原則(具體可參見 Symbols的說明)。

image-20220225231217484

Unicode 中同一個字符的不同字形或字意被重複編碼。注意源自東亞標準中的 FULL WIDTH

這些重複編碼違背了 Unicode 設計中的抽象字符原則

在傳統編碼中很少有組合字符的說法,所以諸如 Å(瑞典語字符,以及長度單位“埃”)在傳統編碼(如 ISO/IEC 8859)中視作一個獨立字符,但在 Unicode 中視作兩個字符 A(U+0041)和 ̊(U+030A)的組合字符。爲了兼容傳統編碼,Unicode 在支持組合的同時,還必須將該字形視作單獨的字符分配額外碼點(U+00C5)——Unicode 中稱這種字符爲預合成字符(precomposed character)。

Å 不但存在動態組合與預合成的問題,該字符本身由於在一些傳統編碼標準中作爲長度單位“埃”和作爲拉丁字母 Å 做了不同的編碼,Unicode 中也必須作此重複(U+00C5:LATIN CAPITAL LETTER A WITH RING ABOVE;U+212B:ANGSTROM SIGN)。

Unicode 中存在大量的這種違背動態組合原則的字符。

image-20220226002130115

大部分韓語在 Unicode 中也是可以組合的,所以也存在多種編碼的可能。

Unicode 中視預合成字符和動態組合字符是等效的,也就是說,如果文本中存在兩個 Å,一個是組合的:<U+0041,U+030A>,另一個是預合成的:U+00C5,則軟件應該將其視爲一種字符,搜索的時候兩個都應該能搜出來——不過目前貌似很多軟件並沒有實現這一點。

Unicode 基本概念就介紹到這裏,下一篇我們講講 Unicode 的三種計算機編碼實現:UTF-8、UTF-16 和 UTF-32。



原文鏈接:字符集編碼(中):Unicode

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章