Universal Character Set (UCS) and Unicode Transformation Format (UTF)
對於Unicode如何組成二進位資料這件事情,各方有不同的想法,因而發展出多套各有特色的Unicode編碼方式,其中最出名或最被廣泛使用的三大編碼機制為:UTF-32(UCS-4)、UTF-16(UCS-2)、與UTF-8。它們的編碼細節以及優劣分析將在下面說明。
UTF-32編碼細節
字元編碼表其實就是數字與文字的對應,加上Unicode的字碼範圍又這麼大,因此很直覺的就可以想到把Unicode的字碼當成一個32位元(4個位元組)的整數來儲存。這是最直接最原始的編碼辦法,下表將以4個文字範例做為解釋:
文字 |
Unicode編號 |
編碼結果(BE) |
編碼結果(LE) |
A |
41 |
00 00 00 41 |
41 00 00 00 |
Ü |
DC |
00 00 00 DC |
DC 00 00 00 |
⿂ |
2FC2 |
00 00 2F C2 |
C2 2F 00 00 |
𡾞 |
21F9E |
00 02 1F 9E |
9E 1F 02 00 |
表4:UTF-32編碼範例
有些觀眾朋友可能會注意到上面的編碼結果分為大端序(BE)與小端序(LE)兩種,解釋這兩種位元組順序會離題太遠,對於不知道CPU位元組順序的朋友只要記得一個整數如3E9C4B25儲存在記憶體中時,其4個位元組前後排列的資料可能是3E 9C 4B 25 (BE、大端序)或25 4B 9C 3E (LE、小端序)這兩種排列就可以了。也因為位元組順序的關係,使得UTF-32的編碼又被細分為UTF-32BE及UTF-32LE兩種。
另外,我們偶爾會看到UCS-4這種編碼方式,而UCS-4(指4個位元組)與UTF-32(指32個位元)除了名稱不一樣以外,內容是一樣的。
UTF-32分析
UTF-32編碼方式雖然最直覺,可能也最有效率,但是實在太浪費儲存空間了!尤其是對於絕大多數的常用字來說,那一大堆的00簡直就是不環保的最佳表現。現在也許還好,但是在1990年代的記憶體和相關儲存設備的水準來看,使用UTF-32怎樣都不像是個好辦法。
UTF-32還有另外一個缺點,就是寫軟體的人需要改用UTF-32這種一字元佔用4位元組的角度重寫所有的程式,如果直接把UTF-32格式的字串送給傳統以ASCII為基底的程式則會發生文字解析錯誤。舉例來說,以C語言類程式語言寫成的程式是以字碼00標示一個字串的結束,當這種程式讀取到UTF-32的一大堆00時,字串就會立刻被截斷(偏偏多數的基礎程式、驅動程式等很大比例是以C或相似語言寫成)。
總結來說,UTF-32的優點為簡單、直覺、高效;缺點為大量消耗記憶體等儲存設備之資源,並且相關程式需要重新改寫。
UCS-2編碼細節
由於一般應用中絕大多數的文字都出現在基本多文種平面,因此忽視其他輔助平面的存在成為一種折衷方案。這種方案與UCS-4類似,但採用16位元(2個位元組)整數來儲存Unicode字碼。UCS-2同樣也被細分為BE和LE版本。相關轉換範例請見下表:
文字 |
Unicode編號 |
編碼結果(BE) |
編碼結果(LE) |
A |
41 |
00 41 |
41 00 |
Ü |
DC |
00 DC |
DC 00 |
⿂ |
2FC2 |
2F C2 |
C2 2F |
𡾞 |
21F9E |
不支援 |
不支援 |
表5:UCS-2編碼範例
UCS-2分析
UCS-2收納了全世界絕大多數的常用文字,擁有同UTF-32一樣的簡單、高效等特性,而資料空間的需求卻只有UTF-32的一半,可以說是一個非常完美的編碼方式—如果輔助平面真的不存在的話。而UCS-2編碼的缺點很明顯,不支援輔助平面的文字,與當年的五大碼有些類似。只不過UCS-2就算只靠16位元編碼也可以囊括世界上6萬多個文字,缺點相較於五大碼來說較不明顯,世界上的許多人可能一輩子也碰不到基本多文種平面以外的文字;可惜說中文用中文的華人大概是全世界最容易碰到意外的一群人。
由於Windows系統從1993年的Windows NT 3.1開始採用UCS-2做為核心的字碼系統,一直到2000年的Windows 2000才改用UTF-16,況且當時很多人根本就不認識UTF-16,以為它和原來的UCS-2是一樣的東西(事實上兩者真的難以分辨),因此才讓許多的程式設計人員(特別是學校老師)存有Unicode就是16位元編碼系統的誤解。
總結來說,UCS-2的優點為簡單高效、資源使用經濟;缺點為不支援罕用字、相關程式也需要重新改寫(同UTF-32)。而由於Windows的關係,此種編碼常造成人們對於Unicode的誤解。
UTF-16編碼細節
UTF-16編碼方式在1996年公佈,這種編碼方式與UCS-2基本相同,但是增加了對輔助平面的支援。Unicode在D800到DFFF之間的碼位就是特別空下來給UTF-16的代理對(Surrogate Pairs)使用。
UTF-16在號碼FFFF以下的字碼儲存方式與UCS-2完全相同,而在10000以上的字碼將被拆分為兩個介於D800到DFFF之間的16位元整數。其中第一個整數被稱為「前導代理」(Leading Surrogate),值域介於D800至DBFF間,而第二個整數被稱為「後尾代理」(Trail Surrogate),值域介於DC00至DFFF間。因為代理對所在的值域沒有任何的Unicode字元對應,也就是說在UTF-16文字資料裡代理對一定是成對出現,共同表達一個文字;如果資料中間莫名其妙跑出單一一個代理,那一定是發生了資料錯誤的情事。
代理對的拆分過程請見下表描述:
流程 |
動作描述 |
示意 |
|
1 |
一個介於10000至10FFFF間的字碼。 |
XXXXXX |
|
2 |
將字碼減去10000,成為一個介於0至FFFFF間的整數。 |
XXXXX |
|
3 |
這個整數可以用20個位元來表示 |
xxxxxxxxxxxxxxxxxxxx |
|
4 |
將此整數以前後10個位元拆成兩部份。 |
xxxxxxxxxx |
xxxxxxxxxx |
5 |
將兩筆資料的前端各自嵌上所屬的代理對記號,即成兩個各16位元的整數。 |
110110xxxxxxxxxx |
110111xxxxxxxxxx |
6 |
得到結果 |
XXXX (前導代理) |
XXXX (末尾代理) |
表6:UTF-16代理對編碼示意
流程 |
動作描述 |
範例 |
|
1 |
一個介於10000至10FFFF間的字碼。 |
21F9E (𡾞) |
|
2 |
將字碼減去10000,成為一個介於0至FFFFF間的整數。 |
11F9E |
|
3 |
這個整數可以用20個位元來表示。 |
00010001111110011110 |
|
4 |
將此整數以前後10個位元拆成兩部份。 |
0001000111 |
1110011110 |
5 |
將兩筆資料的前端各自嵌上所屬的代理對記號,即成兩個各16位元的整數。 |
1101100001000111 |
1101111110011110 |
6 |
得到結果。 |
D847 (前導代理) |
DF9E (末尾代理) |
表7:UTF-16代理對編碼範例
文字 |
Unicode編號 |
編碼結果(BE) |
編碼結果(LE) |
A |
41 |
00 41 |
41 00 |
Ü |
DC |
00 DC |
DC 00 |
⿂ |
2FC2 |
2F C2 |
C2 2F |
𡾞 |
21F9E |
D8 47 DF 9E |
47 D8 9E DF |
表8:UTF-16編碼範例
UTF-16分析
UTF-16可算是在UCS-2上增加對輔助平面支援的結果,使其可以支援所有的Unicode編碼,對於大部分的常用字使用2個位元組來表示,而遇到其他罕用文字時將使用4個位元組來表示。但是增加了代理機制的UTF-16不再像UCS-2那樣的單純,文字處理軟體不再能夠斬釘截鐵的認為一個字一定佔用兩個位元組來計算文字的數量、位置等,加上對於代理對的分析處理較為複雜,因此UTF-16不再具有如同UTF-32或UCS-2在計算處理效率上的優點。
既然UTF-16最麻煩的地方在於代理對,那如果把代理對的機制拿掉如何?基本上拿掉代理對的UTF-16就是UCS-2了!這件看似有點蠢的事情為什麼會被我提起呢?我們偶爾會看到有些軟體的作者搞不清楚狀況,軟體號稱支援UTF-16,但實際上卻懶得實做代理對的處理或根本就不知道代理對的存在(比方說誤解Unicode等於16位元編碼的人),使得軟體實際上支援的是UCS-2。然而人家到底用的是UTF-16還是UCS-2?說實在的也很難分辨,只有在你發現它不支援某些罕用字的時候才能分辨出來。
總結來說,UTF-16的優點是資料佔用空間不多不少,而且可以彈性變化以支援輔助平面,整體效能近似於UCS-2;缺點是編碼較為複雜,使用效率稍差一些,並且相關程式需要重新改寫(同UCS-2)。
UTF-8編碼細節
UTF-8是在1993年公佈的編碼方式,這是一種資料空間使用更加彈性的編碼方式,一個Unicode字碼在UTF-8編碼格式下可能會佔用1至4個位元組。傳統的ASCII碼位在UTF-8格式中只佔用一個位元組,並且最高位元為零,也就是說Unicode中0至7F的碼位在UTF-8格式下和傳統的ASCII一模一樣。而80以上字碼就需動用兩個以上的位元組來表示,其中第一個位元組最前面(二進位格式)會被填入N個1,最後再以一個0做為區隔,用來表示這個字將使用N個位元組來共同表達;而從第二個位元組開始的所有位元組最前面(二進位格式)一律填上10,以表達這些位元組為某個Unicode字碼的子位元組。除了上面說明的標示位元以外的其他位元就用來儲存Unicode的二進位字碼資料,各種位元組大小的UTF-8編碼及其位元資訊示意和碼位對應關係表列於下:
位元組數 |
字碼範圍 |
位元資料示意(二進位格式) |
1 |
0 ~ 7F |
0xxxxxxx |
2 |
80 ~ 7FF |
110xxxxx 10xxxxxx |
3 |
800 ~ FFFF |
1110xxxx 10xxxxxx 10xxxxxx |
4 |
10000 ~ 1FFFFF |
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
5 |
200000 ~ 3FFFFFF |
111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx |
6 |
4000000 ~ 7FFFFFFF |
1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx |
表9:UTF-8使用之位元組數整理
從上表可以看到,理論上UTF-8的一的字元最多可使用6個位元組,最高碼位達到7FFFFFFF,也就是一個32位元有號整數的最大值。但由於Unicode最高碼位僅達10FFFF,因此實際上有效的UTF-8字元最多僅使用4個位元組。
從上表可以看出對於ASCII字碼來說UTF-8和ASCII完全一模一樣;字碼號在80以上的許多歐洲字母佔用2至3個位元組;大部分的中、日、韓文字佔用3個位元組;而基本多文種平面以外的字碼全部佔用4個位元組。
下面來看看UTF-8的編碼範例:
流程 |
動作描述 |
資料表示 |
1 |
Unicode字碼號。 |
2FC2 (⿂) |
2 |
用二進位來表示。 |
0010111111000010 |
3 |
由於這個號碼介於800至FFFF間,查表可知需要使用3個位元組。 |
0010111111000010 1110xxxx 10xxxxxx 10xxxxxx |
4 |
將該字碼依3位元組的儲存空間分佈拆成3份。 |
0010 111111 000010 1110xxxx 10xxxxxx 10xxxxxx |
5 |
合併二進位資料。 |
11100010 10111111 10000010 |
6 |
得到結果。 |
E2 BF 82 |
表10:UTF-8編碼過程範例
文字 |
Unicode編號 |
編碼結果 |
A |
41 |
41 |
Ü |
DC |
C3 9C |
⿂ |
2FC2 |
E2 BF 82 |
𡾞 |
21F9E |
F0 A1 BE 9E |
表11:UTF-8編碼範例
UTF-8分析
從上表範例可以看出,由於UTF-8以8位元整數做為編碼單位,因此不存在像UTF-32、UTF-16等編碼的位元組順序問題;也因為同樣的原因,把UTF-8字串送給傳統程式也不會發生字串被中途截斷等問題。在7F碼以下與ASCII一模一樣,加上在多位元組字中所有位元組的最高位元一定被設為1的特性,避開了任何有效的ASCII碼位,因此不會在不支援UTF-8的程式裡發生像五大碼那樣的次碼衝突問題。
由於UTF-8種種的特性與傳統ASCII(包含基於ASCII的多位元組擴充字碼表)極為相似,因此程式人員幾乎不需要轉換從前所熟知的字串慨念,程式人員甚至可以把UTF-8當成傳統編碼中的某一國多位元組編碼系統來理解與使用。幾乎不需要或極少需要修改既有的程式碼(專門顯示或編輯文字的程式除外),就可以在新時代的Unicode世界裡生存。甚至對於既存的文字檔案,如果他們原來是純ASCII格式檔案的話,那更是完全不用修改就可以直接「升級」被視為Unicode檔案。綜和這些特性成為UTF-8最為人所讚賞的優點,甚至讓UTF-8的幾種缺點顯得微不足道。
UTF-8對於資料空間的使用看似更有彈性,然而實際上未必更加節省空間。在說英語用英語的世界裡,UTF-8在絕大部分的狀況下所使用的資料空間只有UTF-16的一半;但是在東亞地區,使用中、日、韓等語言的國家中,一個字元通常要佔去3個位元組,通常會比UTF-16還佔用空間。
和UTF-16類似,由於一個文字所佔用的位元組數不固定,相關處理軟體不能快速的計算文字的資料大小與位置。也由於UTF-8在2個位元組以上的編碼中需要使用更多的資料計算與處理程序,因此在計算效率上比UTF-16更為不足(相比於UTF-16要遇到輔助平面的文字才需要特別處理,UTF-8需要資料計算與分離合併的場合和機率都大得多)。
總結來說,UTF-8的最大優點在於其與傳統編碼方式極為相似的特性,使得舊有程式、檔案、或習慣等幾乎不需更動就能沿用;缺點就是編碼相比於其他的Unicode編碼更為複雜、計算處理效率不如單純的編碼方式優良。
Byte Order Mark (BOM)
由於Unicode被使用的實際編碼方式就有三大種:UTF-32(UCS-4)、UTF-16(UCS-2)、和UTF-8 (註:由於UTF-16和UCS-2極相似又難以區分,因此在這裡合併叫做UTF-16類),加上位元組序的變化更形成了五大編碼方式,那麼當我收到一個文字檔案時,我(意指電腦)要如何區分這個文件是屬於哪種編碼呢?
由於三大編碼的特色鮮明,尚可區分,但是對於位元組順序就沒有這麼容易了!為了這個緣故,Unicode定義了一個字碼號為FEFF的特別空白(前面有提過),特別設計用來區分文字編碼的位元組順序。這個字碼用不同的編碼方式會得到不同的二進位資料,而編出來的資料就叫做「位元組順序記號」(BOM),請見下表:
編碼方式 |
字碼號 |
編碼結果 |
UTF-32BE |
FEFF |
FE FF 00 00 |
UTF-32LE |
FEFF |
00 00 FF FE |
UTF-16BE |
FEFF |
FE FF |
UTF-16LE |
FEFF |
FF FE |
UTF-8 |
FEFF |
EF BB BF |
表12:Byte Order Mark (BOM)
如此,一段文字資料或檔案只要在最前面加上這個記號,就可以標示後面這段資料所使用的位元組順序,而實際上連編碼方法都可以被區分出來。
萬惡的UTF-8 BOM
有些讀者會發現一件事:既然UTF-8不存在位元組順序的問題,那麼UTF-8還需要BOM記號嗎?對於這個問題,世界上分成支持UTF-8不加BOM與加BOM的兩派,各有其說法。
不支持UTF-8加BOM的人認為:
-
UTF-8本來就沒有位元組序的問題。
-
雖然UTF-8和傳統的各國編碼文字長相相似,但由於UTF-8的編碼特徵鮮明,對於一份文字資料其實可以很快的分析出它是不是屬於UTF-8格式的資料。
-
對於號稱傳統純ASCII文字檔案完全不需更改就可以被視為Unicode檔案的UTF-8,如果強制所有文字檔案都要加上BOM記號,不就破壞了它的一項大優點。
支持UTF-8加BOM的人認為:
-
即使UTF-8沒有位元組序的問題,它也可以用來標示編碼方式,何況UTF-8和傳統的各國編碼長的這麼像。
-
雖然可以快速分析一段資料是否為UTF-8格式,但是再快有快過直接查詢資料頭標記嗎?
由於雙方的理由都非常有道理,又各自擁有相當的支持者,因此UTF-8又分為帶BOM與不帶BOM的版本。至此,世界上常見的Unicode編碼變成六大編碼方式(同樣這裡把UTF-16和UCS-2合併計算):
-
UTF-32BE
-
UTF-32LE
-
UTF-16BE
-
UTF-16LE
-
UTF-8 No BOM
-
UTF-8 with BOM
然而UTF-8的BOM也產生了許多的問題(至於UTF-32和UTF-16反正橫豎都要加BOM,所以反而沒有問題):
-
有些程式總是希望UTF-8格式的檔案要加BOM,如果沒有則會將該檔案視為傳統編碼,這部份以微軟Windows體系的軟體為大宗。
-
又有些程式總是認為UTF-8格式的檔案不應該加BOM,如果你加了BOM,則這份文件在被解析時有很大的機率會出錯(因為檔首3個字元不是ASCII字元,加上程式又不能認識以及處理這3字元),這部份以自由世界的軟體、網頁分析軟體、以及部份根本就不認識UTF-8的舊軟體為大宗。
參考資料:
-
大五碼、倚天中文 – 小木偶的網頁
-
國內中文字碼之發展 – 交大資科BBS
-
王安電腦的興衰 – Jserv's blog
-
「許功蓋」的問題 – 網管很忙
http://203.64.20.7/lifetype126/index.php?op=ViewArticle&articleId=15&blogId=1
-
中文標準交換碼全字庫
-
Unicode編碼表
-
淺釋Unicode – novus
http://novus.pixnet.net/blog/post/30371481-%E6%B7%BA%E9%87%8B-unicode
-
談程式碼使用的字集 – novus
-
每個軟體開發者都絕對一定要會的Unicode及字元集必備知識 – 周思博
-
Unicode是用幾個位元來進行編碼? – LSS實驗室Beta
-
非常經典的UTF-8 – Gea-Suan Lin's BLOG
http://blog.gslin.org/archives/2013/10/01/3670/%E9%9D%9E%E5%B8%B8%E7%B6%93%E5%85%B8%E7%9A%84-utf-8/
-
UTF-8 Everywhere
-
認識中文字元碼 – 中研院計算中心 曾士熊
-
Wikipedia
http://zh.wikipedia.org/wiki/%E5%A4%A7%E4%BA%94%E7%A2%BC
http://zh.wikipedia.org/wiki/Unicode
http://zh.wikipedia.org/wiki/%E8%BC%94%E5%8A%A9%E5%B9%B3%E9%9D%A2
http://zh.wikipedia.org/wiki/UTF-32
http://zh.wikipedia.org/wiki/UTF-16