原文链接:(https://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/)
原文作者:Joel Spolsky
上网多了,就必然会遇到一些关于字符编码的问题,如HTML中的Content-Type tag,比如有时候文本打开是乱码(Excel,txt都有出现)等。我们有时候会去尝试使用一些文本编辑器如notepad++之类的去更改切换,直至乱码最终能可见(或者失败)。但是不回去深思究竟编码集是如何控制的。但是其实,这些只是对程序开发是很重要,而且不是那么的难以理解。(还有些清谈,没翻译了)
要适应国际化的多语言环境,必须要适应使用特定的字符集编码,而不仅仅是八个字节的ASCII编码机处理简单的英文字符。文章内容是尽量简单基础,不会涉及到一些高级的编码特性,但是足够一个程序员理解字符编码集。
理解一件事物的最简单的方法是从起源发展逐渐说明开来。
在K&R 书写 The C Programming Language 时候,使用的是ASCII字符集编码字符,这个字符集使用8比特存储字符,32-127为可见字符,0-31为控制字符,不可打印。如果只是一位简单的英语使用者,这个编码字符集基本够用。最高位空余1比特,可以用于自定义字符。例如,在那个年代,IBM使用高位1的128字符来表示带头部的字符(OEM character),如 e ˙ \dot e e˙ 等,用于一些特定的字符语言使用者。然而,由于没有一个统一的标准,可能不同的语言会共用同样的字符集编码位置。导致语言传输显示不能跨平台。
最终OEM字符生成了一个ANSI标准,在这个标准中,统一规定了高位128字符的表示方法,在一个系统内,定义一个编码页,里面统一标识一个字符的表示结果。使用这种方式,字符语言基本能够完全表示。但是这样又存在一个问题,如果在一个文本中想同时书写希腊语和冰岛语,这时候使用ANSI也是无法表示的,因为编码页只能选择一种。
而在亚洲语系中,语言的表示就更加复杂了,汉语的基础字符就有几千数万个,这么多的基础字符不可能通过一个8比特来表示,就出现了使用DBCS(double byte character set)的处理方式,一些字符使用单字节,一些字符使用双字节,但是在程序开发中就不再适用s++/s–方式去遍历字符了,如在windows开发环境中,只能通过AnsiNext和AnsiPrev方法去遍历字符。
这种情况下,仍然有一些开发人员把一个字节当成一个字符,这样处理方法,好像也不会出问题,只要不去将字符串从一个电脑传输至另一台电脑或者从一个语系迁移到另一语系。但是事实是这种情况随着网络技术的发展越来越常见,字符编码出现问题也就越来越多。这时候,就开发了Unicode编码。
Unicode是字符编码中的一个创举,使用单个字符集来标识所有的字符。很多人误解为Unicode是一个16比特的编码,所以最多表示65536个可能的字符,但是,这个想法是错误的。这也是Unicode最奇妙的地方,所以有这个误解,也不奇怪。
实际上,Unicode使用了一种完全不同的方式来考虑字符表示,只有当你把思维带入进去,才能真的理解这种编码方式。在这里,我们假设一个字符和磁盘/内存中的一定量比特存在映射关系,如
A → 01000001 A \rightarrow 01000001 A→01000001
在Unicode中,一个字符映射到一个字符指针(code point)的抽象概念上(字符指针是怎样表示为磁盘内存上的比特不在本文考虑范畴中)。 例如,字符A,不同于字符B,也不同于字符a,但是和字符 A 是相同的,不同字体的A也被认为是同一个字符。这在英语中容易理解,但是在其他语系中,可能有不同,如德语中字符 ß 含义就是ss,而在希伯来语中,一个单词的最后一个字符的形状会发生变化,这算不算是一个新的字符?所有这些疑问都在Unicode中已经有了定论。
所有语系中的所有字符,都在Unicode中有一个唯一的 magic number,如 U+0639 ,这就是一个code point,其中,U+ 表示这是一个Unicode字符,数值 0639 是一个16进制的数字,所有Unicode字符的表示可以在网站 The Unicode Web site 看到。
Unicode能够定义的字符是无穷多的,而不仅仅是2个字节16比特表示的65536个字符。例如,我们编写一个字符串:
hello
这个字符串在Unicode的表示为
U+0048 U+0065 U+006C U+006C U+006F
截止目前为止,我们只说明了字符的Unicode表示,但是还没有涉及到这些表示是如何表示在内存或者文本中。这些就属于编码的内容了。
最早的编码方式,使用了两个字节来存储,所以上文的表示,在比特上为
00 48 00 65 00 6C 00 6C 00 6F
么?但是也可能是表示为
48 00 65 00 6C 00 6C 00 6F 00 ?
先看说明:早期的Unicode设计者希望可以设置存储他们的code point为大端存储或者小端存储,用来适配各种CPU架构如何,都能正确读取。因此在Unicode字符串中,前面必须插入一个 FE FF 的字符标识,这个标识被称为 Unicode Byte Order Mark,也就是Unicode字节序标识。如果你将标识转换为 FF FE ,那么读取Unicode的程序就知道后面的字符串也需要这样转换处理。
这样处理好像就没有问题了,但是又有人抱怨,Unicode这样处理,基本英文字符前面的一串比特0,是不是太浪费了。对于英语语系的国家,他们的文本存储相当于增加了一倍存储。而且,流通存储在网络上的那么多的文本资源,使用ANSI或者DBCS编码的,谁去做这个编码转换?因此,在一段时间内,Unicode是无人问津的。
在这个时候,**UTF-8 **出现了,UTF-8使用另一种方式来表示一个Unicode code point,在内存比特中,使用8比特的字节来表示一个Unicode字符,其中,code 0-127存储为单个字节,code point 在128-更多的,存储为2,3,或者最多6字节。
在这种优雅的表示下,所有的英语基本字符,都可以在UTF-8中正常表现,如同ASCII一样,不会有任何改变,这样,所有的美国人或者英语系国家的人们,使用起来就都没有抱怨,甚至察觉不到之间的区别。而对于其他语系的字符,可能就需要更多的字节来存储code point了(UTF-8还有一个好的性质,就是会避开使用字节0进入编码,防止被识别为字符串终止)。
有三种方式来编码Unicode,传统的使用2字节方式的UCS-2,使用16比特的UTF-16,和使用多字节的UTF-8方式,其中UCS-2需要识别出是大端还是小端表示。UTF-8是最通用的表达方式。还有一些其他的编码方式,如UTF-7,使用方式类似UTF-8,但是保证每个字节的首位比特为0;UCS-4,固定使用4字节表示一个Unicode。
Unicode编码的code point可以被多种传统编码集识别,例如Unicode字符串 Hello (U+0048 U+0065 U+006C U+006C U+006F)在ASCII,或者旧版的OEM编码,ANSI编码中都能正常显示,只是有一点,有些code point可能在当前编码集中没有对应的显示,此时,可能会显示为?或者�。一些通用的编码集,如Windows-1252,ISO-8859-1,Latin-1等,如果去存储俄文等,会出现一些问号表示无法显示,而UTF-7,8,16,32等,都能够正确存储所有的code point值。
最重要的一点认识:如果不知道字符串的编码形式,那就无法解析这个字符串的含义
那么,我们怎么识别出一个文本的编码方式呢?这里,有一些标准的做法,如对于email,你接受到的email数据中,有个header文本为:
Content-Type: text/plain;charset="UTF-8"
而对于web page,在HTTP response中header也会携带类似信息。如下图
这样的处理,存在一个问题,假设你运营着一个庞大的网站,里面有来源不同的页面,提供给不同语言的人阅读,这里网络服务器本身是不知道每个文件的编码格式,所以它不能够正确的添加对应的Content-Type头。这时候,就是使用HTML的meta标签的用法了。例如:
由于这个标签是用于之后文本的解析的,所以一定要放在页面的最前面,这样,浏览器在解析之后的页面时候,才不会解析文本出错。
如果浏览器在HTTP头和HTML页面都没有发现响应的charset设置,会如何处理呢?在这里,不同浏览器的处理方式不同,IE会基于整个文件的字节表示,推测是使用的哪种编码方式,哪种语系。这个推断是基于每个语系都有一些最常用的字符,通过推测整个文本的字符字节表示,可以大概推算出是使用了哪种编码、哪种语言。这样也会存在一种情况,一个网站使用了通用的英语,而且没有设置字符集,浏览器也是默认推断出正确的字符集和语言,浏览者也正常阅读。某一天,网站重写了部分内容,添加了部分其他语系的文字,然而用户端的推测可能就会出错。
Joel也提到了在他开发CItyDesk软件中,所有的内置文本都使用了UCS-2(2字节)的Unicode表示方式,这个也是VB,COM和windows xp内置的字符串编码类型,在所有的C++代码中,使用wchar_t定义字符串,使用wcs方法而不是str方法操作字符串,创建UCS-2字符串,使用L"Hello" 在前置L即可。使用这个软件发布web页面时,所有的字符转换为浏览器支持的UTF-8编码,这样保证了字符编码不会出错,能被各种内置字符集的用户浏览器正常阅读。
总结:
Unicode是一种 编码规范,它是将各种语系的各种基本字符统一编入一个字符集中,使用一个唯一的Unicode code point值标识,这个值也可以表示为文本形式的U+0039这样的形式,而其存入比特中的形式是多样的,这些编码集可以是变长字节的UTF-8,也可以是固定长度的UCS-2或者UTF-16等形式,而一个 Unicode编码字符集 是一个能够支持显示的Unicode code point值的一个子集,保证了在这个子集中的字符的正常显示。