当前位置:Linux教程 - Linux - Linux Unicode 编程

Linux Unicode 编程

如何(在程序中)加入并使用 Unicode 以实现外语支持
Thomas W. Burger (twburger@bigfoot.com)
Thomas Wolfgang Burger Consulting 的老板
2001 年 8 月

作为一个计算机的多字节字符表示系统,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: 0xxxxxxx
0x00000080 - 0x000007FF: 110xxxxx 10xxxxxx
0x00000800 - 0x0000FFFF: 1110xxxx 10xxxxxx 10xxxxxx
0x00010000 - 0x001FFFFF: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
0x00200000 - 0x03FFFFFF: 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
0x04000000 - 0x7FFFFFFF: 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

字节 10xxxxxx 是一个扩展字节,它的 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 值。每个八位字节以 10xxxxxx 开头,确保字节不与 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
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
size_t mbstowcs(wchar_t *pwcs, const char *s, size_t n);


strlen 的一种常见用法是估算显示宽度。中文和其它表意符号将占用两列位置。wcwidth() 函数用来测试每个字符的显示宽度:

清单 3. wcwidth() 函数
#include
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
#include
//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 模式。带有 库头文件的与 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。

参考资料

请访问 Unicode 联盟的 Unicode 主页,这里定义了 Unicode 字符之间的行为和关系,并为实现者提供了技术信息。
国际标准组织(International Organization for Standardization,ISO)是一个由 140 个国家组成的全球性的国家标准社团联盟。
ANSI 是个私有的、非营利组织,它管理并调整 U.S. 的志愿标准化以及一致性评价系统。
ISO C99 Draft(Acrobat PDF 格式,556 页),是新的 C 语言标准,来自 Calgary 大学 Ben 的 C 编程课程。
C 语言的新 ISO 标准讨论了 C9x 标准。
请阅读 Roman Czyborra 的 Unix 环境下的 Unicode。
请查阅由 David A. Wheeler 撰写的 Secure Programming for Linux and Unix HOWTO 中的 Character Encoding 章节。
请阅读 IANA(Internet Assigned Numbers Authority)中的 IANA Charset Registration Procedures。
请参阅 Virginia 大学图书馆 Robertson Media 中心的 Unicode Music Symbols。
请看看 graphic representation of the Roadmap to the BMP, Plane 0 of the UCS。这些表包含了由 0 号,也就是通用字符集(Universal Character Set,UCS)的基本多语言平面(Basic Multilingual Plane,BMP)实际大小的映射组成的。Everson Gunn Teoranta 是一个自 1990 年开办的支持少数民族语言团体的软件和出版公司,由 Michael Everson 和 Marion Gunn 共同建立。
请浏览 UTF-8 and Unicode FAQ for UNIX/Linux,Markus Kuhn 的综合性的 one-stop 信息资源,关于您如何在 POSIX 系统(Linux,UNIX)使用 Unicode/UTF-8。
请检查 Technology Appraisals Ltd 的 Solution Given by the Universal Character Set,其中提供了独立的、高质量的有关电子商务系统、电子信息传递、XML、网络和 IT 安全的信息、教育和培训。
请阅读 Mulberry Technologies, Inc 的 Unicode presentation titled“10646 and All That”,一个专攻基于 SGML 和 XML 系统的电子出版物的咨询公司。
UTF-8, a transformation format of ISO 10646 是由俄亥俄州立大学的计算机和信息科学系指定的因特网社区的因特网标准跟踪协议。
请咨询 Linux 程序员手册上的 UTF-8 — an ASCII compatible multi-byte Unicode encoding。
请阅读 Unicode Standard Annex#15 Unicode Normalization Forms,一篇描写了四种 Unicode 文本标准化格式规范的文档。有了这些格式,等价的(规范或是兼容的)文本将会有同样的二进制表式。当实现工具在标准化的格式中保留了一个字符串,可以确保有一个以二进制形式表现的独一无二的等价字符串。
请阅读 man-pages.net 上的 mbstowcs,它把多字节字符串转换成了宽字符的字符串,man-pages.net 为 Linux 手册页面提供了永久的基于 Web 的归档文件。
请阅读 Menlo 学校的主页上的 wcwidth,它能决定一个宽字符代码值的所占列位置的列数。
请阅读 Hewlett Packard 的开发者资源站点的 Linux 程序员手册上的 wcsrtombs,它能将宽字符的字符串转化为多字节字符串。
请阅读 MKS 工具箱文档中的 setlocale(),它能改变或查询语言环境。MKS 软件公司是在 Windows 环境或混合 UNIX/Linux 和 Windows 环境中用于系统管理和开发的 Windows 自动化工具的领先供应商。
请学习 IBM Classes for Unicode (ICU),一个 C 语言和 C++ 语言库,它在许多平台上提供了健壮的和功能完善的 Unicode 支持。
请参阅 IBM 的“Introduction to Unicode”站点,这里深入涵盖了 Unicode 基础知识。
在 IBM 的关于新兴技术的 alphaWorks 站点。请参阅:
UnicodeCompressor,这里提供了使用标准 Unicode 压缩方案的压缩和解压缩 Unicode 文本的工具
Unicode Normalizer,为实现快速排序和搜索将 Java 字符串对象转换为标准 Unicode 格式。
请阅读 TW Burger 撰写的“Cyrillic in Unicode”和 Jim Melnick 撰写的“Multilingual forms in Unicode”,也在 developerWorks 上。
请在 developerWorks 上浏览更多 Linux 参考资料。
请在 developerWorks 上浏览更多 Unicode 参考资料。

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