字元集與編碼的歷史發展錯綜複雜,要瞭解程式處理字元編碼確實是很大的議題,然而現代開發者仍需具備字元集與編碼知識基礎,面對更複雜的編碼議題,才有解決的脈絡可循。

字元集與字元編碼
文字是人類溝通、書寫符號,字元是電腦中處理的符號資料,文字是可見的,但字元可能無法顯示。每個國家、地區,甚至每段歷史使用的文字符號都不同,也隨時在增加,為了讓電腦可以有限方式處理字元,必須事先規範哪些集合包括哪些字元,也就是定義字元集(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字元只會用到7個位元,於是有人把第8個位元拿來用,像是字碼128到255拿來表現英語系以外的歐洲文字,然而,多出的字碼空間並不足以表現所有文字,造成了使用到第8位元的同一位元組,在採某編碼的電腦顯示某字元,而採另一編碼的電腦顯示另一字元。

為解決這個問題,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字元集。其中,每個字元對應1個碼點(Code point),前256字元相容ISO-8859-1。Unicode字元使用「U+」與十六進位數字來表示碼點,例如「U+0048」對應A字元。

但普及的錯誤認知是,Unicode使用兩個位元組編碼,可容納6萬多個字元,這大概是因為常見Unicode編碼多是4個十六進位數字而產生的誤解,實際上Unicode還會使用到5位或6位十六進位數字。

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,用可變長度位元組來儲存字元,字元儲存長度從單位元組到4個位元組。

舉例來說,如果用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編碼。

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實體與編號
在HTML中規範了實體(Entity)與實體編號(Entity number),用以表達網頁檔案採用的編碼所無法直接表現的字元。

實體名稱格式是&entity_name;,以「<」、「>」這兩個字元為例,因為在HTML原始碼中,都用來作為標籤之用,要在網頁上呈現「<」與「>」,在HTML原始碼中必須撰寫為<與>。

實體編號的格式為&#entity_number;,如果知道字元的Unicode碼點,要得到它的實體編號,只要將十六進位表示換為十進位表示就可以了,例如用實體編碼來表示<與>,則必須寫為<與>。「?」的Unicode編碼為U+7287,7287為十六進位表示,換為十進位表示就是29319。那麼Big5編碼的網頁,要怎麼顯示「?」呢?答案就是在HTML原始碼中輸入犇。

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

 

作者簡介


Advertisement

更多 iThome相關內容