[Tech] 字元編碼:從百家爭鳴到 Unicode
![想讓只看得懂 0 與 1 的電腦儲存字,必須要先把字透過某種方式編碼成二進位,等到要使用的時候再解碼回原本的文本。這個過程被稱作「字元編碼(character encoding)」,就像是一本雙語字典一樣,負責在人類語言以及電腦語言之間翻譯。 /image/202310/encoding-cover.png](/image/202310/encoding-cover.png)
人類社會使用的文字種類數以千計,光是在我們的日常生活中,就會看到漢字、數字、歐文以及標點符號等字符。
但想讓只看得懂 0
與 1
的電腦儲存這些「字」,必須要先把這些「字」透過某種方式編碼(encoding)成二進位,等到要使用的時候再解碼(decoding)回原本的文本。
這個過程被稱作「字元編碼(character encoding)」,就像是一本雙語字典一樣,負責在人類語言以及電腦語言之間翻譯。在編碼過程時,先在字典上找尋與文本與對應的代碼;而解碼過程則與之相反,會回頭查詢與代碼相對應的文本。
![](/image/202310/encoding-09.png)
假設在我們編寫的字典——即「字元集(character set)」裡,「晚」這個字被放在第 50
個位置、而「安」這個字被放在第 51
個位置。在編碼階段,會把「晚安」轉成在二進位裡代表 50
的「110010
」以及代表 51
的「110011
」;而解碼時,只要反查「110010
」與「110010
」,就能找到對應的「晚」和「安」。
EBCDIC 與 ASCII 的嘗試
在電腦剛誕生的時候,大多只被用來處理複雜的計算任務,資訊交換的必要性極低,所以字元編碼一事還沒有被特別重視。
不過,在大型商業電腦與通用作業系統的出現後,電腦不再只是一台計算機,隨著各種應用程式落地發展,我們開始需要和陌生人、隔壁公司、甚至是他國交換資訊——但大家的字典都不一樣呀,要怎麼讓雙方都能無歧意的交換訊息?
此時,人們才總算意識到,我們需要一本共用的字典,也就是一套泛用的字元集標準。 1964 年,作為當時大型商業電腦的領導者,IBM 公司推出了 EBCDIC(Extended Binary Coded Decimal Interchange Code,擴展二進位編碼的十進位交換碼)解決方案,嘗試將一些常用的符號與拉丁字母進行編碼。
如此一來,不同的電腦便能透過這本字典進行資料的交換與溝通。雖然 EBCDIC 開創了字元編碼的先河,但其設計時並沒有把英文字母放在連續的相鄰格子,這給程式處理帶來一定程度的麻煩。
![](/image/202310/encoding-10.png)
四年後的 1968 年,ANSI(American National Standard Institute,美國國家標準學會)在檢討 EBCDIC 的優點與缺點後,發布了影響後世極深的 ASCII(American Standard Code for Information Interchange,美國訊息交換標準碼)編碼方案。
和前輩的 EBCDIC 最大的不同就在於,ASCII 將英文大小寫放在連續位置上,極大方便了程式的處理。舉例來說,如果要將大小寫轉換,可以簡單的對 ASCII 索引值加減 32 來進行轉換,但 EBCDIC 就必須考慮到字符中間不連續的狀態。
|
|
![](/image/202310/encoding-12.png)
在 ASCII 的設計裡,七個位元(bit)來表示一個字,每個位元都可以是 0
或 1
,因此共有 2^7=128 種組合,足以表示 128 個不同的字符——這個數量用來紀錄英文字母已經相當綽綽有餘。
另一方面,為了確保資料在傳送時的可靠性,廠商通常會額外再原有編碼之上添加一個位元,用作校驗使用。這個最高的位元通常被預留為 0,而每個字就是以這七個位元加上最高位的校驗位元,共八個位元來表示。
![](/image/202310/encoding-02.png)
舉例來說,大寫的 A 在 ASCII 裡面被放在第 65
號位置(更前面還有符號跟看不見的控制字元),以位元來表示的話就是 0100 0001
、小寫 a 則在第 97
個位置,以位元來表示的話就是 0110 0001
。
擴充 ASCII
很快地,ASCII 在美國與英國獲得了成功,成為了那個年代裡最通用的編碼標準。但對於其他擁有變音字母的歐洲語言來說,例如法文 café 裡的 é,又該怎麼處理?原本規劃的 128 個格子已經塞滿了呀。
於是,有廠商把腦筋動到了最高位的位元上——反正目前這一位都是 0
,只要把他改成 1
,就可以再額外「擴充」出 128 個格子,用來放這些變音字母。像是 IBM 就曾替擴充拉丁文字、希臘文字、西里爾文字等設計擴充的 ASCII 方案。
簡而言之,由於使用八個位元來表示一個字符,只要該書寫系統所需的文字數量不多於 2^8=256 個,便能透過 ASCII 與擴充方案進行編碼交換。
![](/image/202310/encoding-03.png)
但這種權宜之計依舊會遇到交換問題——如果我跟你的擴充方案不一樣,對應的字符就會是不同,「亂碼」就出現了。此外,當 1980 年代電腦登陸東亞市場,面對動輒數千、甚至數萬的漢字又要怎麼處理?
於是,東亞各國的電腦廠家便自行在原本通用的 ASCII 標準上進行額外的擴充,誕生出不同的地域標準,例如日本工業標準 Shift JIS、中國國家標準 GB2312、台灣業界標準 Big5 等等。
在 ASCII 嘗試統一編碼之後,我們很難過地又重回字元編碼的戰國時代,各個「標準」的高牆再度如雨後春筍般的築起,這對於資訊的推廣來說相當不利。
我們需要一個足以大一統的字符編碼標準,一本足以囊括全世界文字的字典。
Unicode 的統一
在混亂的背景下,1987 年,任職於 Xerox 的 Joe Becker、Lee Collins 和任職於 Apple 的 Mark Davis 萌生了統一字元編碼的想法並開始傳教。到了 1991 年,當時電腦界的大公司們聯手成立了 Unicode 聯盟(Unicode Consortium),希望能解決編碼標準不統一的問題。
在一開始,Unicode 聯盟將 ASCII 失敗的原因歸咎為擴充性不足——藉由八個位元來表示一個字,使其至多僅能放入 256 個字,這個數量實在太少了,不如改用 16 個位元來表示一個字,這樣就有 2^16 = 65,536 個格子可以用了,比 ASCII 多了 256 倍!用來收錄文字應該已經很夠了——嗎?
在 Unicode 1.0 釋出時,僅收錄了 24 種書寫文字、共 7,129 個字符。由於當時漢字的整理還沒結束,所以這個版本裡並沒有收錄漢字。
不過很快地,面對東亞各國不斷提交的新的漢字來源,以及眾多學術單位有將歷史文本電子化的需求,希望連已經消亡的古代文字都收錄進標準。面對大家爭先恐後提交的行為,Unicode 聯盟發現他們輕忽了標準所需的數量。
於是,在不改動原本規劃的 65,536 格子——後來這一區被稱作基本多語言平面(Basic Multilingual Plane, BMP)——之外,Unicode 聯盟額外規劃了第 1 到第 16 輔助平面(Supplementary Planes),每個平面都可以放入 65,536 個字符。
![](/image/202310/encoding-05.png)
在這總共 17 個平面(plane)裡,至多可以容納 1,114,112 個字符,藉此來收錄那些或許你曾聽過的、也或許沒聽過,目前人類正在使用的、或是曾經在歷史上被使用過的文字。
在經過 30 餘年的搜羅後,在 2023 年 9 月公告(也就是最近!)的最新版 Unicode 15.1 中,已經收錄了多達 161 種書寫文字,其中包含 94 種當代文字、以及 67 種歷史曾使用的文字,共 149,813 個字符。
![](/image/202310/encoding-06.png)
表示方法
Unicode 習慣透過十六進位的方式表示字符的碼位(code point),並帶有「U+
」的前綴。
在 BMP 上的字符碼位範圍為四位數的 U+0000
到 U+FFFF
。而為了向下兼容 ASCII,前 128 個碼位,也就是英文字母與常見符號的區域,與 ASCII 的編碼相同,這讓使用 ASCII 紀錄的資料很容易的可以過渡到 Unicode 規格。
基本上,我們日常生活中會看到的文字,像是拉丁文字、阿拉伯數字、符號、常見漢字、日文假名、韓文、希臘字母、西里爾字母都是放在這一區。
而輔助平面則對應五位數編號的 U+10000
到 U+10FFFF
。其中第一輔助平面放置了一些已經消亡的古代字母(像是楔形文字、埃及聖書體、線性文字),比較特別的是 Emoji 也被放在這一個平面上。
![](/image/202310/encoding-11.png)
第二與第三輔助平面基本上都是一些極為罕見的漢字,提交源頭基本上都是姓名罕用字、地名罕用字、宗教用字(例如道教經書、神道教經書)、甲骨文隸定字或是古典裡的訛字。 第四到第十三輔助平面目前都是空的(好消息是至少還有這麼多的空間可以放),第十四輔助平面則是用來放置控制字元。
與私人使用區的互補
雖然 Unicode 已經盡可能地羅列全天下的文字,但仍有相當多的文字因為尚待整理、還沒有共識而未被正式收錄。
例如 瑪雅文字(Maya Script) 與 甲骨文 ,就因為寫法多變、標準化困難而仍處於草案階段。舉例來説,在之前甲骨文的草案中,就把「中」字的 19 個造型分開編碼,雖然彼此都有些微的差異,但對於是否應該要統一成同一形狀,還未有定論。
![](/image/202310/encoding-04.png)
另外就是,Unicode 已經明定不會收錄人造或虛構的文字,例如《魔戒(The Lord of the Rings)》裡的談格瓦(Tengwar)字母、《星艦迷航記(Star Trek)》的克林貢(Klingon)字母、流傳於文藝復興時期歐洲的底比斯女巫字母(Theban)就不在收錄範圍內。
![](/image/202310/encoding-08.png)
但這些沒有被收錄進 Unicode 的「文字」,在學術界或是同好圈子裡,仍然會有需要「交換」的時機呀,此時又該遵從哪套標準?
為了避免因為沒有被列入標準就無法被交換的尷尬情況發生,Unicode 將基本多語言平面的後半部(U+E000
-U+F8FF
,共 6,400 個碼位)以及第十五、十六平面(分別為 U+F0000
-U+FFFFD
,U+100000
-U+10FFFD
,各 65,534 個碼位)定義為私人使用區(Private Use Area,PUA)。
![](/image/202310/encoding-07.png)
舉例來說,愛沙尼亞語言研究院(Eesti Keele Instituut,EKI)便曾經使用 PUA 放置那些還沒有被納入標準的衍生拉丁字符,例如 U+E00A
的「M WITH CEDILLA」;台灣的教育部萌典也曾將 U+FF545
定義為客語用字「𱱿」(⿰皮卜,該字後來被正式收錄到 Unicode Ext-H,碼位為 U+31C7F
)。
在 PUA 裡,每個編碼的樣子和意義都可以任由國家、私人企業、組織、同好會、甚至是自己去定義。一旦有其他人願意接受同樣的規則,私人使用區的碼位便有了意義,訊息的交換也有了可能。
碼位排序 = 字母順序…嗎?
Unicode 的碼位順序為排序帶來了一定程度的方便,舉例來説,想要讓幾個詞按照英文字母的順序,只要由前往後,逐字比較 Unicode 碼位大小即可,但如果有擴充拉丁字母怎麼辦?
許多程式語言內建的 sort()
函數其實都是基於 Unicode 碼位的順序,也因此常常會出現變音符號順序在後的問題,例如:
|
|
caff 居然會比 café 還要更前面——因為 f
(U+0066
) 比 é
(U+00E9
) 還要前面,但在法文裡,é
不被當成獨立字母看待,而是 e
加上尖音符,所以位置應該要和 e
相同位階。
另外一個子是,在英文中 n
的後面接的是 o
,但在西班牙文中,n
的後面是 ñ
(西班牙語就是 español),再來才是 o
。如果直接排序,因為 ñ
(U+00F1
) 的碼位在 n
(U+006E
) 跟 o
(U+006F
) 的後面,所以位置會被放到最後面。
|
|
對於某些有跨國客群的應用程式,這部分的順序要特別注意。
收錄原則
最後,讓我們來看看 Unicode 的設計原則:
- Universality:提供單一且通用的字元集,將正在使用或歷史曾使用的字進行編碼。
- Efficiency:易於處理與分析。
- Characters, not glyphs:對抽象的字符進行編碼,而不是對於字形編碼。
- 換句話說,這個這個字長怎樣是由字型檔案所決定,Unicode 只管理抽象意義的編碼。
- Semantics:字元要有對應定義的語意。
- Plain text:僅收錄於文字。
- 話雖如此,卻有種符號越收越多的感覺…
- Logical order:以邏輯順序保存。
- Unification:統一不同語言中、同一種書寫系統中的相同字元。
- Dynamic composition:允許附加符號與變音符號的動態組合。
- Stability:字符一但被分配了碼位,就不會變動、移除、釋出,或是重分配。
- 理論上應該要這樣,但歷史上因為各種
大人的因素,還是有編碼搬過家,不過那都是很久以前的事情了,現在已經不會再出現類似的抓馬了。
- 理論上應該要這樣,但歷史上因為各種
- Convertibility:與其他常見的字元編碼集(例如 ASCII)可以互相轉換。
尾聲
從最初 EBCDIC 和 ASCII 的發想與創立,到 Unicode 的統合與整理,字元編碼的進程可以說是當代網際網路世界得以交流的基礎,不僅促進了跨文化的互動,讓世界得以更緊密的連接在一起、溝通無礙;同時也妥善紀錄了人類社會的文化與歷史,讓過去的古籍經典得以數位的形式恆常保存。
本文同步刊於 justfont blog 與 iThome。詳見人與電腦資訊交換的雙語字典:字元編碼、DAY 04 |字元集的難題:在 Unicode 之前、DAY 05 | Unicode 的統一與原則。