
※ [本文轉錄自 anndy 信箱]
作者: anndy.bbs@bbs.kmsh.tnc.edu.tw ("太陽下的水晶碑")
標題: [doc] 每個軟體開發者都絕對一定要會的Unicode及字元集必備知識
時間: Mon Jul 10 18:13:22 2006
作者: anndy.bbs@bbs.sayya.org (I love Gentoo)
標題: [doc] 每個軟體開發者都絕對一定要會的Unicode及字元集必備知識
時間: 2006/07/09 Sun 23:20:20
作者: jserv (松鼠) 站內: jserv
標題: [doc] 每個軟體開發者都絕對一定要會的Unicode及字元集必備知識
時間: Sat Jul 8 08:58:48 2006
作者: luke1209 (Luke) 看板: UranusSky
標題: [轉錄] 每個軟體開發者都絕對一定要會的Unicode及字元集必備知識
時間: Thu Jun 22 20:59:19 2006
每個軟體開發者都絕對一定要會的 Unicode 及字元集必備知識 (沒有藉口!)
The Absolute Minimum Every Software Developer Absolutely, Positively
Must Know About Unicode and Character Sets (No Excuses!)
作者:周思博 (Joel Spolsky)
譯:Paul May 梅普華
Wednesday, October 08, 2003
屬於Joel on Software, http://www.joelonsoftware.com
轉錄來源 http://www.csie.ntu.edu.tw/~p92005/Joel/Unicode.html
-----------------------------------------------------------------------------
還搞不懂那個神秘的 Content-Type tag 嗎?你知道的,就是那個應該放在 HTML 裡卻
又永遠不知道該設成什麼內容的標籤啊。
你曾經收到在保加利亞的朋友寄來,主題是「???? ?????? ??? ????」的電子郵件嗎?
很多軟體開發者並未真正完全理解字元集、字元編碼、Unicode 等等的神秘世界,當我
發現不懂的人那麼多時真的很失望。數年以前,某位 beta 測試人員想知道 FogBUGZ
是否能處理日文的電子郵件?他們竟然用日文寫電郵?我完全不知道耶。我們用了一個
商用 ActiveX 控制元件來分析 MIME 電郵訊息,當我仔細調查這個元件時,才發現它對
字元集的處理完全錯誤,所以我們還寫了些了不起的程式,把錯誤的轉換還原後再重做
正確的轉換。我又去看看另一個商用程式庫,它的字元編碼實作也是完全不對。我聯絡
該軟體的開發者,結果他似乎有點認為沒辦法改善。他跟很多程式師一樣,只希望這個
問題能憑空消失。
不過問題並不會消失。PHP 是個很普遍的 web 開發工具,不過它完全忽略字元編碼問題
(註一),PHP 很愉快地用 8 位元來處理字元,因此幾乎不可能開發好的國際化 web 應
用程式。當我發現這件事時,覺得真是夠了。
所以我要做一個宣告:如果你在 2003 年還是個程式師,而你不知道字元、字元集、字
元編碼、以及 Unicode 的基本知識,我就要去抓你,我會讓你在潛艇裡關 6 個月剝洋
蔥。我發誓我一定會的。
另外還有一件事:
這並沒有那麼難。
我會在這篇文章中讓你確實瞭解每個現役程式師都應該知道的事情。所謂「純文字 =
ascii = 字元都是 8 個位元」的說法不僅不對,而且還錯得離譜;如果你還是照這個想
法寫程式,那麼你大概不會比不相信細菌的醫生好多少。在讀完這篇文章之前請暫時不
要寫程式。
在我開始之前應該先提醒一下,如果你是極少數瞭解多國語言軟體製作的人,會發現我
的討論有點過度簡化。我只是想設立一個底線,讓大家能瞭解這是怎麼一回事,而且寫
出的程式有希望能處理任何語言的文字,而不是只認得沒有重音符號的英文。另外我也
要提醒你,字元處理只佔建立多國語言軟體的一小部份,不過我一次只能寫一件事,所
以今天只談字元集。
歷史的觀點
要瞭解這些事,最簡單的方法就是按年代來看。
你或許會認為我會講些 EBCDIC 之類很古老的字元集。我不會,EBCDIC 跟你的生活無
關。我們並不用回逆到那麼前面。
回到沒那麼古老的從前,Unix 被發明而 K&R 正在寫 The C Programming Language 的
那個時代,當時每件事都非常簡單。EBCDIC 正在被淘汱。唯一重要的字元集就是古老
美好的無重音英文字母,我們有一個對應的編碼系統叫做 ASCII,可以用 32 到 127
的數字表示每一個字元。空白是 32,字母 A 是 65,如此類推。這種方法可以把文字
存成 7 個位元。當時大部份電腦的一個位元組都是 8 個位元,所以儲存全部 ASCII 字
元之後有很多個位元沒用到。如果你夠邪惡,就會偷用這些空位元:事實上 WordStar
的壞蛋就用把最高位元設起來,代表一個單字中的最後一個字母。這是開玩笑的。空的
位元被用來當控制字元,比如 7 會讓你的電腦發出嗶聲,而 12 會讓印表機把目前正
在印的紙張送出並且捲入一張新紙。
所以天下太平,不過只限於英語系的人。
由於位元組有 8 個位元的空間,所以很多人就開始想啦:「對了,我們可以把 128 到
255 的碼拿來自己用。」問題是很多人同時都有這個想法,所以 128 到 255 的空間該
怎麼用,大家都各自有自己的想法。IBM-PC 用了一種名為 OEM 字元集的東西,提供了
某些歐洲語言用的重音字母和一堆線條繪圖字元:水平線、垂直線、右邊有個小吊釣的
水平線等等。你可以用這些線條繪圖字元在螢幕上拼出很漂亮的方框和線條,在乾洗店
裡的 8088 電腦還可以看到這種圖案。事實上當 PC 開始賣到美國以外時,各種不同的
OEM 字元就被憑空創造出來,大家都把上面這 128 個字元拿來自己用。舉例來說,字
元碼 130 在某些 PC 上會顯示為é,不過在以色列賣的電腦上就變成希伯來文字母
Gimel,所以當美國人把履歷(résumé)寄到以色列就會變成r?sum?。 在很多情況下,比
如說俄文好了,本身對於上面 128 個字元(值>127)就有很多不同的想法,所以什麼連
俄文文件本身都不能可靠地轉換。
後來這段 OEM 亂用區終於在 ANSI 標準裡固定下來。在 ANSI 標準中,大家都同意小
於 128 的字元定義(基本上和 ASCII 一致),不過由 128 開始的字元就有很多不同的處
理方法,會依照你住的地方而定。這些不同的系統就叫做頁碼(code page) (註二)。舉
例來說以色列的 DOS 用叫 862 的頁碼,而希臘用戶則是用 737。它們在 128 以下是一
樣的,不過由 128 起就不同了,裡面充滿奇奇怪怪的字母。美國版本的 MS-DOS 有幾十
種頁碼,由英文到冰島文都可以處理,甚至還有一些「多語」頁碼可以在同一台電腦上
處理世界語和加利西亞語!了不起!不過要一台電腦同時處理希伯來文和希臘文是絕對
不可能的,除非你自己寫程式自己用圖顯示所有文字。因為希伯來文和希臘文對 128 以
上字元的解釋方法不同,必須用到不同的頁碼。
在同一時期亞洲發生的事情更誇張。由於亞洲的字母系統有幾千個字母,不可能用 8 個
位元表示。通常是用一種叫 DBCS 的麻煩系統來處理。DBCS 是雙位元組字元集 (Double
Byte Character Set),字元集中的某些字母是一個位元組來存,其他字則要用兩個位
元組。在 DBCS 的字串中要向後移到下一個字很容易,不過幾乎不可能往回移到前一個
字。程式師被指示向後及往回移時不能用 s++ 和 s--,而是呼叫 Windows 的 AnsiNext
和 AnsiPrev 之類的函數,只有這些函數才知道怎麼處理這些麻煩。
不過大多數人還是假裝一個位元組就是一個字元,而一個字元就是 8 個位元。只要不會
把字串在電腦間移動,或者只用一種語言,這種想法大致上還是能用。不過當 Internet
興起,在電腦間移動字串變成隨時都在做的事,整團麻煩自然就爆出來了。還好這時已
經發明了 Unicode。
Unicode
Unicode 是個勇敢的嘗試,想用單一個字元集去涵括地球上所有合理的書寫系統,另外
也要包括克林貢語等杜撰的語文。有些人誤認為 Unicode 只是個16位元碼,裡頭每個字
都要佔 16 位元,所以總共有 65,536 個字元。事實上這並不正確。這是關於 Unicode
常見的誤解,所以如果你也這麼認為的話,不用難過。
事實上 Unicode 對字元有不一樣的想法,你必須瞭解 Unicode 的想法,否則是搞不懂
的。
到目前為止,我們都假設一個字母會對映到某些位元,這些可以存在磁碟或記憶體中:
A -> 0100 0001
在Unicode裡一個字母是對映到一個叫 code point 的東西(還只是一個理論上的概念)。
要如何在記憶體或是磁碟上表示 code point 就完全是另一回事。
在 Unicode 中,字母 A 是個精神上的觀念。它只會漂浮在天堂裡:
A
這個觀念上的 A 和 B 或者 a 都不一樣,不過 A(正常體) 和 A(斜體) 以及 A(縮小)
都一樣。Times New Roman 字型的 A 和 Helvetica 字型的 A 是相同的字元,但和小
寫的 "a" 不一樣,這種想法似乎沒什麼好爭論的。不過在某些語言中,光是要決定一個
字母是什麼就有得吵了。舉例來說,德文字母β究竟真正的字母還是 ss (譯註:拉丁文
的 gei)的另一種特別寫法呢?如果字母的形狀在單字結束時會改變,改變之後要當作不
同的字母嗎?希伯來文說是,阿拉伯文卻認為不是。不管如何,Unicode 協會的聰明人
已經在過去十年左右搞定了,雖然有一大堆政治爭論伴隨而來,不過你不用擔心。他們
已經完全搞定了。
Unicode 協會把所有字母系統中每一個觀念上的字母都分配一個魔術數字,這個數字的
寫起來就像是:U+0645。這個魔術數字就叫一個 code point。U+ 的意思是 Unicode,
數字則是用十六進位表示。U+FEC9 就是阿拉拍文的字母 Ain。英文字母 A 則是
U+0041。你可以用 Windows 2000/XP 的 charmap 工具把這些數字全找出來,到
Unicode 網站(註三)也可以找到。
Unicode 可以定義的字母數量並沒有實質限制,事實上可以超過 65,536 個,所以並不
是所有的 Unicode 字母都能擠進兩個位元組裡,不過反正那本來就是個迷思。
好吧,假設我們有個字串:
Hello
用 Unicode 來表示的話,這個字串會對映到下面五個 code point:
U+0048 U+0065 U+006C U+006C U+006F
就只是一堆 code point。實際上也就是數字。不過我們還沒有提過要如何儲存到記憶體
或在電郵訊息中表示。
字元編碼
這就是字元編碼上場的地方。
Unicode 編碼最初的想法導致了兩個位元組的迷思,簡單說就是把那些數字都存成兩個
位元組。所以 Hello 變成
00 48 00 65 00 6C 00 6C 00 6F
這樣對嗎?等一下!也有可能會是:
48 00 65 00 6C 00 6C 00 6F 00 ?
好吧,技術上是的,我的確相信可以這樣寫,而事實上早期的實作者希望能把 Unicode
碼存成 high-endian 或 low-endian 模式,可以依據 CPU 用哪一種最快來決定。於是
就有兩種儲存 Unicode 的方法。所以人們被迫想出奇怪的作法,在每個 Unicode 字串
的開頭存一個 FE FF;稱之為 Unicode Byte Order Mark。如果你把高低位元組對調,
標記就會變成 FF FE,讀字串的人就知道其他位元組都要對調。不過外面的 Unicode 字
串開頭並不一定都會有這種位元組順序標記。
有一段時期這個方法好像還不錯,不過後來有程式師在抱怨了。他們說:「看看那些零
」,因為他們都是美國人,看到的都是很少用到 U+00FF 以上 code point 的英文文字
。何況他們還是注重保育(哼)又崇尚自由的加州嬉皮。如果他們是德州佬,才不會在意
要花掉兩倍的位元組呢(譯註:指德州人少地方大,所以財大氣粗)。不過那些加州糊塗
蛋受不了字串儲存空間會倍增的想法,而且外頭已經有太多文件是用各種ANSI和DBCS字
元集寫的,要找誰來轉換這些文件?新聞局嗎?光是這個理由,就讓大多數人就決定不
管 Unicode,幾年下來情況就變得愈來愈糟了。
然後就有人發明了 UTF-8 這個絕佳的點子。UTF-8 是另一個儲存系統,用 8 位元方式
把 Unicode code point (就是那些神秘的 U+ 數字) 存在記憶體中。在 UTF-8 中,由
0-127 的 code point 都存成一個位元組。只有 128 和更大的 code point 會存成 2
或 3 或個位元組,事實上最多可以用到 6 個位元組 (註四)。
這樣做有個很巧妙的副作用,就是英文文字用 UTF-8 和用 ASCII 會完全一樣,所以美
國人根本不會覺得有啥不對。只剩世界上其他地方的人得跳火圈。具體來說 Unicode為
U+0048 U+0065 U+006C U+006C U+006F 的 Hello 的,會被存成 48 65 6C 6C 6F。看
吧!這跟存成 ASCII 或 ANSI 或是地球上每一種 OEM 字元集的結果都一樣。這樣子一
來,如果你斗膽敢用重音字母或希臘字母或是克林貢字母,就得用多個位元組來儲存一
個 code point,只是美國人永遠不會發現。 (UTF-8 還有一個蠻好的特性。由於舊的字
串處理程式並不知道 Unicode,在處理字串時會用一個值為零的位元組作為字串結尾。
如果用 UTF-8 的話,這些舊程式不會中途截斷字串。)
到目前為止我說了三種 Unicode 編碼的方法。全部都存成兩個位元組的傳統作法叫做
UCS-2 (因為用兩個位元組) 或是 UTF-16 (因為有16位元),不過你還是得分辨是
high-endian UCS-2 還是 low-endian UCS-2。再來是普遍使用的新 UTF-8 標準,這個
標準有良好的特性,在用只認識 ASCII 的舊程式處理用英文文字還是一切正常。
Unicode 其實還有其他多種編譯方式。其中之一叫 UTF-7,非常像 UTF-8 不過保證最
高位元一定是零,所以即使經過某個認為 7 位元很足夠的嚴苛警察國家電郵系統,還
是能亳髮無傷地通過。另外也有每個 code point 都存成 4 個位元組的 UCS-4,好處
是每個 code point 都容量都一樣,不過連德州佬都不敢浪費那麼多記憶體。
實際上現在你正在以概念性的字母 (表示成 Unicode code point) 來思考事情,這些
Unicode code point 也可以用任何老式的編碼方法來編碼!舉例來說,你可以把
Unicode 字串 Hello (U+0048 U+0065 U+006C U+006C U+006F) 編碼成 ASCII 或舊的
OEM 希臘編碼,也可以編成希伯來 ANSI 編碼或是到目前為止已發明的數百種編碼方
式,不過有一個陷阱:某些字母可能會畫不出來!如果某個 Unicode code point 在你
所用的編碼方式中沒有對應的字元,通常就會看到一個小問號?或是一個小方框。在箭
頭後面你看到的是什麼呢?-> 嚙 (譯註:這個字是十六進位 EFBF,用 UTF-8 好像是
沒有字,在大五碼裡就是「嚙」,因為譯文用 big5 編碼,所以看到的是「嚙」)?
多達數百種的傳統編碼方式都只能正確儲存部份的 code point,而其他 code point
則是全部變成問號。常見的英文文字編碼有 Windows-1252 (Windows 9x 對西歐語言的
標準) 和 ISO-8859-1 又名 Latin-1 (也是用於西歐語言),不過想要用這些編碼方式
儲存俄文或希伯來文時就會得到一大堆的問號。而 UTF 7, 8, 16 和 32通通都能正確
的儲存任何一個 code point。
關於字元編碼最重要的一個事實
如果你完全不記得我剛說的東西,請至少記住一件超級重要的事實。光有字串卻不知道
編碼方式是不行的。你不能再把頭埋在沙裡假裝「純」文字就是ASCII。
根本就沒有純文字這種東西。
假設你有一個字串,不管是在記憶體或在檔案還是在電郵訊息裡,你都必須知道字串用
的編碼方式,才能正確解譯出來並呈現給使用者。
「我的網站都是亂碼」或「她看不到我用重音符號寫的電郵」之類的笨問題,幾乎全部
都是因為某位天真的程式師不瞭解一個單純的事實:如果不知道某個字串的編碼方式是
UTF-8 還是 ASCII 還是 ISO 8859-1 (Latin 1) 還是 Windows 1252 (西歐),根本不可
能正確顯示出來,甚至連在哪結束可能都找不到。大於 127 的 code point 有上百種
編碼方式,連猜都猜不到。
我們要如何保存某個字串的編碼資訊呢?好吧,是有一些標準方法可以用。以電子郵件
來說,郵件表頭應該會有一個字串:
Content-Type: text/plain; charset="UTF-8"
如果是網頁的話,最原始的想法是在網頁之外,再讓 web 伺服器傳回一個類似的
Content-Type http header。不是放在 HTML 裡面,而是在傳 HTML 網頁之前先送的
header。
這樣做會有問題。假設你有一個很大的 web 伺服器,很多使用各種語言的人在裡面放了
很多網站和網頁,所有網頁的編碼方式都是由微軟 FrontPage 自動產生。Web 伺服器本
身其實並不知道各個檔案的編碼方式,所以也沒法子傳出正確的 Content-Type header。
利用某些特別的 tag 把 HTML 檔案的 Content-Type 放在 HTML 檔案裡比較方便。當然
這會讓純粹主義者抓狂...你怎麼能在不知道編碼方式之前讀 HTML 檔案呢!?幸運的是
,幾乎所有編碼方式由 32 到 127 的字元都是一樣的,所以不需用到怪字母就能在
HTML 網頁取到這些資訊:
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
不過這個 meta tag 一定得放在 <head> 段落非常前面的地方。因為網頁瀏覽器一看到
這個 tag 就會停止分析,然後改用你指定的編碼方式重新解譯整個網頁。
如果瀏覽器在 http header 或 meta tag 都找不到 Content-Type 時會怎麼做呢?
Internet Explorer 會做一件很有趣的事:它會依據各位元組在各種常見語言編碼中出
現的頻率,猜測網頁所用的語言及編碼方式。由於各種舊的 8 位元頁碼通常把該國的
字母放在 128 到 255 範圍內不同的位置,而各種人類語言的字母使用頻率都有不同的
分佈特性,所以這種做法的確有機會成功。這種做法真的很奇怪,不過似乎的確很有效
。效果好到那些天真到不知道要用 Content-Type header 的網頁製作者根本不知道自己
錯了,因為他們的網頁用瀏覽器來看時一切正常。等到某一天,當他們寫的內容不符合
所用語言的字母頻率分佈時,Internet Explorer 就會把它認成韓文來顯示。我認為這
也證明 Postel's Law 中關於「發送時嚴謹,接收時寬鬆」的論點實在不是一個良好的
工程原則。不管如何,當遇到這個用保加利亞文寫卻顯示成韓文 (還不是有意義的韓文)
的網頁時,可憐的讀者要怎麼辦呢?他會用由選單選 檢視|編碼,然後嘗試各種不同的
編碼 (裡面有十幾種東歐語言) 直到看起來對為止。不過當然是要他會這招才行,不過
大多數人都不會。
我們公司有出一套網站管理軟體 CityDesk,從上一版起我們決定內部全部使用 UCS-2
(2 個位元組) 的 Unicode,它也是 Visual Basic、COM、以及 Windows NT/2000/XP 的
標準字串型別。寫 C++ 程式時只要在字串宣告時用 wchar_t ("wide char") 代替char
,再用 wcs 函數代替 str 函數 (比如用 wcscat 和 wcslen 代替 strcat 和 strlen)
即可。要在 C 程式裡建立一個 UCS-2 字串常數,只要在字串前面加個 L 就好了,就是
這樣:L"Hello".
當 CityDesk 發行網頁時會把網頁轉成多年來廣受瀏覽器支援的 UTF-8 編碼。這也是
Joel on Software 上29種語言版本編輯的方式,而且還沒有人跟我抱怨過有問題。
這篇文章寫到這裡已經很長了,反正我也不可能寫完所有關於字元編碼和 Unicode 的事
情。不過我想你既然都讀到這裡了,應該也學夠了可以回去寫程式,這次別再用水螅和
咒語了,改用現代的抗生素吧。這就是我留給你的工作。
註一:http://ca3.php.net/manual/en/language.types.string.php
註二:http://www.i18nguy.com/unicode/codepages.html#msftdos
註三:http://www.unicode.org/
註四:http://www.joelonsoftware.com/pictures/unicode/utf8.png
--
※ 發信站: 批踢踢兔(ptt2.cc)
◆ From: 203.73.163.110
◆ Modify: 06/07/08 8:59:48 <info.sayya.org>
--
※ 發信站: 批踢踢實業坊(ptt.cc)
◆ From: 220.134.30.220
※ 編輯: anndy 來自: 220.134.30.220 (07/11 07:38)
