Linux Unicode 編程
如何(在程序中)加入并使用 Unicode 以實現外語支持
|
|
級別: 初級
Thomas W. Burger
(twburger@bigfoot.com)Thomas Wolfgang Burger Consulting 的老板
2001 年 8 月 01 日
作為一個計算機的多位字符表示系統,Unicode 支持世界上所有語言的編碼和轉換。這篇文章說明了 Linux 應用程序中的國際語言支持的重要性,以及規劃 Unicode 支持并將之結合到 Linux 應用程序中去的思想。
Unicode 并不只是一個編程工具,它還是一個政治的、經濟的工具。沒有結合世界的語言支持的應用程序通常只能被那些能讀寫 ASCII 所支持語言的個人使用。這使得建立在 ASCII 基礎之上的計算機技術脫離了世界上大部分人。Unicode 允許程序使用世界上任何一種字符集,因此它支持所有語言。
Unicode 讓程序員為普通人提供用他們本國語言就能使用的軟件。這樣就不用再學一門外語了,而且更容易實現計算機技術社會和財政上的利益。很容易設想,如果用戶必須為使用因特網瀏覽器而學習烏爾都語的話,您就難以看到計算機在美國的使用。Web 就更不會出現了。
Linux 承擔了對 Unicode 很大程度上的支持。Unicode 支持被嵌入到內核和代碼開發庫中。在很大程度上,使用程序中幾句簡單的命令就能將它們自動的結合到代碼中。
所有現代字符集的基礎都是在 1968 年以 ANSIX3.4 版本出版的美國信息交換標準碼(American Standard Code for Information Interchange,ASCII)。一個值得注意的例外是在 ASCII 之前定義的 IBM 的擴充的二進制編碼的十進制交換碼(Extended Binary Coded Decimal Information Code,EBCDIC)。ASCII 是一個編碼字符集(coded character set,CCS),換句話說,它是整數到字符表示的映射。ASCII 編碼字符集允許用一個八位(基于二進制的,用值 0 或 1 表示的)字段或字節(2^8 =256)表示 256 個字符。這是一個高度受限的編碼字符集,它不能表示許多不同語言的所有字符(如中文和日文),不能表示科學符號,更不能表示古代文字(神秘符號和象形文字)和音樂符號。通過更改一個字節的長度而使更大的字符集得以被編碼,這似乎有效但完全不切實際。所有的計算機都基于八位字節。解決方法是一種字符編碼方案(Character encoding scheme,CES)― 用定長或變長的多字節序列能夠表示比 256 大的數.這些數值接著通過編碼字符集被映射到它們表示的字符。
Unicode 的定義
Unicode 通常用作涉及雙字節字符編碼方案的通用術語。Unicode CCS 3.1 的官方稱謂是 ISO10646-1 通用多八字節編碼字符集(Universal Multiple Octet Coded Character Set,UCS)。Unicode 3.1 版本添加了 44,946 個新的編碼字符。算上 Unicode 3.0 版本已經存在的 49,194 個字符,共計 94,140 個。
Unicode 編碼字符集利用了一個由 128 個三維的組構成的四維編碼空間。其中每個組包含 256 個二維平面。每個平面由 256 個一維的行組成,并且每個行有 256 個單元。每個單元在這個編碼空間內對一個字符編碼,或者被聲明為未經使用。這種編碼概念被稱為 UCS-4;四個八位元用來表示指定組、平面、行和單元的每個字符。
第一個平面(第 00 組的第 00 平面)是基本多語言平面(Basic Multilingual Plane,BMP)。BMP 按字母、音節、表意符號和各種符號及數字定義了常規使用的字符。后續的平面用于附加字符或其它還沒有發明的編碼實體。我們需要這完整的范圍去處理世界上的所有語言;特別是擁有將近 64,000 個字符的一些東亞語言。
BMP 被用作雙字節的編碼字符集,這種編碼字符集確定為 ISO 10646 UCS-2 格式。ISO 10646 UCS-2 就是指 Unicode(并且兩者相同)。BMP,像所有 UCS 平面那樣,包含了 256 行,其中每行包含 256 個單元,字符僅僅按照 BMP 中的行和單元的八位元在單元中被編碼。 這就允許 16 位編碼字符能夠被用來書寫大多數商業上最重要的語言。UCS-2 不需要代碼頁切換、代碼擴展或代碼狀態。UCS-2 是一種將 Unicode 結合到軟件中的簡單方法,但它只限于支持 Unicode BMP。
若要用 8 位字節表示一個多于 2^8 =256 個字符的字符編碼系統(character coding system,CCS),就需要一種字符編碼方案(character-encoding scheme,CES)。
Unicode 轉換
在 UNIX 中,使用得最多的字符編碼方案是 UTF-8。 它考慮到了對整個 Unicode 全部頁和平面的全面支持,而且它仍能正確的識別 ASCII。除了 UTF-8 的其他選擇還有:UCS-4、UTF-16、UTF-7.5、UTF-7、SCSU、HTML 和 JAVA。
Unicode 轉換格式(Unicode Transformation Formats,UTFs)是一種通過映射多字節編碼中的值來支持 Unicode 的字符編碼方案。本文將分析最流行的格式 ― UTF-8 字符編碼系統。
UTF-8
UTF-8 轉換格式正逐步成為一種占主導地位的交換國際文本信息的方法,因為它可以支持世界上所有的語言,而且它還與 ASCII 兼容。UTF-8 使用變長編碼。從 0 到 0x7f(127)的字符把自身編碼成單字節,而將值更大的字符編碼成 2 到 6 個字節。
表 1. UTF-8 編碼
0x00000000 - 0x0000007F: |
|
0 xxxxxxx |
0x00000080 - 0x000007FF: |
|
110 xxxxx10 xxxxxx |
0x00000800 - 0x0000FFFF: |
|
1110 xxxx10 xxxxxx10 xxxxxx |
0x00010000 - 0x001FFFFF: |
|
11110 xxx10 xxxxxx10 xxxxxx 10 xxxxxx |
0x00200000 - 0x03FFFFFF: |
|
111110 xx10 xxxxxx10 xxxxxx10 xxxxxx 10 xxxxxx |
0x04000000 - 0x7FFFFFFF: |
|
1111110 x10 xxxxxx10 xxxxxx10 xxxxxx 10 xxxxxx10 xxxxxx |
字節 10 xxxxxx是一個擴展字節,它的 xxxxxx 位位置被以二進制表示的字符代碼號的位所填充。這是能夠代表被使用代碼的最短的可能的多字節序列。
UTF-8 編碼示例
Unicode 字符版權標記字符 0xA9 = 1010 1001 用 UTF-8 編碼如下所示:
11000010 10101001 = 0xC2 0xA9
“不等于”符號字符 0x2260 = 0010 0010 0110 0000 編碼如下所示:
11100010 10001001 10100000 = 0xE2 0x89 0xA0
通過獲取 continuation byte
的值可以看到原始數據:
[1110]0010 [10]001001 [10]100000
0010 001001 100000
0010 0010 0110 0000 = 0x2260
第一個字節定義后面緊跟的八位元數,如果是 7F 或更小,這就是等價的 ASCII 值。每個八位字節以 10 xxxxxx 開頭,確保字節不與 ASCII 的值混淆。
UTF 支持
在 Linux 平臺上使用 UTF-8 之前,請確信分發包里有 glibc 2.2 和 XFree86 4.0 或更新的版本。早先的版本缺少 UTF-8 語言環境支持和 ISO10646-1 X11 字體。
在 UTF-8 發布之前,Linux 用戶使用各種不同特定語言的擴展 ASCII,像歐洲用戶用 ISO 8859-1 或 ISO 8859-2,希臘用戶使用 ISO 8859-7,俄羅斯用戶使用 KOI-8 / ISO 8859-5/CP1251(西里爾字母)。這使得數據交換出現了很多問題,并且需要為這些編碼之間的差異編寫應用軟件。這種語言支持是不完善的,而且數據交換沒有經過測試。Linux 主要的發行商和應用程序開發者正致力于讓主要以 UTF-8 格式表示的 Unicode 成為 Linux 中的標準。
為了識別 Unicode 文件,Microsoft 建議所有的 Unicode 文件應該以 ZERO WIDTH NOBREAK SPACE(U+FEFF)字符開頭。這作為一個“特征符”或“字節順序標記(byte-order mark,BOM)”來識別文件中使用的編碼和字節順序。但是,Linux/UNIX 并沒有使用 BOM,因為它會破壞現有的 ASCII 文件的語法約定。在 POSIX 系統中,選中的語言環境識別了在一個過程中的所有輸入輸出文件期望的編碼形式。
有兩種方法可以將 UTF-8 支持添加到 Linux 應用程序中。第一種方法,數據都以 UTF-8 形式存放在各處,這樣軟件改動很少(被動的)。另一種方法,被讀取的 UTF-8 數據用標準的 C 語言庫函數轉變成為寬字符數組(轉換的)。在輸出時,用函數 wcsrtombs()
使字符串被轉變回 UTF-8:
清單 1. wcsrtombs()
#include <wchar.h>
size_t wcsrtombs (char *dest, const wchar_t **src, size_t len, mbstate_t *ps);
|
方法的選擇取決于應用程序的性質。大多數應用程序可以使用被動的方法操作。這就是在 UNIX 平臺上使用 UTF-8 會如此流行的原因。像 cat
和 echo
那樣的程序就不需要修改。字節流仍只是字節流,并沒有對它進行任何處理。ASCII 字符和控制代碼在 UTF-8 語言環境中不改變。
通過字節計數對字符進行計數的程序需要一些小小的改動。在 UTF-8 中應用程序不對任何擴展的字節進行計數。如果選擇了 UTF-8 語言環境,C 語言庫的 strlen(s)
函數需要用 mbstowcs()
函數來代替:
清單 2. mbstowcs() 函數
#include <stdlib.h>
size_t mbstowcs(wchar_t *pwcs, const char *s, size_t n);
|
strlen
的一種常見用法是估算顯示寬度。中文和其它表意符號將占用兩列位置。 wcwidth()
函數用來測試每個字符的顯示寬度:
清單 3. wcwidth() 函數
#include <
wchar.h>
int wcwidth(wchar_t wc);
|
Unicode 的 C 語言支持
在正式情況下,從 GNU glibc 2.2 開始,wchar_t 類型只為 32 位的 ISO 10646 格式數值所特定使用,與當前使用的語言環境無關。通過 ISO C99 所要求的 __STDC_ISO_10646__ 宏的定義作為信號通知應用程序。 __STDC_ISO_10646__ 的定義用來指出 wchar_t 是 Unicode。精確的值是一個十進制的 yyyymmL 格式的常數。例如,使用:
清單 4. 指出 wchar_t 是 Unicode
#define __STDC_ISO_10646__ 200104L
|
是為指出 wchar_t 類型的值是由 ISO/IEC 10646 和到指定的年月為止的所有修正與技術勘誤定義的字符編碼表示。
對 wchar_t 的利用如這個示例所示,使用宏確定在 ISO C99 可移植代碼中寫雙引號的方法。
清單 5. 確定寫雙引號的方法
#if __STDC_ISO_10646__
printf("%lc", 0x201c);
#else
putchar('"');
#fi
|
語言環境
激活 UTF-8 的恰當的辦法是 POSIX 語言環境機制。語言環境是一種包含有關軟件行為特定文化約定的配置設定。它包含了字符編碼、日期/時間符號、分類規則以及度量系統。語言環境的名稱通常由 ISO 639-1 語言、ISO 3166-1 國家或地區代碼以及可選的編碼名稱和其它限定符組成。您可以用命令 locale -a
獲取所有安裝在系統上的語言環境列表(通常在 /usr/lib/locale/)。
如果沒有預安裝 UTF-8 語言環境,你可以用 localedef
命令生成它。若要為某個特定用戶生成并激活一個德語的 UTF-8 語言環境,請使用如下語句:
清單 6. 為特定用戶生成語言環境
localedef -v -c -i de_DE -f UTF-8 $HOME/local/locale/de_DE.UTF-8
export LOCPATH=$HOME/local/locale
export LANG=de_DE.UTF-8
|
有時候為所有用戶添加 UTF-8 語言環境會很有用。root 用戶使用如下指令就可以完成:
清單 7. 為每個用戶生成語言環境
localedef -v -c -i de_DE -f UTF-8 /usr/share/locale/de_DE.UTF-8
|
若要為每個用戶將這個語言環境設為缺省值,可以將以下行添加到 /etc/profile 文件中:
清單 8. 為所有用戶設置缺省的語言環境
處理多字節字符代碼序列的函數行為依賴于當前語言環境的 LC_CTYPE 類別;它確定了依賴語言環境的多字節編碼。值 LANG=de_DE(德語)會導致輸出按 ISO 8859-1 被格式化。值 LANG=de_DE.UTF-8 會把輸出格式化成 UTF-8。語言環境設置會導致 printf
中的 %ls
格式說明符調用 wcsrtombs()
函數以便于將寬字符的參數字符串轉換成依賴語言環境的多字節編碼。語言環境中的國家或地區標識符如:LC_CTYPE= en_GB (英國英語)和 LC_CTYPE= en_AU(澳大利亞英語),它們之間的差異只在 LC_MONETARY 類別中,原因在于貨幣的名稱和打印貨幣數量的規則不同。
請給您首選的語言環境設置環境變量 LANG。當一個 C 程序執行 setlocale()
函數時:
清單 9. setlocale() 函數
#include <stdio.h>
#include <locale.h>
//char *setlocale(int category, const char *locale);
int main()
{
if (!setlocale(LC_CTYPE, ""))
{
fprintf(stderr, "Locale not specified. Check LANG, LC_CTYPE, LC_ALL.
");
return 1;
}
|
C 語言庫將會依次測試環境變量 LC_ALL、LC_CTYPE 和 LANG。其中第一個含值的環境變量將決定為 LC_CTYPE 類別裝入哪種語言環境數據。語言環境數據分裂成獨立的類別。值 LC_CTYPE 定義了字符編碼,而 LC_COLLATE 定義了排序順序。我們用 LANG 環境變量為所有類別設置缺省語言環境,但 LC_* 變量可以用來覆蓋單個類別。
您可以用命令 locale charmap
查詢當前語言環境中字符編碼的名稱。如果您從 LC_CTYPE 類別中成功選取了 UTF-8 語言環境,會輸出 UTF-8。命令 locale -m
提供一張已安裝的所有字符編碼名稱的列表。
如果您使用專門的 C 語言庫的多字節函數來完成所有外部字符編碼和內部使用的 wchar_t 編碼之間的轉換,那么 C 語言庫將承擔責任,根據 LC_CTYPE 使用正確的編碼方式。這甚至不需要程序被明確的編碼成當前的多字節編碼。
如果需要一個應用程序能明確的支持 UTF-8(或其它編碼)轉換方法而不用 libc 多字節函數,則應用程序必須確定是否需要激活 UTF-8 模式。帶有 <langinfo.h> 庫頭文件的與 X/Open 兼容系統可以用如下代碼:
清單 10. 檢測當前的語言環境是否使用了 UTF-8 編碼
BOOL utf8_mode = FALSE;
if( ! strcmp(nl_langinfo(CODESET), "UTF-8")
utf8_mode = TRUE;
|
為檢測當前語言環境是否使用了 UTF-8 編碼。首先必須調用 setlocale(LC_CTYPE, "")
函數,依據環境變量設置語言環境。nl_langinfo(CODESET) 函數也是由 locale charmap
命令調用,從而查找當前語言環境指定的編碼名稱。
另一種可以使用的方法是查詢語言環境變量:
清單 11. 查詢語言環境變量
char *s;
BOOL utf8_mode = FALSE;
if ((s = getenv("LC_ALL")) || (s = getenv("LC_CTYPE")) || (s = getenv ("LANG")))
{
if (strstr(s, "UTF-8"))
utf8_mode = TRUE;
}
|
這項測試假設 UTF-8 語言環境名稱中有值“UTF-8”,但實際情況并不總是如此,所以應該使用 nl_langinfo()
方法。
總結
為支持世界上的所有語言,需要一種具有八位字節字符編碼策略的字符編碼系統,它的字符應多于 ASCII(一種使用無符號字節的擴展版本)的 2^8 = 256 個字符。Unicode 就是這樣一種字符編碼系統,它具有由 128 個三維組(帶有由大量字符編碼方案的方法支持的 94,140 個定義好的字符值)組成的四維編碼空間,在 Linux 中更流行的字符編碼方案是 Unicode 轉換格式 UTF-8。
參考資料
關于作者