乐趣区

关于unicode:每个开发必须了解的Unicode和字符集的那些事

你已经对神秘的 Content-Type 标签感到好奇吗?就是那个在 HTML 中常常用到然而很少有人理解为什么要去应用它的标签。

你已经收到过一封来自保加利亚的敌人发给你的邮件,邮件的题目是“???? ?????? ??? ????”?

我很悲观的发现有十分多的软件开发者并不理解字符集,编码,unicode 等相干的常识。几年前,FogBUGZ 网站的一个测试人员想要晓得它是否可能胜利接管来自日本的邮件。日本?日本也要用这个邮件系统?我一头雾水。在认真钻研用来解析 MIME 邮件音讯的商业 ActiveX 控制器后,发现它解析字符集的形式是齐全谬误的,所以咱们不得不大胆的写一些代码来纠正错误的转化使其正确解析。看了其余的商业化代码库之后,发现它们的字符解析实现也十分的简陋。我分割了那个库的开发者,他们的态度是“咱们啥都做不了”。和很多程序员一样,他心愿这件事件能够就这么过来了。

然而显然这个问题不能就这么算了。当我发现 PHP 这个如此风行的 Web 开发工具都简直齐全忽视了字符编码的问题, 随便的用着 8 位存储的字符,使得简直无奈用其开发国际化网页利用。我感觉真的够了,再也忍不了了

所以在此我要郑重声明:如果你当初是一名程序员却不理解字符,字符集,编码和 Unicode 的基础知识,一旦被我发现,我就要罚你到深海潜水艇上寂寞的剥 6 个月的洋葱!

我还要说一点,这个问题并没有设想中的那么难!

这篇文章我会聊一些每一个程序员所必须晓得的内容。什么“plain text = ascii = 8 位自符”这些货色几乎是大错特错。如果你还用那种思路编程,就好像是一个不置信细菌存在的外科医生。请在浏览完本文之后再去持续你的编码生涯。

在开始之前,我要揭示那些极少数理解国际化编程的同学,你们会发现这篇文章的内容有些适度简化。因为我只分享了最根底的内容,从而让每一个人可能了解并且试着写出一个非英语环境下都可能正确运行的程序。我还要申明,正确的字符编码只是国际化程序可能良好运行的一个很小的前提,但这次不扩大范围,先只聊这件事。

历史的视角

理解这个问题最好的形式就是沿着工夫线追溯。

你可能认为我要说一说十分古老的字符集 EBCDIC,然而我不~EBCDIC 曾经和咱们当初的编码无关了,咱们不须要追溯那么远的历史。

在上古期间,当 Unix 刚刚被创造进去,K&R 还在写 C 语言的时候,一切都是那么的简略。EBCDIC 刚刚被淘汰出局,咱们只须要关注一种字符类型,那就是英文字母。咱们应用了一种叫做 ASCII 的编码方式,通过 32 和 127 之间的数字来示意任意一个字符。比方 Space 的编码是 32,A 的编码是 65。这种编码能够用 7 位轻松存储。那个年代大多数的电脑都应用 8 位字节,因而咱们不仅能够存储每个 ASCII 码字符,还有一个闲暇位来反对一些控制指令,比方 7 能够示意让电脑告警,12 能够命令打印机的当前页移出并引入新的纸张。

所有看上去是那么美妙,前提是你是一个英文开发者。

因为一个字节有 8 位而 ASCII 编码只用了其中的 7 位,很多人都开始想,“诶哟,咱们能够自定义 128~255 这个区间所代表的字符”。问题是,过后很多人 同时 产生了这个想法,并且创造了各式各样的自定义编码映射。IBM 电脑提出了一个称为 OEM 的字符集,其中蕴含了一些欧洲语言中带有音调的字符和一些绘图式字符… 比方水平线,垂直线,带有小箭头的水平线等等。你能够用这些线状字符在屏幕上绘制出精美的盒子形态图形,直到现在还能在一些装有 8088 芯片的洗衣机上看到这些图形。事实上,随着美国之外的人们开始买电脑,各种各样的字符集应运而生,各自都有着不同的含意。比方,在一些电脑上 130 编码代表é,然而在一些以色列售卖的电脑上却是希伯来语 Gimel()。所以当美国人将 résumés 发送到以色列,它将被翻译成 r sum。甚至是一个国家内,比方俄罗斯,对于 128 位以上的字符都有很多不同的映射,所以同一份俄语文件都可能被解释成不同的内容。

最终,这些随便的 OEM 编码们在 ANSI 规范中得以扭转。在 ANSI 规范中,每个人对于 128 以下的编码内容达成统一,这部分根本和 ASCII 编码,然而对于 128 以上的编码映射在不同的地区有不同的解决形式。这些不同的区域编码零碎被称为_编码页_。比方在以色列的 DOS 零碎中用的编号 862 的编码页,而希腊用户应用编号 737 的编码页。这些编码页在 128 以下的内容雷同,然而在 128 位以上的字符就形形色色了。MS-DOS 的国内版本有几十个这样的编码页,用于解决各种各样的语言,甚至有一些编码也可能同时反对多种语言!然而,换句话说,要想用一个编码页在一台电脑上同时反对希伯来语和希腊语是不可能的,除非写一个自定义的程序来展现位图图形,因为希伯来语和希腊语须要应用不同的编码页来翻译高位的编码。

于此同时,在亚洲,编码变得更加疯狂,因为亚洲的语言通常有上千个字母,根本无法只用 8 位来示意这些字母。这个问题通常用一个叫做 DBCS(double byte character set)的很蹩脚的零碎来解决,这个零碎中局部字符用一字节来示意,一些用两字节来示意。这样的设计使得在 string 中从前往后遍历很轻松,然而简直不可能从后往前遍历。程序员通常被倡议不要应用 s ++ 或者 s – 来前移或后移,而是调用函数如 Windows 的 AnsiNext 和 AnsiPrev,让操作系统决定如何解决这些字符。

即便如此,很多人仍然认为一个字节就是一个字符,一个字符是 8 位。只有不将这个字符串挪动到另一台电脑上,或者这个字符串不波及别的语言,这所有都看上去很失常。然而,随着国际化趋势,将字符串挪动到另一台电脑变成了一件很常见的事件,于是所有开始崩塌。幸好,Unicode 随之问世了。

Unicode

Unicode 做了一个大胆的尝试,它创立了一个字符集编码将这个星球上所有的正当的或是假造的(如 Klingon)语言都囊括进来。有些人误以为 Unicode 就是一种长度为 16 位的编码,每 16 位代表一个自负,因而一共有 65,536 中可能的字符。这个了解不完全正确。这也是对于 Unicode 最常见的误会。所以如果你也是这么认为的,不必感觉丧气。

事实上,Unicode 用一种全新的形式来翻译字符。试着用它的形式来思考才可能真正明确 Unicode 的编码方式。

当初,咱们假如一个字母被映射成一些二进制位从而可能存储到磁盘或者内存中:
A -> 0100 0001

在 Unicode 中,一个字母映射到一个称为代码点(code point)的货色,这依然只是一个实践上的概念。至于这个代码点是如何在内存或者磁盘上示意的就是另一个问题了。

在 Unicode 中,A 这个字母是一个理想化的符号。这个理想化的 A 不等于 B,也不等于 a,然而和 不同模式的_A_ 和 A 却是雷同的。在一种字体下的 A 和另一种字体下的 A 被认为是一个符号,然而和小写的 a 相比就是不同的符号。这看上去没什么争议,然而在一些语言中明确一个字符到底是什么就会产生争议。比方德语字母ß到底是一个理想化的符号还是只是用来表白 ss 的简写?如果一个字母的在单词开端时形态扭转了,那它是否是另一个字母?希伯来语对这个问题的答复是必定的,然而阿拉伯语却不是。总而言之,那些创造 Unicode 的聪明人儿在过来十年将这个问题想明确了,尽管随同这很多高度政治化的争执,然而他们究竟还是梳理分明了。

每一个现实符号都被调配了一个相似于 U+0639 的魔法值。这个魔法值被成为代码点(code point)。U+ 代表是 Unicode 编码,前面紧跟着十六进制的数字。U+0639代表阿拉伯字母 Ain,而英文字母 A 则是 U+0041。你能够在 Windows 2000/XP 的charmap 工具或者 Unicode 网站上查看全副的编码信息。

Unicode 可能定义的字母数量其实没有下限,它们早就超过了 65,536 个字母,所以并不是每个 Unicode 字母都可能被压缩进两个字节,这个问题到本文目前为止还是一个谜。

好了,假如咱们当初又一个字符串Hello,在 Unicode 中对应这么 5 个代码点U+0048 U+0065 U+006C U+006C U+006F。至于这些代码点将如何在内存中存储或者在邮件中展现,咱们还没有做介绍。

编码

接着就要聊一聊编码了。
晚期 Unicode 的编码采纳了两个字节来存储,所以 Hello 这个单词被编码成00 48 00 65 00 6C 00 6C 00 6F。看上去还不错~等下,那是不是也能够被编码成48 00 65 00 6C 00 6C 00 6F 00。事实上这么编码也不是不能够,而晚期的开发者心愿可能依据具体的 CPU 架构来抉择是采纳高位模式还是低位模式来进行存储。所以人们不得不遵循一种奇怪的约定,在每个 Unicode 字符串前存储一个 FE EF 前缀,这个前缀被称为 Unicode 字节程序标记位(Unicode Byte Order Mark)。而如果你将字符串的高下位对换地位后,你就须要加上 FF FE 前缀,从而让阅读者晓得这里须要做一次替换。然而,并不是每一个 Unicode 字符串的结尾都有字节程序标记位的。

这样一度看起来很不错,然而有些程序员开始埋怨了。“嘿!看这一大串零!”,因为这些人是美国人,而英文很少会用到 U+00FF 以上的编码。这意味着这些零导致的双倍的存储空间。而且当初曾经有了那么基于 ANSI 和 DBCS 字符集编码的文档,谁来将他们转换成 Unicode 编码。因而很长一段时间大多数人都忽视了 Unicode 编码,而于此同时,编码不对立带来的问题开始变得越发重大。

因而 UTF- 8 随之诞生。UTF- 8 是另一个应用 8 比特位将 Unicode 代码点的字符串(那些神奇的 U + 数字)存储在内存中的零碎。在 UTF- 8 中,每个 0 -127 之间的代码点用一个字节来存储,只有 128 及以上的用 2,3 个甚至 6 个字节来存储。

这种设计最大的益处就是英文的编码和 ASCII 编码一摸一样,所以美国人简直不会发现有什么区别,而其它国家则气的跳脚。比方Hello,原本应该是 U+0048 U+0065 U+006C U+006C U+006F,会被存储成48 65 6C 6C 6F。就和 ASCII,ANSI 和任何 OEM 字符集编码产生的内容一样。当初,如果你大胆的应用一些其余国家的语言如希腊字母或克林贡字母,你就须要用额定的字节来存储一个代码位。(UTF- 8 还具备一个不错的属性,即那些应用单个 0 字节作为空终止符的老旧字符串解决 UTF- 8 代码不会截断字符串)

目前为止我曾经通知你 Unicode 编码的三种形式,传统的那种全副用两个字节存储的办法叫做 UCS-2(因为它由两个字节形成)或者 UTF-16(因为它有 16 位),然而你仍然须要辨别是高位的 UCS- 2 或者是低位的 UCS-2。还有就是比拟风行的 UTF- 8 规范,能够同时兼容英语字母的历史编码和其它语种的编码。

还有一些别的 Unicode 编码方式,比方有一个叫做 UTF-7,它和 UTF- 8 很相似,然而它确保高位永远都是 0. 所以如果你想要将 Unicode 在某些邮件系统中传递,而 7 位的长度曾经足够,那么这种编码可能提供很好的压缩。还有 UCS-4,它用 4 个字节来存储每个代码点,因而每个代码点编码后都是等长的。然而很少有人可能承受这样的存储空间节约。

当初当你再看看这些用 Unicode 代码点示意的每一个现实字符,这些 Unicode 代码点能够用任何一种老式的编码工具进行编码。比方你可能将 Hello 这个 Unicode 字符串用 ASCII 或者老式的希腊 OEM,或者希伯来 ANSI 进行,或者上百种现有的编码方式进行编码。然而可能有一个问题,一些字母可能展现不进去。如果 Unicode 的代码点在以后的编码集中没有对应的字符,它可能会变成一个小小的问号?

大多数的传统编码只能正确的存储局部代码点,而其余的代码点会被翻译成问号。一些比拟风行的英文文本编码如 Windows-1252,ISO-8859-1,当你是这用这些编码来翻译俄文或者希伯来文时,你会生成一大堆问号。UTF 7, 8, 16, 和 32 都可能正确的存储任何的代码点。

对于编码必须晓得的最重要的一点

如果你曾经忘了我刚刚说的所有,请至多记住最重要的一点。当你拿到一个字符串却不晓得它的编码的话,这个字符串实质上毫无意义。你不能在把脑袋埋在沙堆里伪装它默认是 ASCII 编码。这世界上不存在默认编码这回事!

如果你在内存、文件或者邮件中有一个字符串,你必须晓得它的编码格局,否则你无奈正确的翻译或展现它。

简直每一个愚昧的问题,如“我的网站看上去在胡说八道”或者“我应用方言的时候她看不懂我的邮件”,都来自于一个不懂这个简略情理的天真的程序员。如果不通知你这个字符串是用 UTF-8 还是 ASCII 还是 ISO 8859-1 (Latin 1)还是 Windows 1252 编码的,你基本没法正确的展现它,或者是找到这个句子的结束符。这世界上有上百种编码,猜想 127 之上的编码方式就是一种徒劳。

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

对于一个网页,最后的想法是 web 服务端返回一个相似 Content-Type 的 HTTP 申请头和相应的网页。也就是说不是 HTML 网页自身携带 Content-Type 定义,而是让申请头来标记这个网页的编码。然而这种形式带来了一些问题。如果你领有一个大型的 web 网站和大量的网页,这些网页由来自各个国家的人用不同的语言参加开发,并且应用了开发工具举荐的各种各样不同的编码。web 服务器本人都不晓得每个文件具体的编码模式,因而它无奈确定 Content-Type 头的内容。

相比而言,间接将 HTML 文件的 Content-Type 用非凡的标签保留在 HTML 注释中就显得更加不便一些。当然这可能让一些谋求极致的人抓狂 … 你怎么能在解析了 HTML 后才晓得具体的编码格局呢?幸好,简直每一种编码在 32 和 127 之前的实现是根本相似的,所以你能够在解析如下的 HTML 的时候失去正确的内容:

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

然而这个 meta 标签肯定要放在 <head> 标签中的第一位,因为 web 浏览器一旦读取到这个标签就会暂停解析页面,并应用指定的编码从新翻译。

如果 web 浏览器没有在 http 报文头或者 meta 标签中找到 Content-Type 信息怎么解决?IE 浏览器会做一件很乏味的事件:它会基于以后不同字符呈现的频率来猜想应用的语言和编码。因为不同的语言对于字符有不同的应用法则,这个性能还真的有肯定的可用性。这也是为什么一些天真的网页开发人员发现即便不退出 Content-Type 标签,网页看上去也很失常,直到有一天他们编写了一个不遵循他们母语应用法则的网页,而 IE 判断出这是一个韩国网页并依照相应的编码进行解析。这也证实了伯斯塔尔法令所说的“承受多变,输入激进”并不是一条很好的软件工程法令。总之,那些可怜的网站用户在看到本应该是保加利亚语编写的网页被翻译成韩语(甚至不是连贯的韩语)时会怎么办?他可能会应用 View | Encoding 工具并尝试一系列不同的编码,直到生成一个看上去失常的后果页。前提是他晓得浏览器有这么一个工具,而其实大多数人都不晓得这个性能。

退出移动版