Linux Unicode 编程

如何(在程序中)加入并使用 Unicode 以实现外语支持

developerWorks
文档选项
将此页作为电子邮件发送

将此页作为电子邮件发送

未显示需要 JavaScript 的文档选项



级别: 初级

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 会如此流行的原因。像 catecho 那样的程序就不需要修改。字节流仍只是字节流,并没有对它进行任何处理。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. 为所有用户设置缺省的语言环境
export LANG=de_DE.UTF-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。



参考资料



关于作者

TW Burger 从 1979 年起曾经做过编程、讲授中等计算机课程以及撰写有关计算机技术方面的书。他正在经营一个信息技术咨询公司。您可以通过 twburger@bigfoot.com 与他联系。