程序员

每一个开发者绝对必须至少需要了解unicode和字符集(没有例外

2018-11-24  本文已影响11人  Deepdoo

--- 原文地址在文章最下方


回顾历史

  想弄明白字符集最好的方式就是按照年代的顺序去了解它的发展历程

  在半导体盛行的那个年代,那时候Unix被发明出来,K&R开发了C语言,所有的一切都很简单。对于那时的计算机和程序员来说,唯一有意义的字符集合就是英文字符,当时有一套编码规范叫ASCII,这个规范可以用32到127之间的数字表示所有的英文字符。32表示空格,65表示大写字母A。这套规则可以很方便的用7个二进制位来存储。此时大多数计算机都是使用8个二进制位作为一个字节,所以一个字节就足以保存所有的ASCII字符,并且还有一个完整的二进制位没有被用到。ASCII编码中数值小于32的被称为不可打印字符,他们主要是用来念咒语诅咒别人,可惜这是一个玩笑,他们真正的目的是作为控制字符,比如7代表的字符会使你的计算机发出beep的声音,12代表的控制字符会控制打印机换纸。

  对一个英语使用者来说,这一切都不会有什么问题。

  正是因为一个字节有8个二进制位,很多人开始想“嗯,我们可以使用128-255之间的字符来表达我们自己的编码”,问题是很多人在同一时间有了这个想法,但是如何填充128-255之间的空间他们的想法又不一样,最终产生了许许多多对这128个字符的使用方式,也就产生许多的不同的字符编码方式。后来IBM-PC设计出来一种名叫OEM的字符编码规范,为不同地区不同语言的计算机的128-255的空间分别制定了一套字符编码规则,而且这种规范还被收录到ANSI的标准中,在ANSI标准里,大家都对于低7位的使用都达成一致,就是和ASCII一样,但是有很多不同的方法来使用高128的空间,具体如何使用就取决于你在哪里居住,你使用什么语言。这些不同的标准就被称为 code pages(可能被翻译为 编码页,但是使用英文更容易理解)。比方说以色列的DOS操作系统使用862号code page,而希腊的用户使用737号code page。MS-DOS系统的不同国家语言的各个版本分别支持了不同 code page,用来处理从英文到冰岛语的各种语言,甚至还有支持“多语言”的code page,能同时在一台计算机上处理Esperanto(世界语) and Galician(加利西亚语),但是如果两种语言用互不兼容的方式使用最高位的128个空间,那就无法同时支持。

  与此同时,在亚洲,更加疯狂的事情有发生了,亚洲的文字多达上千个,8个二进制位根本放不下。然而一种叫DBCS(“double byte character set”)的复杂编码出现了,在这种编码里,有的字符占用一个字节,有的字符占用两个字节,这样一来,很容易在字符串中向前移动,但是向后移动变得不太可能,编程者被鼓励使用 Windows’ AnsiNext 和 AnsiPrev 两个方法来取代类似s++和s--的指针移动指令,因为这两个方法知道如何处理这种复杂的映射关系。

  但是,大部分人还是依旧把一个字节当做一个字符,认为一个字符占用8 bits,当然前提是你永远不会把字符串从一台计算机移动到另一台,并且你不会使用两种不同的语言,如果是这样的话,一切还可以正常运行。可是,伴随着互联网的出现,在计算机之间传输数据变成了家常便饭,这样本来乱糟糟的各种编码规范瞬间瓦解了。幸运的是,Unicode被发明了出来。

Unicode

  Unicode 勇敢的尝试着创造了一个唯一的字符映射集合,包含了这个星球上所有的可以书写出来的字符,甚至也包含了一些杜撰出来的语言,比如 Klingon(克林贡语)。有些人误以为Unicode只不过是简单的16位编码,每个字符都占用16个二进制位,总共有2的16次方(65536)个字符。事实上,这并不正确。这只是一个对于Unicode的很常见的说法,如果你也这样认为,不要觉得悲伤。

  事实上,Unicode用一种不同的视角来看待字符,你必须要以Unicode的方式来看待问题,不然一切都没有意义。

  截止目前,我们假设一个字符可以映射成为二进制的一些数据位,你可以把这些bits存到内存或者硬盘里:

  A -> 0100 0001

  在Unicode中,一个字符映射为一个被称之为 code point 的东西,但是这只是一个理论性的概念。这个code point如何在内存和硬盘里存储是另外一个完全不同的话题。在Unicode中,字符 A 只是一个概念(原文是:`the letter A is a platonic ideal. It’s just floating in heaven`),它和 B 不同,和 a 也不同, 但是它和Times New Roman字体的A一样,和Helvetica字体的A也一样,这似乎没有什么争议,但是在一些其他语言中,可以找到能引起争议的字符:德语中的ß是真表示一个字符还是两个 ss 的另外一种漂亮的书写方式?如果一个单词末尾的字符的书写形式改变了,它就变成了另外一个字符吗?犹太人说是的,阿拉伯人说并不会。不管怎样,Unicode组织中聪明人在距今大约25年前的时间(原文是10年前,文章著于2003年)已经把这个问题弄明白了,同时也伴随着大量的高度政治辩论。

  Unicode为所有语言的字母表中的每一个概念性的字符都被分配了一个魔法数字,写出来像这样:U+0639.这个魔法数字就是 code point。“U+”意味着这是Unicode,后面的数字是16进制(hexadecimal)。U+0639表示的是阿拉伯字符 Ain,英文里的 A 的Unicode 是 U+0041.你可以使用Windows系统中的 charmap 命令查找字符的Unicode映射,或者访问 Unicode官方网站

  Unicode能够定义多少个字符并没有真正意义上的限制,事实上早已超过了 65536 ,所以并不是每一个Unicode字符都可以被放置到两个字节里,所以那只是一个说法而已。

  好了,假设我们有下面这样一个字符串:

    Hello

  根据这五个字符的Unicode转换成Unicode编码就是这样的:

    U+0048 U+0065 U+006C U+006C U+006F

  只是一串code points,就是一串数字而已?我们还没有讨论如何在内存中存储这些Unicode编码或者如何在邮件信息中表示他们。

 Encoding

  这个时候字符编码出现了。

  最早时候对Unicode的编码的方式就是将Unicode字符存储在两个字节中,这种方式就导致了后来人们对Unicode字符占2个字节的误解。所以 Hello 就变成了:

  00 48 00 65 00 6C 00 6C 00 6F

  但是,也可能是这样的:

  48 00 65 00 6C 00 6C 00 6F 00

  早期对Unicode编码的实现者希望可以将字符编码保存为high-endian 和 low-endian 两种模式,具体使用哪种取决于他们的CPU对哪种存储方式处理的更快速,所以就有了两种方式存储Unicode。为了实现两种字节序,又有一种怪异的惯例被发明出来,就是在文件的最开始存储两个多余的字节 FE FF(大端序) ,这个被称为 UBOM (Unicode Byte Order Mark),如果你把这两个前缀字节的顺序交换位置就变成了 FF FE (小端序),然后你的字符串就会被解码的人按照相反的字节序解析。一开始并不是所有的Unicode字符都有字节序的。

  在一段时间内Unicode看似很不错了,但是开发者开始抱怨了。他们说“看那些多余的0”,因为他们是美国人,他们使用英语,所以他们几乎用不到 U+00 ~ U+00FF 之外的 code point,因此有的人认为应该保留单字节的编码,有的人也并不介意增加一个字节的空间存储字符。而且更多人不想更改编码是因为已经有大量的文本是使用各种各样的 ANSI 和 DBCS字符集编码的,没人愿意重新把这些文件转换编码。就因为这个原因,大部分人决定忽略Unicode,这种情况持续了好几年,同时也使事情变得更糟糕。

  就这样,精妙的UTF-8编码思想被发明出来了。UTF-8是另外一种编码方式,使用8位的字节在内存中存储Unicode 的code points,也就是U+数字。值在0-127之间的code point使用一个字节存储,只有 > 128 的 code point 使用 2个,3个最大至6个字节存储。看图:

  这样的处理方式对英文几乎没有影响,因为对于单字节字符来说UTF-8和ASCII的存储方式一模一样,只有英语之外的语言使用者需要处理多个字节,对于 Hello 来说,会被存储为 48 65 6C 6C 6F,这不光和ASCII的存储方式一样,而且和ANSI以及地球上的每一种OEM字符集都一样。如果你使用的 code point 需要多个字节来存储,美国人也不会觉察到。UTF-8还有一个很好的特性:一些老旧的字符串处理代码使用一个0字节作为字符串的结束标识,UTF-8可以防止字符串被这种代码截断。(这样翻译不知道是否准确,附上原文:UTF-8 also has the nice property that ignorant old string-processing code that wants to use a single 0 byte as the null-terminator will not truncate strings)。

  到这里,已经介绍了三种编码Unicode的方法:一个是传统的2个字节存储的UCS-2(大端序或者小端序),另外一个是UTF-16(使用16个bits),和最新流行的UTF-8标准,它拥有良好的特性,可以让在英文文本上工作感知不到和ASCII有什么区别。事实上还有一些别的编码方式,比如UTF-7,比较类似UTF-8,但是它保证字节的最高位永远是0,还有 UCS-4,使用4个字节存储所有的code point。

  可能你已经考虑到了,Unicode表示的所有字符对应的 code points 同样也可以被编码到老旧的编码规范里!比如,你可以把表示 Hello 的Unicode字符串 (U+0048 U+0065 U+006C U+006C U+006F)编码成ASCII,或者旧版的 OEM 希腊编码,或者希伯来语的 ANSI编码,或者任意的其他上百中已经被发明出来的编码,但是有一个情况是:如果你尝试使用的编码里没有和这些 code points 正好对应的映射关系,那么这些 code points 中的某些可能不会被正确的显示出来,通常显示出来的会是一个小问号:?,或者是像这样的一个小方块:�。

  有上百种传统的字符集编码只能存储一部分 code points ,剩下的其他 code points 都会被显示成问号。其中有一些很流行的英文编码,Windows-1252 (the Windows 9x standard for Western European languages) 和 ISO-8859-1, 还有 Latin-1 (对西欧语言来说很好用),但是如果尝试把俄语或者希伯来语存储到这些编码里,你会的到很多很多的问号。UTF 7, 8, 16, 以及 32这类编码是可以完整而且正确的存储所有的 code points 的。

The Single Most Important Fact About Encodings

  如果你已经完全忘了前面解释过所有一切,请记住一个尤其重要的事情。

  (It does not make sense to have a string without knowing what encoding it uses. )字符串离不开编码。你再也不可以把头埋在沙子里,然后假装普通文本就是 ASCII。(There Ain’t No Such Thing As Plain Text.)根本没有普通文本这回事。

  如果你有一个字符串在内存里,或者在一个文件里,或者在邮件里,你必须知道这个字符串是用什么方式编码的,否则你无法正确的处理它或者把它展示给用户。

  如果你不指明你的字符串是使用了UTF-8 或者 ASCII 或者 ISO 8859-1 (Latin 1) 或者 Windows 1252 (Western European)中的哪一个,你肯定就无法正确的显示它,甚至你根本找不到字符的在何处终止。就是因为这样,开发者经常会抱怨这些问题,“我的网站又乱码啦”,“当我使用音调的时候她就看不懂我的邮件了“。现在已经有多达上百种编码针对大于 127 的 code points,靠运气已经不切实际了。

  我们要如何标识一段字符使用了什么编码格式呢?当然,有一些标准方法,对于电子邮件信息,你需要在header里放置这种格式的字符串

  Content-Type: text/plain; charset="UTF-8"

  对于网页来说,最初的想法是服务器增加一个类似 Content-Type 的 http header 和网页一起返回,并不是写在HTML里,而是做为一个响应 header 在 HTML页面之前发送,但是这导致了一个问题,如果一个网站的所有页面各自由使用不同语言的开发者开发,而且这些开发者各自使用不同的编码,服务器就无法知道应该发送什么编码格式了。所以如果可以在HTML文件里使用某种标记保存 Content-Type 用来指明使用何种编码,这样是很方便的。但是这个想法很疯狂:如果你不知道HTML文件使用的是什么编码,那你如何能读取它呢???但是值得庆幸的是,大部分常用的编码对于32-127之间的字符的编码规则都是一致的,所以你可以在读取整个HTML文件之间顺利的获取下面这段内容:

<html>

<head>

<meta http-equiv="Content-Type" content="text/html; charset=utf-8">

  因此网页中的 meta 标签要放置在 head 标签里而且是在整个文件的最上方,这样只要浏览器发现这个标记,它就会停下对 HTML 的解析,然后使用指定的编码对整个文件重新解析。

  如果浏览器在 http header 和 meta 标签里都没有找到 Content-Type它会如何处理呢?IE浏览器会做一些有意思的事情:它会基于各种各样的字节在不同语言的编码所表示的文本中出现的频率去猜测页面用的是什么语言和编码。因为各种各样的老旧的8bits的 code pages 总是把他们语言里的字符对应到128-255不同的范围里,而且不同语言里字符使用的频率是各具特色的,所以这种猜测还是有一定的成功的概率的。这虽然很奇怪,但是通常情况下一个完全不知道需要在页面放置 Content-Type 的网页开发者也能够看到他写的页面在浏览器里正常展示,直到有一天,他在页面里使用了大量的低频字符,然后 IE浏览器把他的页面解析成了韩语,所以坦白的说,Postel的“conservative in what you emit and liberal in what you accept”法则也并非那么适合工程师。不管如何,当一个用户看到保加利亚语编写的网页被展示成韩语的时候,他会做什么呢?他可能会使用 View|Encoding 菜单功能将网页更改成其他的编码,直到网页正常展示,但是大部分用户是不知道这么做的。

  我们有一个网站管理软件叫 CityDesk ,在它的最新版里,我们决定在代码内部使用 UCS-2(两个字节)的Unicode 来处理所有的字符串,Visual Basic, COM, and Windows NT/2000/XP 也都使用这种编码作为他们的字符串类型。在c++ 的代码里,我们使用 wchar_t(wide char)替代 “char”,使用 wcs 相关函数替代 str 的相关函数 (比如使用 wcscat 和 wcslen 而不是 strcat 或者 strlen)。在 C 语言里你可以在一个字符串前面加一个 L 来创建一个 UCS-2 字符串,像这样: L:"Hello"。

  当发布 CityDesk 网页的时候,会被转成 UTF-8 编码,浏览器在许多年里对UTF-8都支持的很好。这就是29个语言版本的 Joel on Software (作者自己的网站)是如何被编码的,而且我从未听过有一个用户在浏览这些网页的时候有什么显示的问题。

  这篇文章很长,但是也不可能涵盖 字符编码 和 Unicode 的所有一切知识,但是我相信如果你读到这里,对于编程来说你已经了解到了足够多的东西。最后,有一项任务交给你:使用抗生素代替水蛭和咒语(文章开篇作者举了一个例子: if you’re still programming that way, you’re not much better than a medical doctor who doesn’t believe in germs)。

--------The End


原文地址:

The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!)

上一篇下一篇

猜你喜欢

热点阅读