软件全球化-Unicode
前言
当听到软件国际化或本地化的时,很多人第一反应这是一个翻译的过程,其实不然。翻译只是软件国际化或本地化过程中的一个环节,在此之前还有很多关于工程以及技术上需要解决的问题。这篇文章我们从全球化最为基础的知识-Unicode开始,之后的文章还会介绍如何实施软件全球化。
1. 构建计算机世界巴别塔的基础-Unicode
混沌的世界
当构建的软件被一个市场认可后,公司为了增加利润往往会将软件推广到其它的市场,而这些市场很可能使用不同的语言,这个时候我们首先想到的是翻译用户界面的文字来方便用户的使用。但是因为计算机发展历史的问题,其实简单的翻译根本不能完全解决软件本地化的需求(因为软件本地化不仅仅是语言的问题,还包含地域文化问题)。
时势造英雄-Unicode
在计算机被发明后,西方人以他们的文化认知以为一个字节(Byte可以表达256个字符)就足以表达他们已知的文字和符号(当时硬件、网络的限制这是合理的),于是他们为常用的128个字符定义了一套编码(我们所熟知的ASCII)。随着亚洲文明的进入,我们发现一个字节根本就不够用,单单一本新华字典就接近一万个字。于是华夏儿女开始利用双字节进行编码,这样最多可以表示65536个汉字和符号(GB2312编码),接着日本、韩国,阿拉伯、印度都开始定制自己的编码标准。一开始每个国家都各自为政好像并没有发生什么冲突。随着经济全球化,互联网的兴起,不同国家或地区的通讯变得愈发频繁,冲突也开始慢慢呈现。因为标准的不一致,我们需要对不同的标准进行转换,假如世界上200多个国家都有自己的一套编码标准,那么我们就需要2万种转换算法,这是多么庞大的工程。为了减少构建跨国家或跨地区的软件成本,我们必须统一计算机的语言,于是Unicode出现了。
Unicode标准
Unicode对全球的文字、符号进行统一编码,每一个字符或符号都有一个唯一的内码(Code Point)。就像字母“a”的ASCII码是0x61(97)一样,每一个字符或符号在Unicode标准中都有一个类似的内码。当然Unicode是兼容ASCII的,它的前128位的字符码和ASCII完全一致唯一的区别是Unicode用双字节表示,比如字符“a”的Unicode内码是0x0061(97)。
Unicode由专门的国际机构统一维护,目前的版本是9.0,它包含了128,172文字、符号、表情等符号,也许你已经发现字符的总数已经远远超过两个字节可以表示的范围(0-65536)。目前Unicode使用的是UCS-4(Universal Character Set:4字节的通用字符集),理论上它可以表达2³²种符号。根据标准,以0开头的前8位表示128个组(group),紧接着后面8位表示256平面(plane),最后16位表示每个平面包含65536种编码。根据ISO-10646规定Unicode组织不会对超出0x10FFFF进行编码(鬼知道哪天会不会出现火星文,人类的认知也在不停的被刷新)。这样Unicode最多可表示1114112(17*65536总共17个平面),目前已编码的字符主要分布在平面0(BMP-Basic Multilingual Plane),平面1,2,14(SMP:Supplementary Multilingual Plane,其中平面2包含了中日韩等汉字-CJK)以及平面15, 16(Private Use Areas:保留私人自定义的编码)。
2. Unicode的实现方式
Unicode是一个标准,但是如何实现这个标准则有很多方式,下面介绍最常用的三种编码方式(TF32、UTF16、UTF8)
-
UTF32
UTF32是以32位无符整数为单位的编码方式。目前Unicode使用的UCS-4,所以以4个字节来表示一个内码是最直接的方式。比如汉字“中”的UTF32编码是(0x00004E2D),英文字母“a”的UTF32编码是(0x00000061)。UTF32是效率最高的编码方式,但是我们知道时间复杂度和空间负责度是相互牵制的,所以编码UTF32会比其它编码更占内存。
-
UTF16
UTF16是以16位无符整数为单位的编码方式。当然16位无符整型最多可表达65536种内码,所以在BMP(第0平面)平面定义的内码可以直接由2个字节表示。但是BMP(第0平面)以外的字符(大于0x100000)必须由4个字节表示。现在问题来了,在一串字节流中有的字符是由2个字节表示而有的则是由4个字节表示,那么计算机如何区分这些情况呢?在Unicode标准里定义了一个代理区(Surrogate)的概念,它保留了(0xD800 - 0xDBFF)和(0xDC00 - 0xDFFF)之间的值作为高位代理和低位代理。只要前两个字节的范围在0xD800到0xDBFF之间并且和它相邻的后两个字节范围在0xDC00到0xDFFF之间那么计算机就认为这4个字节表示的是一个字符。否则两个字节表示一个字符。UTF16相对于UTF32需要的编码空间更少,但是它需要一些计算来区分是4个字节还是两个字节表示一个字符。
-
UTF8
UTF8是以8位无符整型为单位的编码方式。由于它编码空间少,所以很适合作为网络传输。因为UTF8以8位无符整型为单位,在表示ASCII中的128个符号时,它只需要一个字节。但是这也给计算机带来了复杂度,因为表示0x1FFFFF种字符可能需要用1-4个不等的字节来表示。于是UTF8定义了如下图的编码规则:
——————————————————————————————————————————————————————————————
Unicode 编码 | UTF8
——————————————————————————————————————————————————————————————
0x000000 - 0x00007F | 0xxxxxxx
——————————————————————————————————————————————————————————————
0x000080 - 0x0007FF | 110xxxxx 10xxxxxx
——————————————————————————————————————————————————————————————
0x000800 - 0x00FFFF | 1110xxxx 10xxxxxx 10xxxxxx
———————————————————————————————————————————————————————————————
0x010000 - 0x1FFFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
————————————————————————————————————————————————————————————————
UTF8在存储空间上做了折中的方案,比如字母“a”的编码是0x61,而“中”字的编码是0xE4B8AD(“中”字反而比UTF16编码更占空间)。
3. 字节序
在介绍了编码后,我们不得不提另一个和编码密切相关的概念-字节序。
字节顺序是指大于一个字节类型的数据在内存中的存放顺序。尤其是互联网的出现,异构计算机之间的交互变的越来越频繁。而不同CPU的计算机处理字节是不同的比如Intelx86是以小端(Little-Endian)处理数据,而PowerPC 、SPARC和Motorola处理器则是大端(Big-Endian)处理数据(很多中高档服务器并没有使用Intel CPU)。于是在网络这层统一规定利用TCP/IP传输协议就必须以(Big-Endian)的顺序处理字节。
下面我们还是以“中”字为例,看看它的大小端表示方式。“中”的UTF16和UTF32的编码分别为(0x4E2D)和(0x00004E2D)
————————————————————————————————————————————————————
Encoding | Big Endian(FE FF) | Little Endian(FF EF)
————————————————————————————————————————————————————
UTF16 | 2D 4E | 4E 2D
————————————————————————————————————————————————————
UTF32 | 2D 4E 00 00 | 00 00 4E 2D
————————————————————————————————————————————————————
这里我们将编码的高位放在右侧低位放在左侧则表示大端,反之表示小段。也许你已经发现UTF8并没有列出大端和小端,确实UTF8是不需要指定大小端的,因为UTF8是以一个字节为单位的,所以计算机可以根据第一个字节知道后续还需读取多少个字节来表示一个字符(如果第一个字节的第一位是0表示就一个字节表示当前字符,或者第一个字节的前几位有多少个1就表示有几个字节表示当前字符)。那么如何知道一个字节流是大端还是小端呢?Unicode建议用BOM(Byte Order Mark)来表示字节序。在Windows notepad中我们可以选择不同编码形式保存文件,比如我们以Unicode格式保存,那么在以16进制打开文件时,我们会发现文件的前四位是FF EF。如果以Unicode Big Endian编码保存时前四位是EF FF。根据BOM计算机可以清楚的知道当前处理的字节流是大端还小端,虽然UTF8不需要指定字节序但是Unicode依然给它保留了BOM(EF BB BF)。
——————————————————————————————————————————————————
UTF Encoding | Byte Order Mark (BOM)
——————————————————————————————————————————————————
UTF-8 without BOM | -
——————————————————————————————————————————————————
UTF-8 with BOM | EF BB BF
——————————————————————————————————————————————————
UTF-16LE | FF FE
——————————————————————————————————————————————————
UTF-16BE | FE FF
——————————————————————————————————————————————————
UTF-32LE | FF FE 00 00
——————————————————————————————————————————————————
UTF-32BE | 00 00 FE FF
——————————————————————————————————————————————————
4. Windows对Unicode的支持
- 字符类型(Char)
Windows默认的编码是UTF16,所以.Net中Char的类型是2个字节,C++中定义的wchar_t也是2个字节(据说Linux平台wchar_t是4个字节)。因为.Net的是跨操作系统的,所以即使在Linux平台下.Net也可以统一Char为2个字符。C++虽然不能由平台统一,但是我们可以自己定义编译条件:
#ifdef CHAR16T
typedef unsigned short Char
#elif CHAR32T
typedef unsigned int Char
#else
typedef char Char
#endif
- 代码页(Code Page)
很多国家都为自己的语言定义了一套字符编码标准(统一称为Multi Byte Character Set多字节字符集),那么如何正确显示不同编码的文字呢?Windows中有个代码页的概念,只要我们在控制面板把区域和语言设置成指定的语言,那么相应编码就可以显示正确字符,否则就会出现乱码。当然我们可以将非Unicode的编码转成Unicode编码,如何实现不同编码到Unicode的转换呢?Window提供了一个转换函数:
int MultiByteToWideChar(
UINT uCodePage,
DWORD dwFlags,
PCSTR pMultiByteStr
int cchMultiByte,
PWSTR pWideCharStr,
int cchWideChar)
其中第2个参数,是区分重音节的标志,这个参数并不是很常用,后四个参数分别是多字节字符串的大小和首地址以及转换成功的Uncoide字符串大小和首地址。至于第一个参数就是代码页的索引,在操作系统中保存有多个国家的编码标准,而且每个编码标准都有一个索引号,比如GB2312的索引号是936,通过这个索引号我们可以得到具体编码对应的字符,然后通过字符找到Unicode(UTF16的索引号是1200)的编码。 下面是C#的具体实现, Encoding.Convert应该就是调用了上面的API。
using System.Text;
public void string GB2312toUnicode(string text)
{
var gb2312 = Encoding.GetEncoding("gb2312");
var utf16 = System.Text.Encoding.GetEncoding("unicode");
var gb = gb2312.GetBytes(text);
var unicode = Encoding.Convert(gb2312, utf16, gb);
return utf16.GetString(unicode);
}
- 字符的比较
在Unicode中有些字符有多种表示形式,比如字符”ä”有两种表示形式:(0x00E4)和(0x0061,0x0308)。第二个种编码其实是个组合字符,第一个0x0061是字母“a”的编码,而0x0308是a上面的两个点的编码。有了这样的规则我们可以利用现有符号组合出更多的字符(当然不是所有的字符都可以组合的)。看下面一段代码:
string grapheme = "\u0061\u0308";
string singleChar = "\u00e4";
Console.WriteLine("==结果:{0}", grapheme == singleChar);
Console.WriteLine("Equals结果: {0}", String.Equals(grapheme, singleChar,
StringComparison.CurrentCulture));
代码的结果是==操作符返回的是False,而Equals函数返回的是True。因为在Equals函数我们指定必须考虑文化因素,而==操作符只是简单的字节顺序比较。在.Net的System.Globlization下有很多关于本地化操作的功能,我会在后续的文章中介绍。
后记
Unicode的出现大大降低了计算机之间交互的成本,如果没有Unicode我们需要了解每一个国家或地区的编码,然后为每一种编码提供转换成另一种编码的算法,可以想象那是多么大的工程。