有了地方應該要放一點東西,所以就來嘗試把一些很早之前就想寫想整理的東西來寫出來好了。打頭陣的這個文字編碼的題材是很久以前就已經開始寫的 (有紀錄的草稿是七年前開始的),雖然還不確定要怎麼分篇但就慢慢整理吧。
前導閱讀
要談文字編碼,有一個前輩的文章必須先拿出來供著:
Joel on Software (周思博趣談軟體)-每個軟體開發者都絕對一定要會的 Unicode 及字元集必備知識 (沒有藉口!)
英文原文版在這裡:The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!)
這篇文章是 2003 年寫成的了,但當中有很多觀念是現在不少程式設計師仍然沒有的。我很喜歡周思博開場的這段:
所以我要做一個宣告:如果你在2003年還是個程式師,而你不知道字元、字元集、字元編碼、以及Unicode的基本知識,我就要去抓你,我會讓你在潛艇裡關6個月剝洋蔥。我發誓我一定會的。
我不像他那麼……偏激(?),是還不致於講出這種重話啦,但有的時候我看一些其他人回關於字元編碼的東西觀念錯誤百出就實在很想翻白眼。(嘆氣)
接下來我也會基於這篇文章,來延伸講一些他沒有提到的部份。
DBCS 的「範圍」
就從一篇我在 2007 年在 PTT 上回過的文章開始好了:Re: [問題] 關於解碼的問題
OK,我承認引這篇確實有一部份是來現的 XD (幾個朋友後來常拿這篇文章來虧我) 不過我想提的其實是我在這篇文章裡的判斷基準:
(其實看到這種五位數就在想會不會是雙字元組編碼
不過看到有小於32768的本來以為不是
後來突然想到unicode 就抓來試一下馬上試出來了)
為什麼是 32768?為什麼五位數就很有可能是雙字元編碼?為什麼小於 32768 會很有可能不是?又是為什麼會突然想到 Unicode?這都跟 DBCS 編碼 (或者講更廣一點,多位元組編碼) 是如何編定字元的編碼有關。
周思博在他的文章裡其實已經簡單帶過什麼是 DBCS 編碼了:
由於亞洲的字母系統有幾千個字母,不可能用8個位元表示。通常是用一種叫DBCS的麻煩系統來處理。DBCS是雙位元組字元集(Double Byte Character Set),字元集中的某些字母是一個位元組來存,其他字則要用兩個位元組。
先不提別的,教育部公佈的常用國字標準字體表裡面,屬於最常用的第一張表裡的中文字就有 4808 個,code page 的高位範圍 128 個字是表示不完的,因此才會需要兩個位元組來表示。
於是我們有了一套混合的系統:英文字沿用原本 code page 的定義使用一個位元組,中文字則是定義哪兩個位元組是什麼字。問題來了,現在一篇可能是中英混合的文章,要怎麼分辨當中任意兩個位元組到底是兩個英文字還是一個中文字?
舉個例:1假設有一個中文字的編碼是十六進位 0x4141 (十進位 16705) 好了。它表示成兩個位元組時會是兩個十六進位為 0x41 的位元組,而 0x41 (十進位 65) 對應的 ASCII 是 'A'
字。那麼,當我們看到連續兩個位元組都是 0x41 時,究竟要當做 "AA"
還是要當做 0x4141 對應的中文字?
既然直接編碼無法避免和英文衝碼,只好把腦筋動到 code page 裡給大家自訂的高位範圍,規定:只要看到一個位元組在這個範圍,就把它和下一個位元組合起來表示一個字。
因此,DBCS 編碼一個字的第一個位元組的值會大於 0x80 (十進位 128),所以如果把連續兩個位元組當做一個數字的話,這個數字就會大於 0x8000 (十進位 32768)。這就是為什麼這種編碼的數字都會大於 32768 的原因。而上面提到的這個「和下一個位元組合起來」這回事,也是為什麼周思博提到我們無法單純用 ++ 或 — 在字與字之間移動--這部份等之後談到衝碼問題時會再詳細解釋。
EUC 編碼 (Extended Unix Code)
把腦筋動到高位的其實並不只有大五碼,還有一系列被稱做 EUC 的編碼,它是基於 ISO/IEC 2022 標準制定而來的。ISO/IEC 2022 裡將位元組分為以下的四個部份:
- 低階控制字元(C0),包含 0x00 ~ 0x1F (0 ~ 31) 及 0x7F (127);
- US-ASCII字元集(GL),包含 0x20 ~ 0x7E (32 ~ 126),使用 G0 字集,也就是普通的英文字母符號;
- 高階控制字元(C1),是 C0 字元集的第 8 位元設為 1,即包含 0x80 ~ 0x9F (128 ~ 159) 及 0xFF (255);
- 高階字元(GR),是 GL 字元集的第 8 位元設為 1,即包含 0xA0 ~ 0xFE (160 ~ 254),使用 G1、G2、G3 的其中一套字集,由特定 C1 控制字元表示接下來是哪一套。
EUC 編碼則是將這個方法進行延伸,規定兩個位元組都在 GR 字元範圍時就用這兩個位元組當做座標來決定表示什麼字。GR 範圍扣掉因為和空白 0x20 對應而有一點特殊意義的 0xA02,一共有 94 個位元組是實際有「字」的位置,因此 EUC 就將定碼空間定為 94「區」乘以 94「位」,一共 8836 個字碼空間。這便是各種 EUC 編碼據以編定字碼的空間。
大五碼在定義當初,一共預計要收入約一萬三千多個中文字,這個量是不夠放入 EUC 編碼的空間的。以下僅僅是我的猜測,但我想也許是因為這個原因,大五碼決定將每一個「區」當中「位」的空間藉由納入 0x40 ~ 0x7E (64 ~ 126) 增加到 157 個,使得全部能夠定碼的空間擴大到 94×157 = 14758 個字;會刻意避開 C1 控制字元而去用到英文字母的空間,我認為則是大五碼當初制定時有參考 EUC 編碼 (甚至可能是更背後的 ISO/IEC 2022) 的狀況證據。只是延伸到英文字母區也造成了一定程度的相容性問題,例如上面只有簡單提到的衝碼問題,這就之後再談了。
隨著時代演進,C1 控制碼的意義逐漸消失3,其他的 DBCS 編碼以及已有編碼的擴充就會把位元組擴充到包含 C1 控制碼的範圍。我所知的例子包括:
- 現在被叫做「大五碼」的 CP950 字碼頁已經把第一位元組擴充到包含 C1 控制碼,由 0x81~0xA0 都在範圍內,只不過沒有新字而是將其定義成造字區而已。
- 簡體中文的 GBK 編碼 (擴展自 GB2312 這個又稱 EUC-CN 的編碼) 在保留原有 EUC-CN 的字碼定義外,第一位元組同樣擴充到了 C1 控制字元,第二位元組則還延伸到英文字母區,因此總計有 126×191=24066 個字的空間能夠定字。
- 日文的 Shift-JIS 則是一個相對特別的狀況:由於它保留了已經廣為使用的 JIS X 0201 標準當中 0xA1~0xDF 做為半形片假名的位置,第一個位元組僅使用 0xE0~0xFE 是不足以放下預計要收錄的 JIS X 0208 的漢字的,因此它的第一位元組自然地必須使用到 C1 控制字元空間。而就算這樣使用了,首位元只有 62 個位置,第二位元組只使用 GR 也還是不夠,所以也延伸到了英文字母區和 C1 控制字元區,總計有 190 個字的空間;這樣的一組 190 個字正好放得下 JIS X 0208 字碼的兩個區 (94×2=188),因此這就是 Shift-JIS 最終的定義方式:第一位元組使用 47 個字 (0x81~0x9F 及 0xE0~0xEF),每一個起始位元組對應 JIS X 0208 的兩個區,第一區放在第二位元組 0x40~0x9E (扣除 0x7F),第二區放在 0x9F~0xFC。
字碼分佈
回到開頭引的那篇我的文章,其實我在判斷大五碼時還有另一個判準在:大五碼當中常用字的編碼在 0xA440 到 0xC67E 之間。這兩個數字的十進位分別是 42048 和 50814,不過我當時看到這篇文章時記得的這個範圍只有「四萬多」而已;但這已經足夠懷疑這八個數裡有七個是 4 字頭的數其實是以大五碼表示的中文字了。
這其實是在猜測某個文字串是否為某種編碼時的常見手段。繼續引用周思博的文章:
如果瀏覽器在http header或meta tag都找不到Content-Type時會怎麼做呢?Internet Explorer會做一件很有趣的事:它會依據各位元組在各種常見語言編碼中出現的頻率,猜測網頁所用的語言及編碼方式。由於各種舊的8位元頁碼通常把該國的字母放在128到255範圍內不同的位置,而各種人類語言的字母使用頻率都有不同的分佈特性,所以這種做法的確有機會成功。這種做法真的很奇怪,不過似乎的確很有效。
追根究柢,這和英文的置換字謎裡多半會假設 E 是出現最多次的字母的核心概念是一模一樣的;一篇英文的文章,其位元組分佈裡面 0x65 (小寫字母 e) 的出現頻率一定很高。只不過英文因為字母少重覆多,因此著重在一個字的出現次數;而雙位元組編碼因為字數多,大部份字的出現頻率不是 0 就是 1,所以重點會比較放在出現了哪個範圍裡的字 (或者反過來說,這之外的字都沒出現)。
這個概念不僅 8 位元頁碼適用,DBCS 編碼也適用,甚至連 Unicode 在某種程度上也適用--這即是我在那篇文章裡會「突然想到 Unicode」的原因了:因為 Unicode 的常用漢字範圍是 U+4E00 (19968) 到 U+9FFF (40959),也就是大多都是兩三萬的數,正好對上文章一開始他的「編碼後」的那些兩三萬的數。雖然只有八個參考點,但是兩邊都有合理懷疑是中文字編碼的可能,自然就會懷疑所問的「編碼」實際上在做的其實是轉碼了。當年的我因此直接叫起 Mathematica (我推文裡所說的工具就是它),利用內建的數字轉編碼功能輸入這些數字和懷疑的編碼,就發現它們變成了完全一樣的八個中文字--然後就成了那篇文章了。
小結
第一篇文章就先寫到這裡吧。這篇簡單介紹了所謂 DBCS 編碼,並且點到了好些我之後會打算稍微深入聊聊的題材。接下來預計應該會是一個星期一篇吧?
註腳
- 這是我國小時在一本介紹倚天系統的書上看到過的例子,書名早已忘記,所以就簡單記下這個註腳權充引用來源。認真說起來,這本書裡對中文內碼的介紹一定程度上引發了還是國小的我去研究電腦文字編碼的興趣;小六時的我已經是個會拿大五碼常用字的字碼表來「玩」的小鬼了 XD ↩︎
- 通常也是代表某種有排版意義的空白,例如不斷行空白--這個意義後來給 Unicode 繼承了,所以大家在 HTML 裡打的 輸入的不斷行空白其 Unicode 就是 U+00A0。 ↩︎
- 我個人認為這件事其實發生的比想像中還早,可能是由於 IBM PC 的字碼頁在這些位置定義字開始的;由於 IBM PC 的普及,已經定義成字的範圍如果還有控制碼意義在處理上就會更麻煩。 ↩︎