2008年12月23日 星期二

C++ 中文處理指南 (概念篇)

字元的編碼 Character Encoding


  • 字元 (character): 抽象的文字符號。
  • 字元集 (character set): 系統支援的所有抽象文字符號所成集合。
  • 編碼字元集 (CCS: Coded Character Set): 在電腦中為了表示字元,把字元集中的每個字元都對應到一個非負整數(稱做碼點)' 有多少字元,就有多少碼點!
  • 字元編碼形式 (CEF: Character Encoding Form): 因為碼點直接用二進位表示會很長(表示所有人類用過的字元需要3 bytes以上),所以要想辦法把碼點對應到一個以上的區段(一個區段通常是電腦硬體可以處理的一個單位,例如說1 byte),以便處理 ' 在此階段,每個字元會被對應到"一到多個區段長的"位元串;舉例來說,ASCII會把拉丁語系字元集對應到0~127的碼點,再對應到一個位元組長的位元串。
  • 字元編碼模式 (CES: Character Encoding Scheme): 在儲存或傳輸時,整數的高位元在前 (BE: Big-Endian) 還是 低位元在前 (LE: Little-Endian),有沒有另外壓縮等等。通常在Intel IA-32架構上都是Little-Endian,故我們在此處不多做討論。
  • 把所有字元編碼成一樣長的位元串的方法稱作固定長度編碼;反之稱做變動長度編碼。固定長度編碼方便處理(因為大家都一樣長),而變動長度編碼節省空間(因為常用字比較短)。

ASCII (American Standard Coding for Information Interchange) 編碼

  • ASCII是拉丁語系(英語系)字元集的編碼,其中包含95個可見字元,包括半形英文大小寫字母,半形數字和常用標點符號(例如',', '.', '+', '-', '*', '|');以及33個不可見的控制字元(如NUL ('\0' 結尾字元),響鈴 ('\7'),定位點tab ('\t')),共128個字元。
  • ASCII是固定長度編碼,每個字元分配到一個0~127的碼點,以及1 byte長,開頭最高位元為0的位元串(故ASCII之字元集屬於單字節字元集(SBCS: Single Byte Character Set));128~255的部分被稱做延伸ASCII碼 (EASCII: Extended ASCII),包含一些像是小節符號'§',版權符號'(c)'(抱歉此處無法直接打出),發音符號等等的字元,一般情形下不使用(因為Unicode或是Big-5都有定義等價符號可用了,況且他會混淆中文的判斷,所以一般只用於拉丁語系國家,像是德法等國)。
  • 大寫英文字母在ASCII編碼下,依序出現在65~90處;小寫字母則依序出現在97~122處。是故,大寫字母 +32可直接得到該字母之對應小寫字母。
  • ASCII編碼列表可見http://zh.wikipedia.org/wiki/ASCII.

五大碼 或 大五碼 (Big-5) 編碼

  • Big-5碼是繁體中文字元集的內碼編碼方法(另一系列編碼是中文交換碼編碼方法),由資策會於1984年策劃制定,宗旨原是儘量不使用到控制碼範圍,並配合國人自制的五大套裝軟體,以此得名。2003年成為台灣標準。
  • Big-5碼系統為2 bytes之編碼系統(故Big-5之字元集屬於多字節字元集(MBCS: Multi-Byte Character Set)雙字節字元集(DBCS: Double Byte Character Set)),共可定義19782個字碼,「高位位元組」使用了0x81-0xFE,「低位位元組」使用了0x40-0x7E,及0xA1-0xFE。常用字位於0xA440-0xC67E區段中,依照筆畫再依部首排序。
  • 因為大五碼沒有避開 '\\' (0x5c, 單反斜線) 等控制字元,所以會造成某些中文字以Big-5編碼時會被誤認為跳脫序列(escape sequence),這些字包括 "許" (0xb35c), "功" (0xa55c), "蓋" (0xbb5c) 等等字元,故此問題被人名化,稱作許功蓋。(注意高位元組不會出現這個問題,因為有避開0x5c)
  • 許功蓋的解決方法: 在「許」、「功」、「蓋」這些字元後面緊接著額外的「\」字元,因為「\\」會被解釋為「\」,所以「成功\因素」這個字串就能無誤地被程式當作「成功因素」的字串來處理。但是額外的困擾是,有些輸出功能並不會把「\」當作特殊字元看待,所以有些程式或網頁就會錯誤地常常出現在「許功蓋」這些字後面多了「\」。
  • 大五碼另一個問題是有很多常用字未收入:游錫堃,王建煊,張栢芝,陶喆,峯(其正體字峰有收入),着(其正體字著有收入),双(其正體字雙有收入),綉(其正體字繡有收入),滙(其正體字匯有收入),邨(其正體字村有收入),平、片假名全未收入等等。
  • 為了解決大五碼的問題,很多人利用大五碼所保留之自由造字區擴充大五碼的涵蓋字元,包括中國海字集、香港擴充Big-5等等。
(本段參考http://www.cns11643.gov.tw/web/word/big5/index.html)
(本段參考http://zh.wikipedia.org/wiki/%E5%A4%A7%E4%BA%94%E7%A2%BC)

萬國碼 (Unicode) 編碼

  • 通用字元集 (UCS: Universal Character Set) 又稱 廣用多八位元編碼字元集 (Universal Multiple-Octet Coded Character Set) 轉成0~65535的碼點,再以2 bytes表示。0~65535的區間稱作基礎多語系平面 (Basic Multilingual Plane)plane 0,如下所示(每個小塊代表256個連續碼點,左上角從0開始算):
  • " 黑 = 拉丁文字及符號
  • " 淺藍 = Linguistic scripts
  • " 藍 = 其他歐洲文字
  • " 橘 = Middle Eastern and SW Asian scripts
  • " 淺橘 = 非洲文字
  • " 綠 = 南亞文字
  • " 紫 = 東南亞文字
  • " 紅 = 東亞文字
  • " 淺紅 = 中日韓漢字
  • " 黃 = Aboriginal scripts
  • " 紫紅 = 符號
  • " 深灰 = Diacritics
  • " 淺灰 = UTF-16 surrogates and private use
  • " 藍青 = Miscellaneous characters
  • " 白 = 未使用
  • 想當然耳,有基礎多語系平面,就有輔助平面 (SP: Supplementary Plane)。迄今有十六個輔助平面,用來放包括罕見字、方言、甲骨文之類的怪東西;此處不詳述,請見http://zh.wikipedia.org/w/index.php?title=%E8%BE%85%E5%8A%A9%E5%B9%B3%E9%9D%A2&variant=zh-cn.
  • 注意:Unicode是字元編碼標準,不是字型(glyph)標準!
  • Unicode字元表示法(不是在 C++ 中):"U+" 後面緊跟著一串16進位的數字。BMP中的字元可以用U+XXXX四個16進位數字表示(亦即,可以用2 bytes表示),SP中字元則需要五到六個16進位數字來表示。
  • 因為Unicode常用字都以 2 bytes 編碼,對於使用拉丁字母文字的國家來說太浪費,所以制定了 UTF-8 和 UTF-16 等標準來節省空間,如下文所述。

Unicode 轉換編碼 (Unicode Transform Formats, UTF):UTF-8, UCS-2, 和 UTF-16 編碼

  • UTF-8 和 UTF-16 都是為了節省 Unicode 暴力以 2 bytes 編碼所造成之空間浪費而制定之轉換編碼方式,都是變動長度編碼。
  • UTF-8 相容於 ASCII(亦即,在 ASCII 支援之拉丁語系字元集中的字元,UTF-8 用和 ASCII 一樣的 1 byte 編碼搞定),但是 UTF-16 不相容(硬加上一個 00000000 位元組確定 BMP 中每個字元以 2 bytes 編碼)。 UTF-8 對於 BMP 中的字元使用 1~3 bytes 加以編碼,越常用的越短;多位元組字元的最高有效位元會設定成 1,以防止與 7 位元的ASCII字符混淆,並保持標準的位元組主導字串(standard byte-oriented string,就像是用最高位元為 1 的位元組 形成的跳脫字串的概念啦)運作順利。
  • UTF-8 中,多位元組序列中的首個字元組的幾個最高有效位元決定了序列的長度。最高有效位為 110 的是 2 位元組序列,而 1110 的是三位元組序列,如此類推。此外,多位元組序列中其餘的位元組中的首兩個最高有效位元必定為 10。多位元組字元編碼最長為 6 bytes。
  • UCS-2 把所有在 BMP 中定義之字元以 2 bytes 固定長度編碼(但是他沒有定義 SP 中字元處理方式),UTF-16 包含了 UCS-2,且把SP中定義字元以超過兩個字元進行變動長度編碼。UTF-16 因此比 UTF-8 容易使用(常用字元之長度較統一)。附帶,UCS-2 的 ASCII 轉換就只是在 ASCII 編碼前面加上一個 00000000 位元組而已。
  • UTF-16 文件的開頭會放一個字,用來表示該文件之存放方式是 BE (Big-Endian) 還是 LE (Little-Endian);FF FE 表示 LE,FE FF 表示 BE。LE 常用於 Windows / Linux 系統,BE 常用於Macintosh 系統。
  • BE/LE 範例:http://zh.wikipedia.org/wiki/UTF-16
  • UTF-8 已經成為一個標準的編碼方法:網際網路工程工作小組(IETF)要求所有網際網路協議都必須支援 UTF-8 編碼。
C++ 程式中的中文字元處理
  • 寬字元之處理相當相依於平台(作業系統)(platform-dependent)。
  • Visual C++ 裡,MBCS 永遠是指 DBCS。Visual C++ 不內建支援大於 2 個位元組的字元集。不能以 1 個寬字元表示的字元,可以透過 Unicode 的 Surrogate (Supplementary Planes) 功能以一對寬字元來表示。
  • 一般來說,寬字元 (Unicode) 需要的記憶體空間要比多位元組字元 (DBCS+SBCS) 多,但處理較快。除此之外,在多位元組編碼裡一次只能表示一個地區設定,然而全世界所有的字元集都可以同時用 Unicode 表示。
  • C++ 中之字元:
    1. char: 最基本之字元,長度通常為 1 byte
    2. wchar_t or __wchar_t: 寬字元,C/C++都支援,長度通常是 2 bytes
    3. unsigned char: 預設char有號(但是不可以寫signed char),加上unsigned則無號,在做字元比較處理時有差異;附帶一提,直接寫unsigned a;意思是unsigned int a;
    4. MFC中定義之可移植型態:char '_TCHAR, char* & LPSTR (Win32 資料型別) ' LPTSTR,const char* & LPCSTR (Win32 資料型別) ' LPCTSTR。在有 #define _UNICODE 情形下,LPTSTR會變成 wchar_t* 否則會變成 char*。
  • 字元和字串字面常數 (character and string literal constants)
    1. 'a': constant char
    2. L'a': constant wchar_t
    3. '\XXX': XXX分別是三個八進位數字
    4. "a": 占兩個bytes('a'之後接'\0'(NUL, != NULL))
    5. L"a": 占四個bytes(L'a'之後接L'\0')
  • 程式中字串之串接: "a" "b" 和 "ab" 完全等價,但是不可以 "a" L"b" 或是 L"a" "b",行為未定義!
  • 定義 _UNICODE 常數: VC++ 下可以不用(預設即有定義),但是 command-line compilation 或是自訂編譯指令時,在程式最前面須加上 #define _UNICODE 其用處見下面說明。
  • 字串處理常式家族
  1. 處理 char 專用: (C-style, #include ) printf, scanf, _sopen_s, fopen_s; (C++ only, #include , using namespace std;) cout, cin
  2. 處理 wchar_t 專用: (C-style, #include ) wprintf, wscanf, _wsopen_s, _wfopen_s(wprintf 在印 char 字串時格式化符號用 %s,在印 wchar_t 字串時格式化符號用 %ls (搞混了會在中間截斷!));(C++ only, #include , using namespace std;) wcout, wcin,
  3. 變色龍,端視有沒有定義 _UNICODE 而決定其為 char 或是wchar_t 版本的函式庫(#include ?
  • 判斷中文字元
依照先前所說,各種多字節(multi-byte)編碼下字元的有效範圍即可判斷。
  • 印中文字元到螢幕上

Visual C++ 原始程式碼是用 DBCS + SBCS(在繁體中文環境下是Big-5)編碼的文字,但是純 C++ 下不可以直接打中文字面常數(不portable);必須用 L"\0x123" 這樣的打法。另外,printf 完全不能輸出成 UNICODE,所以

wchar_t test[]=L"測試1234";
printf("%s",test);

會爛。

  • wprintf 和 wcout 也不能輸出 UNICODE,但是他會把 wchar_t 轉成依據當地 locale 設定之 DBCS+SBCS 格式輸出,所以可以先設定 locale,讓 wout 知道現在要印的是 Big-5 而非 EASCII:
    • (wcout設定locale)

        wcout.imbue(locale("cht"));
        wcout << foo =" L" foo ="[%ls]\n"

    • 要直接輸出到 console 成 UNICODE,可以使用 Windows API 提供之WriteConsoleW:

        wchar_t test[] = L"??1234";
        DWORD ws;
        WriteConsoleW(GetStdHandle(STD_OUTPUT_HANDLE),test,wcslen(test),&ws,NULL);

  • 如果需要跨平台(Windows, Linux)的 console UNICODE 輸出,則須要使用特製的函式庫,例如IBM之ICU。
  • 在Visual C++下,使用適當的 C 執行階段函式來處理 Unicode 字串。可以用 wcs 函式家族,但是可能較喜歡完全可移植的 (已國際化) _TCHAR 巨集。這些巨集是以 _tcs 作字首;它們一對一的取代 str 函式家族。

  • 傳遞寬字元引數到程式

wmain( int argc, wchar_t *argv[ ], wchar_t *envp[ ] ){…}
or
wmain( int argc, wchar_t *argv[ ] ){…}

  • 處理可移植的字串字面常數:定義 _UNICODE 之後,_T 或是 _TEXT 將字串字面常數轉譯成以 L 為前置字元的形式,否則,_T 將字串轉譯成沒有 L 前置字元的字串。
  • 使用 fopen_s, _wfopen_s 開啟 Unicode 檔案。
  • 在 Unicode 應用程式裡,長度會給您字元的數目而不是正確的位元組數目,因為每一個字元為 2 個位元組寬。反而,您必須使用:archive.Write( str, str.GetLength( ) * sizeof( _TCHAR ) ); 但是,以字元而非位元組為主的 MFC 成員函式,使用時不需要這行額外的程式碼:

pDC->TextOut( str, str.GetLength( ) );

1 則留言: