亂碼 1/2(上)頻遭模糊的字元集與編碼基礎


iThome 網站首載:亂碼 1/2(上)頻遭模糊的字元集與編碼基礎

令人訝異地,多數開發者從未真正理解字元集及編碼,甚至有些中高階開發者,對字元集與編碼仍處在一種模糊、忽略或逃避狀態。字元集與編碼的歷史發展錯綜複雜,要瞭解程式如何處理字元編碼確實是很大的議題,然而現代開發者仍需具備常見字元集與編碼知識基礎,面對往後更複雜的編碼議題,才有解決問題的脈絡可循。

字元集與字元編碼

文字是人類溝通、書寫符號,字元是電腦中處理的符號資料,文字是可見的,但字元可能無法顯示。每個國家、地區甚至每段歷史使用的文字符號都不同,也隨時在增加,為了讓電腦可以有限方式處理字元,必須事先規範哪些集合包括哪些字元,也就是定義字元集(Character set)。例如開發者都知曉的ASCII字元集,其中規範了128個字元,包括95個可見字元與33個無法顯示字元。字元集除了規範收納的字元,每個收納的字元會分配編號,稱為字碼(Character code),字碼常用十進位或十六進位表示。例如ASCII中字元A到Z的字碼為十進位65到90(十六進位41到5A)。

電腦使用者普遍存在的錯誤認知是,檔案有所謂二進位檔案與純文字檔案。實際上所有的檔案,都是用二進位方式儲存,並沒有純文字或二進位檔案的區別。所謂純文字檔案,是指以二進位來儲存對應字碼的檔案,也就是字元編碼(Character encoding)。例如電腦會使用單位元組儲存ASCII字元,像是使用01000001來儲存A字元,剛好是十進位65的二進位表示。如果電腦讀到ASCII編碼檔案中有位元組是01000001,就知道是A字元,並使用適當字型繪出文字外觀。

單位元組實際上可表現256個字元,ASCII字元只會用到七個位元,於是有人把第八個位元拿來作為私用,像是字碼128到255拿來表現英語系以外的歐洲文字,然而多出的字碼空間並不足以表現所有文字,造成了使用到第八位元的同一位元組,在採某編碼的電腦顯示某字元,而採另一編碼的電腦顯示另一字元,為解決這個問題,對於多出的字碼128到255,ISO-8859針對不同區域定義了對應的字元集,從最常見的ISO-8859-1到ISO-8859-16等15個字元集(沒有ISO-8859-12,於1997年廢棄)。

字元集包括字元與字碼,普遍的錯誤認知是字元編碼等於字碼,然而從ISO-8859可知並非如此,實際上同一字元集的某個字碼也有可能有多種編碼方式...

Unicode與UTF

單位元組無法表現亞洲動輒數千個的字元,亞洲各國自行定義多位元編碼方式解決。以Big5編碼為例,採用雙位元組對正體中文常見字元編碼,然而為了與ASCII相容,檔案夾雜中英文字元時必須有規則判定,例如Big5常用漢字到次常用漢字採用第一個位元組範圍為0xA4至0xF9,假設讀取時第一個位元在此範圍,會再讀入下一個位元組,由兩個位元確定是代表哪個字元。

亞洲各國各自制定字元編碼方式出現許多問題。例如Big5為了與ASCII相容,浪費了不少字碼空間,有些文字就無法表現,使用Big5編碼的文件常會出現「方方土」來表示「堃」、「火宣」表示「煊」、「吉吉」表示「喆」的情況。由於各國電腦發展的歷史環境,編碼也有各種競爭、推廣問題,同一國家也就出現數種常用編碼的頭痛問題。西方國家也因要將軟體推廣至世界各地,不得不面對海外各國的雜亂編碼。

為了解決問題產生了Unicode字元集,在Unicode中每個字元對應一個碼點(Code point),前256字元與ISO-8859-1相容。Unicode字元使用「U+」與十六進位數字來表示碼點,例如「U+0048」對應至A字元。普及的錯誤認知是,Unicode使用兩個位元組編碼,可容納六萬多個字元,這大概是因為常見Unicode編碼多是四個十六進位數字而產生的誤解,實際上Unicode還會使用到五位或六位十六進位數字。

Unicode只是字元集,如何在電腦中編碼,則有幾個實作方式,最常見的有UTF-16與UTF-8。UTF全名Unicode Transformation Format,也就是Unicode的編碼實現機制。例如「犇」字的Unicode碼點是U+7287,若使用UTF-16,會使用16個位元儲存,也就是兩個位元組,所以UTF-16又稱為UCS-2,UCS表示Universal Character Set。

對於多位元組,處理器可能會採用不同的位元組順序,也因此面對U+7287,就得選擇該採用0x8772的位元順序呢?還是採用0x7287?如果採前者則稱為UTF-16 Little Endian,後者則稱為UTF-16 Big Endian。由於有兩種可能的UTF-16儲存方式,讀取檔案時就造成困擾,有人想出在檔案開頭塞個BOM(Byte Order Mark)作為識別,如果看到0xFFFE,表示UTF-16檔案採用Little Endian,如果看到0xFEFF,表示採用Big Endian。試試看,若Windows的記事本選擇「Unicode」選項時,實際上採用的是哪個呢?

UTF-16採用雙位元組來儲存Unicode字元,連ISO-8859-1的字元也是雙位元組,有些人覺得這對英文字母太浪費空間,於是想出UTF-8,使用可變長度位元組來儲存字元,字元儲存長度從單位元組到四個位元組。舉例來說,如果用UTF-8儲存英文字母,只會使用單位元組,如果儲存中文字「測」,則會用三個位元組。由於對英文字元,UTF-8仍用單位元組儲存,所以UTF-8對於原本就使用英文字元系統來說,既有資料並不用作什麼修改,對於需要多國語系支援的系統來說,經常採用UTF-8作為預設方案。

Unicode標準雖允許為UTF-8檔案標識BOM,但其實不需要也不建議,因為UTF-8沒有位元組順序問題。如果使用Windows記事本儲存時採用「UTF-8」,會在檔案開頭置入0xEFBBBF三個位元組作為BOM,表示這是個UTF-8編碼檔案,不過對於許多不預期會有BOM的程式來說經常造成問題。

URL編碼

現在許多程式都是基於HTTP,開發者多少就得接觸URL編碼問題。URI規範中定義:、/、?、&、=、@、%等保留字元,如果要在請求參數表達URI保留字元,必須在%後以十六進位數字編碼。例如ASCII中「:」字元編碼為0x3A,URL編碼就必須使用%3A表示,%2F則表示「/」字元。如果想發送的參數值為「http://」,必須寫成http%3A%2F%2F,URI規範稱此為百分比編碼(Percent-Encoding),俗稱URI編碼或URL編碼。

不過URI之前,HTTP在GET、POST時也對保留字作了規範,其中一個差別是空白字元,URI規範為%20,而HTTP規範為+。另一差別就是URI規範的URL編碼是針對UTF-8編碼,例如「林」的UTF-8編碼0xE69E97,在URI規範下,「林」的URL編碼就是%E6%9E%97。然而HTTP規範下的URL編碼,並不限使用UTF-8,例如在Big5網頁中,若透過表單發送「林」,瀏覽器會編碼為%AA%4C,因為Big5中「林」字元編碼為0xAA4C。

HTML實體與編號

如果在Big5編碼網頁的表單欄位中輸入「犇」送出會如何?

在HTML中規範了實體(Entity)與實體編號(Entity number),用以表達網頁檔案採用的編碼無法直接表現的字元。實體名稱格式是&entity_name;,以<與>為例,因為<與>在HTML原始碼中,用來作為標籤之用,要在網頁上呈現<與>,在HTML原始碼中必須撰寫為&lt;與&gt;。實體編號的格式為&#entity_number;,如果知道字元的Unicode碼點,要得到它的實體編號,只要將十六進位表示換為十進位表示就可以了,例如用實體編碼來表示<與>,則必須寫為&#60;與&#62;。「犇」的Unicode編碼為U+7287,7287為十六進位表示,換為十進位表示就是29319,所以前一段問題的答案是瀏覽器會送出&#29319。

那麼Big5編碼的網頁,要怎麼顯示「犇」呢?答案就是在HTML原始碼中輸入&#29319,瀏覽器就會顯示「犇」。

下次遇到亂碼問題時,請捫心自問是否至少有這些字元集與編碼基礎,不然再加上程式設計時如何處理編碼的議題,真的就只能大喊「救命啊!我的 XX 為什麼出現亂碼了!」...