北京字典价格联盟

前端历史课:那些来自洪荒时代的编码知识

只看楼主 收藏 回复
  • - -
楼主

(点击上方公众号,可快速关注)


作者:伯乐在线专栏作者 - skiner

欢迎投稿,请点击这里查看详情;

如需转载,发送「转载」二字查看说明


何为编码? 这可能要从一个励志的文字游戏说起。


规则: 规定 A,B 两人之间只能用数字进行沟通, 把 “我爱你” 传递给对方。


于是 A 想出了一个主意,他去书店买了两本一样的新华字典,然后把 “我” “爱” “你” 三个字查出来, 假设分别是在第 672 页的第 3 个字, 和第 102 页的第 6 个字, 和第 378 页的第 1 个字。


于是 A 将这些页码信息写在一起, 即:672310263781


B 接到了这一串数字, 然后通过 A 送的字典, 一个个的将这些字还原了回来, B 感动极了。于是拒绝了 A。


计算机, 数字, 字


众所周知,计算机中只能流转数字, 那我们看到的文字又是怎么显示出来的呢?


通过上面的故事, 我想大家已经找到答案了。


其实在计算机内都有一本虚拟的 “字典” 它记录着 “数字” 与 “自然字” 之间的映射关系。


那么这本 “字典” 是怎么做出来的呢?


早在 196x 年 美帝几个任性的科学家决定用 8 bit (1B) 来表示一个 “自然字” , 当然 1B 的容量限制了这些字的种类数量不能超过 2 的 8 次方(256)个。 不过好在英文字母一共就 26 个。大小写外加标点算起来一共也就百来个(小于 128 个, 其实 7 bit 就足够了), 所以就当时的情况来说, 1B 的空间是绝对够用的, 就这样 256 个空间中的的前 128 个很快就被分配完毕了, 他们的映射关系是:


0 – 31 以及 127 分给了一些有特殊身份的功能性”字符”(如换行 换页 震动 移至行首 等等)


32 – 126 分给了字符(其中 48-57 为数字, 65-90 大写, 97-122 小写,其余标点)


这套映射关系就是后来一直延续到今天的 ASCII 码。(American Standard Code for Information Interchange 美国信息交换标准代码)


标准 ASCII 码是国际标准, 它只占用了 1 个字节的前 7 位,并预留一位做扩展,或作奇偶校验。


这里有一份 标准ASCII表 可以查询详细的对应关系 , 当然 linux 下也可以 man 一下 ascii。


(标准 ASCII 表)


(ASCII 扩展表)


奇偶校验:


简单来说, 奇偶校验是一种检测数据传输时正确性的方法,它分为 奇校验 和 偶校验两种, 分别检测二进制数据中 “1” 的个数是奇数或偶数。 并将校验结果位追加到数据之后。


1。奇校验:


如果数据中的 “1” 的个数是奇数,那么校验位为 0 否则为 1。


例如: 1001000 => 奇校验 1001000+1 , 1001001 => 奇校验 1001001+0


2。偶校验:


如果数据中的 “1” 的个数是偶数,那么校验位为 0 否则为 1。


例如: 1001000 => 偶校验 1001000+0 , 1001101 => 偶校验 1001101+1


奇偶校验只能检查错误,但并不能纠正错误。


各国间混乱的编码


过了一段时间,人们发现这 128 个字的不够用了,于是将手伸向了 127 号后面的区域。


前文说了,ASCII 中只有前7位(128个字)是国际标准, 而后边 128 个坑并没有做出规定或限制, 也没有留下任何使用说明书。


于是,轮到各个国家与公司开脑洞的时间了。


这后 128 个坑位称为 ASCII 扩展码, 它不是国际标准。也不可能是国际标准,因为每个国家都有自己的需求,但坑位又只有 128 个, 不来一场世界大战才怪。 其中用的比较广的可能要算是 IBM扩展字符集 了,也就是 windows 使用的 cp437 字符编码。


DBCS 双字节字符集


计算机进入我国的时间比较晚, 但汉字作为一种非拼音文字却是一个非常难处理问题, 已知汉字总共有 8 万多个, 即便常用的也有 3500 个。 这对于编码空间来讲是一个巨大的考验。


这个难题我想一定是东北人解决的。 因为。


东北第一定律: “世界上没有什么事是一顿烧烤不能解决的。如果有,那就两顿”



如果一个 ASCII 不能解决, 那就两个 !!!


实践结果证明这种做法确实行之有效,并一直保留到了今天。


为了深入了解双字节字符集的概念,下面我们就举例说说我国的这几个常见编码集。


GB 系列: GB2312、GB13000、 GBK、GB18030


GB2312:


GB2312 是最早(1980年)发布的中国国家标准字符集 ,是一个典型的双字节字符集(DBCS)。 他规定了两个连续大于 127 的字节代表一个汉字, 否则就当做 ASCII 来处理, 这样的两字节中高位字节范围 0xA1-0xF7, 低位字节范围 0xA1-0xFE。 二者相乘多出来 8000 多个坑位, 已经足够容纳常用汉字了, 除此之外还增加了数学符号、罗马希腊字母、日文假名等。 这种规则完全兼容 ASCII , 但为了更好的兼容 CP437 也做了很多空间牺牲, 比如 GB2312 的低位也被要求大于 127, 所以 GB2312 的编码空间并不大。


不幸的是当时我国一位重要的名字中, 有一个字是 GB2312 里没有的。 (只能用 钅容)


汉卡:



早期的计算机运算能力非常弱, 而 GB2312 的编码规则无形中又给解码增加了额外负担, 所以为了提高汉字的输入性能, 一种叫 “汉卡” 的硬件出现了, 他直接负责 GB2312 的编码解码。 为 CPU 分担任务。


但作为一款硬件, 它的流行又使得 GB13000 推行不下去。


GB13000:


总结了 GB2312 的种种缺陷, 我国国家标准化委员会又制定了 GB13000, 虽然它也是双字节字符集,但与 GB2312 完全不同,他参照了 Unicode 标准, 也就是说 GB13000 是面向国际化的, 但这也直接导致了他不兼容 GB2312 。 真金白银买的汉卡没法用了。 这怎么行 ?


所以 GB13000 沦为了 “纸面上的标准”。


至于 GB13000 的编码规则这里就不做介绍了, 可以直接参考下文 unicode。


GBK:


鉴于我国汉卡的流行情况, 我国有关部门又制定了 GBK 标准,它兼容 GB2312, 同时又新增了 2w 多个坑位。


它是怎么做到的呢?


GBK 放开了一些范围, 相比 GB2312 , GBK 的高位字节扩展到了 0×81-0xFE, 低位字节扩展到了 0x40-0xFE (不包含0x7F) 编码方式和 GB2312 相同。


由此可见 GBK 是 GB2312 的一个超集, 且和子集兼容良好。


GB18030:


GB18030 是目前我国最新的内码字集, 它和其他 GB 开头的小伙伴最大的区别就是,它是变长的编码集。


所谓变长是指一个 GB18030 字有可能由 1 个、2 个或 4 个字节组成。 与 UTF 系列类似。


它与 GBK , GB2312 , ASCII 都是兼容的。


1 字节的值范围: 0 到 0x7F,与 ASCII 兼容。


2 字节的值范围: 高位 0x81 到 0xFE。 低位 0x40 到 0xFE 与 GBK 标准兼容。


4 字节的值范围: 一字节 0x81 到 0xFE,二字节 0x30 到 0x39,三字节 0x81 到 0xFE,四字节从 0x30 到 0x39


GB18030 可容纳的编码范围巨大, 基本上亚洲所有的少数民族的文字都包含在内了。


可以看出在这 GB 几兄弟中 GB2312 => GBK => GB18030 被依次兼容。


走向世界


上文中的 GB 几兄弟 虽然很好的解决的汉字编码问题,但却不能走向国际舞台,因为无论 GB2312 还是 GBK 它们都是一种 “地区性DBCS”。 这导致了一个非常麻烦的问题,就是同一个文本文件可能因为编码集问题而在国外计算机中根本无法阅读。


看来这个世界需要一个统一的字符集。


于是 ISO (国际标谁化组织) 开始着手解决这个问题。 它发起了 ISO 10646 项目, 名为 “Universal Multiple Octet Coded Character Set”, 简称 UCS。 方案中废弃了所有地方性的 DBCS 方案,并尝试把它们融合在一个 DBCS 方案中。


同时 统一码联盟 Unicode (由各个大型企业及组织共同维护) 发布了统一码(Unicode)项目。


起初,UCS 和 unicode 并不兼容, 但这俩项目本身就是为了解决冲突而生,搞出两套有何意义? 后来 unicode 便与 UCS 统一了。目前的 unicode 普遍采用的是 UCS-2


地方性 DBCS 存在一个共有的问题,就是它们为了兼容 ASCII , 0-127 是不能当做高位来使用的。 这使得一大块编码区域被浪费掉, unicode 为了解放这一区域,规定所有字符都必须用两字节来表示(即使是 ASCII 字符),这样这种 DBCS 的容量就被大大扩充了。


在这种规则下 2 字节共可以产生 65536 个坑位。 不同国家的字符分别占据这 6w+ 坑位中的某一段。 如 希腊字母表使用从0x0370到0x03FF的代码,斯拉夫语使用从0x0400到0x04FF的代码, 美国使用从0x0530到0x058F的代码,希伯来语使用从0x0590到0x05FF的代码。中国、日本和韩国(总称为CJK)使用从0x3000到0x9FFF的代码(还是我们占的比较多 (⊙﹏⊙)b)。


这些坑位目前还有一些空余,一般使用 FFFD 占据,但在不同软件或操作系统中可能会略有不同。


比如 这个字 (u3237) 在 sublime Text 中会显示成 


节约光荣,浪费可耻


虽然 unicode 帮我们解决了字符集统一的问题,但这并不是没代价的。 在 unicode 下所有的字符都是用 2(UCS2) 或 4(UCS4) 个字节来编码,甚至是 ASCII 字符。 这样便造成了大量浪费,尤其是在进行网络传输的时候。 (当你走进联通营业厅后会发现, 流量啊 流量。 你可是真金白银啊。)


为了解决这一问题, 科学家们定义了一种基于 unicode 的便于传输的编码格式 UTF(UCS Transfer Format), UTF 也有 UTF-8 UTF-16 UTF-32 这几个兄弟 横杠后的数字代表尝试将单个 unicode 压缩成几 bit。


比如 UTF-8 它就将尝试用 8 bit 来表示一个 unicode 字。


那么用 8 bit(1字节) 来表示 16bit(2字节)。 肯定会遇到撑不下的情况,怎么办?


Unicode – UTF8


十六进制二进制
0000 0000-0000 007F0xxxxxxx
0000 0080-0000 07FF110xxxxx 10xxxxxx
0000 0800-0000 FFFF1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF11110xxx 10xxxxxx 10xxxxxx 10xxxxxx


上图中的 x 为 UTF-8 的可编码范围。


它的转换规则如下:


1。 如果某字节第一位是 0 ,那么判定为 ASCII 字节,除了 0 外余下的 7 位是 ASCII 码。所以 UTF-8 是兼容 ASCII 码的。 2。 如果第一个字节是 1 , 那么连续的几个 “1” 代表了从这个字符开始,后面连续的几个字符其实是一个字位。 且后面的位置都要是用 10 开头的。


上图,上图,上图



UTF-16 与上面类似。它尝试用 2 字节来表示一个 unicode 字,在 UTF-16 下如果表示 UCS2 那么 2 字节基本足矣。 所以它主要是用来压缩 UCS4 的 当 2 字节搞不定时,就会尝试使用更多字节,实现原理和 UTF-8 类似。


现在 windows 下的 unicode 实现大多使用的都是 utf-16


汉字的窘境


到目前的为止, unicode 默认采用的是 UCS2, 而传输则是 UTF-8, 对于 ASCII 字符, 在 UTF-8 下体积仍然是 1 字节。 但是汉字由于编码段比较靠后, 用 UTF-8 就比较吃亏,基本都需要用 3 个 或 4 个字节来表示。这反而变得比 unicode 本身还大。


所以。 对于 ASCII 字比较多的文章, UTF-8 压缩效率明显, 基本可以降低一半体积。 但是对于汉字较多的文章, UTF-8 反而不如 unicode。


windows 下的 ANSI


windows 下常见的是一种叫 ANSI 的编码。 但其实 ANSI 并不是一种“编码” ,ANSI 会根据操作系统系统所在的地域,自动指定为一种地域性的 DBCS , 如在简体中文的 windows 下 ANSI 其实就是 GBK, windows 默认使用 ANSI , 比如你建一个记事本文件,在里面的字默认会是 ANSI 的。


u"我".encode("unicode_escape")

# b'\\u6211'

u"我".encode("gbk")

# b'\xce\xd2'


“我” 字的 unicode 是 62 11 GBK 下是 ce d2


我们来建一个记事本里,面写一个 “我”



然后我们来查看他的字节码。



可以看到 ce d2 就是 “我” 的 GBK码。


锟斤拷锟斤拷烫烫烫烫。


有诗为证:


手持两把锟斤拷,口中疾呼烫烫烫。


脚踏千朵屯屯屯,笑看万物锘锘锘。


其中最出名的, 应该算是 “锟斤拷” 和 “烫” 了。


这些经典的乱码是怎么出现的呢?


首先来解释 “锟斤拷”


从钅字旁就能看出质地, 拷在中国古代乃是一种兵器, 话说当年女娲补天时 太上老。。(啪~)



“锟斤拷” 主要出现在 GBK 和 Unicode 混用的网页中。


在比较老的 web 服务器上是重灾区。


想打造出 “锟斤拷” 需要三位仙人配合: html文件,web server 和 浏览器。


制造流程是, 你的 html文件是 GBK 的而 web server 把它强行用 unicode 解码,解码后发现味儿不对, 将转码失败的字全部用 「U+FFFD」 代替,然后将「U+FFFD」 转成 utf-8 后又发给了浏览器。


「U+FFFD」转为 utf-8 后为 \xEF\xBF\xBD , 如果是两个连续的 「U+FFFD」那结果就是 \xEF\xBF\xBD\xEF\xBF\xBD


最后你的浏览器用 GBK 解码 \xEF\xBF\xBD\xEF\xBF\xBD 你便得到了神器”锟斤拷”


附一个 python “锟斤拷”的例子


锟(0xEFBF

斤(0xBDEF

拷(0xBFBD


ps: 这里 「U+FFFD」乃是 unicode 的一个占位符, 显示为 � , 当 unicode 遇到解释失败的字时,会尝试用 「U+FFFD」 来代替。


下面说说 “烫”


“烫” 主要出没于 windows 平台下,ms 的 vc++ 编译器中, 当你在栈内开辟新内存时, vc 会使用 0xcc 来初始化填充, 很多个 0xcc 连起来就成了 烫烫烫烫烫 同理在堆内开辟新内存时, 会用 0xcd 填充,这便是 屯屯屯屯屯屯 详见这里


不管是 “锟斤拷” 还是 “烫” 都要求最后是用 GB 码输出。


了解 escape 与 encodeURI ,encodeURIComponent 的编码格式差异


javascript 中 String 原型里提供了一个内置函数 String.prototype.charCodeAt 它返回 string 中某个位置上的 unicode 十进制码值。


var jiong = "囧".charCodeAt(0); // 22247

var jiongHex =  jiong.toString(16); // "56e7"


escape:


escape 不转码的字符包括 *,+,-,。,/,@,_,0-9,a-z,A-Z ,他们会以原样输出。


遇到非上述字符时,它会返回 unicode 码。 比如 escape(“囧”) 返回 “%u56E7″其中 “56E7” 就是 “囧” 的 unicode 码。


encodeURI:


encodeURI 不转码的字符包括 !,#,$,&,’,(,),*,+,,,-,。,/,:,;,=,?,@,_,~,0-9,a-z,A-Z


遇到非上述字符时,它会返回 UTF-8 码。 比如 encodeURI(“囧”) 返回 “%E5%9B%A7″” 其中 “e5 9b a7” 就是 “囧” 的 UTF-8 码。 上面也都说了,UTF-8 下大多汉字都会转成 3 个字节。


encodeURIComponent:


encodeURI 不转码的字符包括 !, ‘,(,),*,-,。,_,~,0-9,a-z,A-Z


同上它也是转换成 utf-8 编码格式。


网络传输国际上通用的格式是 utf-8 , escape 虽不是 W3C 规范中的函数, 却被所有浏览器都实现了。



觉得本文对你有帮助?请分享给更多人

关注「前端大全」,提升前端技能


专栏作者简介 ( 点击 → 加入专栏作者 


skiner:It always seems impossible until it’s done.


举报 | 1楼 回复

友情链接