unicode与字符编码

By | 11/09/2013
这篇文章写的

    我想我已经弄明白了这些问题,但要表述清楚却多花费我好几倍的时间。

    这里的大部分内容都是从其他博客引用而来。针对python,我另外写了一篇 python中的编码

    先定义几个基本概念,概念总是如此的平实而高深:(,在看完整篇博文之后概念才会显得清晰:

  • 字符集:每个符号的二进制代码的集合。

如,汉字“严”的unicode是一个十六进制数0X4E25。
又如,“Hello”用unicode表示为

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

  • 编码/解码:如何从内存中读取二进制代码并转换为字符,以及字符在内存中如何存储。

如,“Hello”

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

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

(这只是特例,许多汉字并不仅仅只有两个字节存储,所以编解码不仅仅是大/小端的问题)

ASCII

        自unix诞生之日,代码世界中只有英文字母,我们就称它为ASCII吧。它使用值为32~127之间的数字代表字符。比如“32”代表空格符,“65”代表“A”等。

         随着Internet的普及,不同国家的人需要在邮件/程序中表达特定的字符。这个时候出现了诸侯间的128~255之间的字符大战。各国对于128~255之间的字符有着不同的想法,导致例如130可能在某些IBM PC(就拿美国做例子吧)上表示é,而在以色列的PC中则表示e。也就是说一个美国人发了一封résumés到以色列会变成resumes。

        除了128~255之间的字符之争,不同国家的语言中包含奇奇怪怪的字符,使用ASCII的8-bit(256)试图容纳全世界所有的字符也是不可能的。

       在亚洲许多国家,即便使用256个字符也不能表达亚洲语言中字符串的万分之一!即便排除某些不能正常/完全访问Internet的国家,256个字符也完全不够用!

Unicode

        这个时候催生了Unicode字符集的发明。

        时刻记住,Unicode与ASCII一样,仅仅是一种特定的字符集。但“Unicode不得不说是项伟大的发明”。

        很多人误认为Unicode只是简单的16位编码的字符,每个字符使用2个字节表示,因此Unicode总共有能表示65536个字符。

         呃,这并不完全正确。

         这也是关于Unicode最神秘的地方。如果你也这样认为,不需要觉得自己过于糟糕。

         严格来说,unicode使用4个字节(32-bit)来编码,总共能代表2^32个字符(我的计算器溢出了:))。

         实际的情况是, Unicode的字符,其二进制编码不一定需要四个字节,像所有的英文字母,只需要一个字节就绰绰有余(这部分集合与ASCII完全相同,这也可以体现unicode的高明之处。Unicode这个超大字符集与ASCII这个古老的字符集的交集在于从0~127这128个字符完全相同。)

         Unicode是一个很大的字符集,可以在unicode网站查到某个特定的字符对应的unicode。

编码/解码 与 UTF-8

        问题又来了,既然我们已经有了一个伟大的集合,可以包容目前存在的所有字符,为什么不直接在计算机间使用unicode进行传输呢?这样所有人收到的字符都表示相同的含义,一就是一,二就是二。
         还得回到那个各国128~255字符的争夺之战中。假设由创造整个宇宙的斯密达国度与创造斯密达国度的天朝组成统一战线,对抗万恶的资本主义。在战争一触即发的情形下,斯密达首领发了封邮件给天朝将军,”129″(特定的字符,在ASCII中表示“开打吗?”)。天朝人民眼看国内生产还没有发展起来,回复了斯密达,说”250“(特定的字符,在天朝中表示“不打”)。斯密达将领一看,”250“在我大斯密达不就是”打“的意思吗?结果就悲了个剧了。
        到这,编码方式已经隐含在其中了。假设天朝将领与斯密达首领都使用中文(同一套标准)那么双方表达就不会出现歧义。
        再对编码进行细说,”129″(开打吗?)经过存储在邮件中并不一定是“129”,而是“0x1234”(经过我本人特定的编码),而“250”存储在邮件中也不一定是”250″,而是“0x0973”(同样,经过我本人特定的编码)。编码过程就是如何将“250”存储到邮件(内icun)中,而解码过程则是如何从邮件(内存)中解释”0x0973″的含义的过程。

        编解码的意义在于,若发送端和接收端认同同一套编码方式,那么:发送端以这套编码方式编码并发送,接收端以这编码方式解码,就能得到同一个字符,使用万能的unicode表一查,就知道这个字符想表达什么意思了。
由此,我们可以断定,“UTF-8”只是Unicode的一种编码方式,只是Unicode的实现的一种子集。

附1:

UTF-8又是如何编码的呢?

UTF-8最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。
UTF-8的编码规则很简单,只有二条:
1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。
2)对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。
下表总结了编码规则,字母x表示可用编码的位。
Unicode符号范围 | UTF-8编码方式
(十六进制) | (二进制)
——————–+———————————————
0000 0000-0000 007F | 0xxxxxxx
0000 0080-0000 07FF | 110xxxxx 10xxxxxx
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
跟据上表,解读UTF-8编码非常简单。如果一个字节的第一位是0,则这个字节单独就是一个字符;如果第一位是1,则连续有多少个1,就表示当前字符占用多少个字节。
下面,还是以汉字”严”为例,演示如何实现UTF-8编码。
已知”严”的unicode是4E25(100111000100101),根据上表,可以发现4E25处在第三行的范围内(0000 0800-0000 FFFF),因此”严”的UTF-8编码需要三个字节,即格式是”1110xxxx 10xxxxxx 10xxxxxx”。然后,从”严”的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,”严”的UTF-8编码 是”11100100 10111000 10100101″,转换成十六进制就是E4B8A5。

附2:

Big/Little Endian
端模式(Endian)的这个词出自Jonathan Swift书写的《格列佛游记》。这本书根据将鸡蛋敲开的方法不同将所有的人分为两类,从圆头开始将鸡蛋敲开的人被归为Big Endian,从尖头开始将鸡蛋敲开的人被归为Littile Endian。小人国的内战就源于吃鸡蛋时是究竟从大头(Big-Endian)敲开还是从小头(Little-Endian)敲开。在计算机业Big Endian和Little Endian也几乎引起一场战争。
在计算机业界,Endian表示数据在存储器中的存放顺序。在考虑endian之前,首先了解对于一个32位整型0x12345678,12表示MSB(Most Significant Byte),78表示LSB(Least Significant Byte)。端模式之争也就是MSB和LSB的顺序问题。
那么,计算机如何知道某个文件是采用大端还是小端的编码方式呢?
Unicode规范中定义,每一个字符串的最前面分别加入一个表示编码顺序的字符,这个字符的名字叫做”零宽度非换行空格”(ZERO WIDTH NO-BREAK SPACE),用”FE FF”表示。这正好是两个字节,而且”FF”比”FE“大1。
对于文本而言,如果一个文本文件的头两个字节是”FE FF”,就表示该文件采用大端方式;如果头两个字节是”FF FE”,就表示该文件采用小端方式。

Reference:
1. joel on unicode
2. ascii/unicode和utf-8
3. ASCII
4. ASCII速查表

发表评论

电子邮件地址不会被公开。 必填项已用*标注