漫谈计算机编码

我们知道,在计算机内部,所有的信息都是以二进制形式进行存储。无论是字符,或是视频音频文件,最终都会对应到一串由 0 和 1 构成的数字串。所以从我们能看懂的人类信息转变为机器级别的二进制语言的过程就可以理解为一种编码的过程,自然,相反的过程就是所谓的解码的过程。

可以这么说,所有的乱码都是源于解码方式与编码方式的不一致。就好像我用英文给你写了一封信(我要表达的信息用英文这种方式 [编码] 了),而你只懂中文,你用中文去读信的内容(用中文 [解码]),于是整封信在你看来就是所谓的 [乱码]。其实,所谓的乱码不是什么复杂的问题,仅仅就是解码的方式不同于编码的方式而已,只要换成合适的解码方式就好了。

本文根据计算机编码的演变历史,从最早的 ASCLL 编码,到一统编码界的 Unicode 编码方式,探讨一下我们的 [人类信息] 究竟是如何被编码成 [计算机级信息] 的。

一、始终被兼容的 ASCLL

上个世纪中旬,美国人发明了计算机,当时并没有考虑到计算机的普及程度会如此之快,所以当时美国人只制定了英文字符和一些控制字符与二进制之间的映射标准,这个最初的标准就是 ASCLL 编码标准

ASCLL 首先对所有需要编码的字符进行了一个编号(总共编排了 128 个字符),例如:数字 0 的编号是 48,字母 a 的编号是 97 等。于是 ASCLL 使用一个字节(8 个比特位)来描述这些字符,将他们各自的编号的十进制转换成二进制即可。于是从 00000000 -- 01111111 (0-127)都被编排了字符。所以,所有采用 ASCLL 编码标准的文件在解析的时候,每八位二进制一起被解释成一个字符,这样所有的英文字符、数字、其他一些字符都已经可以被存储被读取了。下面附一张经典的 ASCLL 表:

image

可见,虽然一个字节只用了七个比特位,但是包含的字符还是相当多的,对于美国人来说,这完全足够用了,但是对于一些欧洲国家,乃至我们伟大的中国来说,一个字节实在是太少了,于是很多地区国家就有了自己的扩展编码标准,但无一例外的兼容 ASCLL 编码(毕竟人家是鼻祖)。

二、Windows-1252 来自欧洲人的扩展

美国人的 ASCLL 标准只定义了 128 个字符的编码方式,使用了 00000000 -- 01111111 这个区间段的二进制。于是欧洲人直接使用 10000000 -- 11111111(128-255)区间段的 127 个二进制位来定义他们自己的一些符号。

image

image

三、GB2312 来自中国人的扩展

我伟大的中华民族有着成千上万的汉字,美国人的一个字节的编码标准怎么能好使? GB2312 (国家标准编码)主要针对的是我们日常中经常使用的一些简体中文,总共收录 6763 个汉字,采用双字节编码,向前兼容 ASCLL 标准。

那么有一个问题,ASCLL 标准的字符采用的一个字节进行编码方式,而我们的中文汉字采用的两个字节进行编码,计算机在解码的时候究竟是一次读取一个字节并把它按照 ASCLL 标准解析成一个字符,还是一次读取两个字节并把它按照我们的 GB2312 标准解析成一个汉字呢?

GB2312 规定,编码汉字的两个字节中,第一个字节的最高位必须为 1。这样,由于 ASCLL 标准的所有字符(00000000-01111111),最高位都是 0,所以当计算机读取到某个字节的最高位为 1 的时候,就连着读取两个字节按照 GB2312 标准解析为一个汉字,否则则认为这是一个普通字符并按照 ASCLL 将它解析为一个普通字符。

下面我们简单描述一下 GB2312 的具体编码细节:

首先,GB2312 是通过所谓的 [分区] 来编排每一个汉字的。

  • 01-09 区编排了一些特殊字符
  • 16-55 区编排了一级常用汉字
  • 56-87 区编排了二级常用汉字
  • 00-15 区及 88-94 区则未有编码

GB2312 的编码方式:0xA0 + 区号,0xA0 + 位号。例如:[杨] 的区位号是 4978(49 区 78 位),所以杨的 GB2312 编码为:0xA0 + 49 ,0xA0 + 78 ,即:D1EE。所以以前有一种区位输入法,就是通过输入四位的数字来进行打字的,而这四位数字就是该汉字的区位号。至于为什么要在区号位号加 0xA0 ,查了很多资料,没有明确的说法,可能就是一种规定吧。

其实仔细想一下,所谓的编码过程不就是两个步骤的组合么,理解这一点很重要。

  • 使用唯一的一个标识编排表示该字符
  • 制定统一的规则将标识映射为底层二进制

ASCLL 标准如此,GB2312 也是如此。

例如:ASCLL 为所有字符进行编号,并且相互不重复(第一步),然后制定了一个规则,某个字符编号的二进制就是它的字符编码(第二步)。

例如:GB2312 为所有的汉字进行分区编号,相互不重复(第一步),然后制定规则使得可以通过区位号得到该汉字的二进制字符编码(第二步)。

GBK 向下兼容并扩展了 GB2312 ,收录了 21003 个汉字,依然是采用的固定两个字节来编码汉字,只是高位字节的取值范围不同而已,此处不再赘述。

四、有雄心的 Unicode

上面我们介绍了美国人的编码标准、欧洲人的编码标准、中国人的编码标准,当然这只是冰山一角,世界上存在着各种各样的编码标准。每个国家的计算机厂商都要根据不同的地域使用不同的编码标准来生产计算机,繁琐低效。有没有一种编码标准能收录世界上所有的字符,并提供存储实现呢?

Unicode 的诞生就是为了统一世界上所有编码的,它编排了世界上近乎所有的字符,总共收录将近 110 多万个字符集合,编号范围从 0x000000 到 0x10FFFF。但大多数字符在范围:0x0000 到 0xFFFF 之间(即小于 65536),每个字符都有一个 Unicode 编号并且一般用十六进制表示,前置 U+。例如:[杨] 的 Unicode 表示为:U+6768。

Unicode 是一种编码标准,它只是为世界上的所有字符进行了编号,并没有指定每个字符每个编号该如何映射为某个二进制串,而 Unicode 的主要实现者有:UTF-32,UTF-16 和 UTF-8。下面,我们分别来看看这些实现者的具体实现细节。

1、UTF-32

这是一种最粗暴的实现方式,采用固定四个字节存储单个字符,所有的字符都使用四个字节进行存储,空间浪费,实际使用中很少采用。

2、UTF-16

针对 Unicode 的存储实现来说,应当遵循一个基本的理念:越常用的字符应当使用越少的字节数表示,而越少见的字符才应该用最多的字节数进行表示。下面我们看看 UTF-16 的具体实现细节:

Unicode 的编码范围从 0x000000 - 0x10FFFF,总共可以编排 1,112,064 个字符。UTF-16 的策略是,编号范围 0x00000 - 0x10000(0-65532)属于常用字符,采用固定的两个字节存储。其中,字符所对应的二进制数值就是该字符本身编号的二进制字面量值。但是,其中 0xD800 到 0xDFFF 编号区间没有编排任何字符,这个区间将用于后续的增补字集编码,这里暂时先不说。

可见,对于常用的字符来说,采用两个字节进行编码,但是不常用不代表用不到,我们接着看看那些增补字符集,也就是所谓的不常用字符集是如何编码的。

对于编号范围 0x10000 - 0x10FFFF 之间的字符来说,UTF-16 使用固定的四个字节进行存储,但是你会发现 0x10000 - 0x10FFFF 之间总共有 FFFF 个字符,即 2^20=1,048,576 个字符,也就是需要 20 个比特位才能编码这么多字符。所以,我们的四个字节里,前两个字节共 16 位至少要提供 2^10(111...111,十个一)种可能,后两个字节也要提供 2^10 种可能,才能组合编排所有的增补字符集。

但是,现在有一个问题:一串二进制数值,我如何判断某个字符是常用字符(使用固定的两个字节存储的),或是增补字符(使用四个字节存储的)?

UTF-16 的解决办法如下:

每个 Unicode 字符都有一个自己的 Unicode 编号,并且对于增补字符来说,他们的编号都大于 0x10000 。用字符本身的编号减去 0x10000 即可得到该字符在所有增补字符集中的排列序号。这个序号的值必然位于范围:0x00000 - 0xFFFFF 之间,占 20 个比特位 ,因为剩下的增补字符数目不会超过 0xFFFF 个。

对于前 两个字节(维基百科上称做前导代理),定义他们的取值范围:0xD800(0xD800 + 0x0000)到 0xDBFF(0xD800 + 0x3FF [10 个 1]),刚好提供了 2^10 种可能取值。

对于后 两个字节(维基百科上称做后尾代理),同样定义了他们的取值范围:0xDC00(0xDC00 + 0x0000)到 0xDFFF(0xDC00 + 0x3FF [10 个 1]),也刚好提供了 2^10 种可能取值。

image

所以,如果发现前两个字节的二进制数值位于范围 0xD800 到 0xDBFF 之间,则说明这个字符属于增补字符并且在编码的时候采用四个字节固定存储了,依次读取四个字节即为当前字符的二进制数值。否则,则说明这是一个由两个固定字节存储的基本常用字符,依次读取两个字节就好了。

下面看几个示例:

1、Unicode 编号 U+0024 的字符

首先,判断得知该编号小于 0x10000,该字符隶属于普通常用字符集,所以该字符的 UTF-16 编码值就是其本身的编号二进制形式。

2、Unicode 编号 U+24B62 的字符

首先,判断该字符的编号值是大于 0x10000 的,说明该字符隶属于增补字符集。

于是,用 0x24B62 减去 0x10000 得到该字符在增补字符集中的排序:0x14B62

通过 UTF-16 编码标准,得到前导代理和后导代理,组合后就是该字符的 UTF-16 编码。以下是计算过程:

0x14B62 -> 0001 0100 1011 0110 0010

前导代理项:0001 0100 10 + 0xD800 = 0xD852

后尾代理项:11 0110 0010 + 0xDC00 = 0xDF62

所以,U+24B62 字符的 UTF-16 编码为:0xD852 DF62

总结一下 UTF-16 的编码标准,对于编号小于 65536 的字符,采用固定两个字节以编号的二进制作为编码的值。对于增补字符集(编号大于 65536),首先拿本身的 Unicode 编号减去 65536 得到当前字符在增补字符集中的排列序号,接着分出两个代理项并加上特定的数值,使得他们各自位于特定的范围中,并以此来区分某个字符究竟是两个字节存储的还是四个字节存储的。

3、UTF-8

UTF-8(8-bit Unicode Transformation Format),是一种针对 Unicode 的可变长度字符编码。使用一到四个字节来编码 Unicode 字符,最常用的字符使用最少的字节数进行存储,很少用的字符使用相对多一点的字节数进行存储。

UTF-8 的编码规则如下图所示:

image

对于编号小于 127 的字符来说,UTF-8 编码标准等同于 ASCLL 编码标准。

对于其余编号范围,按照如图中所示的格式进行编码,其他的也不多说了,现在我们通过一个示例来看看究竟是如何编码的。

汉字 [杨] 的 Unicode 编号是:0x6768 ,十进制:26472

显然,该汉字的 UTF-8 标准编码格式为:1110xxxx 10xxxxxx 10xxxxxx

0x6768 的二进制是:0110 0111 0110 1000

从这个二进制的最后一位开始,依次从后向前替换编码格式中的 [x] 即可。

image

显然,结果已经出来了,对应的十六进制代码为:0xE69DA8

总结一下,UTF-8 编码标准对所有 Unicode 编号进行了分类,排名越靠前,存储时使用的字节数目就越少。不同范围的 Unicode 编号字符集在进行 UTF-8 编码的时候会有不同的模板,以自己编号的二进制按照相应的规则去套模板,即可得到相对应的 UTF-8 编码。

相反的,指定了 UTF-8 编码的文件,计算机在进行解码的时候,以字节为最小单位。如果当前字节的最高位是 0,那么反向我们上述的几个步骤,可以得到该字符的 Unicode 编号二进制形式,继而查表可以得到该字符。

如果当前字节开头有多个一,那么有几个一,该字符的编码后的二进制数值就有几个字节,顺序读取即可。然后同样的反向操作,自然可以得到相对应的字符。

常见的几种编码方式就简单介绍到这,关于编码这块,始终要记得本篇中所总结过一个结论。所有的编码标准实际上都做了两件事情,第一件就是为所有需要编码的字符进行一个编号或标识,第二件就是指定一个规则统一得将这个编号或标识与二进制串进行一个映射。


文章中的所有代码、图片、文件都云存储在我的 GitHub 上:

(https://github.com/SingleYam/overview_java)

欢迎关注微信公众号:扑在代码上的高尔基,所有文章都将同步在公众号上。

image
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,123评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,031评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,723评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,357评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,412评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,760评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,904评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,672评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,118评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,456评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,599评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,264评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,857评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,731评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,956评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,286评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,465评论 2 348

推荐阅读更多精彩内容

  • 乱码是我们在日常的工作中经常遇到的问题,你可能从网上好不容易下载了一个炫酷的jQuery插件,但是却在打开的时候,...
    Single_YAM阅读 944评论 0 4
  • 创业对于我来说总是很诱人,总是充满激情,然而这些都只能说明我年轻! 经商自有经商知道,这对于我来说是一门特殊,熟悉...
    6eec5a602b5c阅读 354评论 0 0
  • 子曰:“不患人之不己知,患不知人也。” 先生说:“不要愁别人不知我,该愁我不知人。” 学而篇最后一章.永远保持一颗...
    一棵树2016阅读 170评论 0 1
  • 工作后打车次数开始多了起来,碰到过形形色色的司机。有一点是我在接触他们之后万万没想到的,他们中的大部分人都不用地图...
    小熊猫在成长阅读 1,805评论 0 1