来了解下字符编码的历史吧

1,888 阅读13分钟

相信关于字符编码,大家应该都有所耳闻.多多少少也都能叫出一些名词,像什么 utf-8 , ASCII , Unicode 等.刚好这两天整理笔记的时候,翻到了之前做过一些有关字符编码的笔记,那就索性和大家一起回顾下相关内容喽.

编码和解码

概念

首先在了解各种字符编码格式和标准之前,我们先来理解下编码和解码的概念.编码(encode) 是信息按照一定的规则从一种形式或格式转换为另一种形式的过程,而 解码(decode)则是一个编码的逆转换过程.编码解码都是有一套预先规定的方案,无论是在编码过程还是解码过程,都要遵守这套规则来运算.比如A和B两个人之间约定,1代表我.2表示喜欢,3代表不喜欢,4代表你.当有一天A给B发送124的时候,B就明白了A的心意,然后给A回了一个134.这里信息在传输的过程中,就是使用的1234数字来表示.从我喜欢你124的过程,就可以称之为编码,而当B收到了124之后,将其转换为我喜欢你 文字的过程,就是解码. 在完成来回两次编码和解码的过程后,一个悲伤的故事就诞生了.

而在我们平时的开发过程中,编码和解码也是一个绕不开的话题.很多地方我们都会涉及到编码解码.且不同的编码解码在不同的场景中具有不同的意义.比如常见的字符编码解码,URL编码解码等.

为什要编码解码

那么可能有童鞋要问了,为什么我们进行编码和解码,让它原本是什么样子就什么样子不好嘛?当然是不好,这是因为在计算机中,是不能存储字符的,只能存储0和1两个数字.不管什么字符,都需要先转换成使用0和1表达的数字后才能储存到计算机中.说白了就是一句话,计算机是采用二进制计算的,而二进制不是就只有0和1嘛.至于计算机为什么采用二进制,有很多原因.在这里简单的说几点,在技术上易实现,因为我们可以使用双稳态电路来表示1和0,高电平为1,低电平为0.且因为只有1和0,所以在传输和处理的过程中不容易出错.另外二进制的运算规则也相对来说简单.综合各方面因素,最终计算机采用二进制.当程序员编写高级开发语言的时候,如C语言等,通过计算机编码将其转化为计算机能识别的机器语言.这样既可以方便程序员编写代码,又能让系统稳定快速的计算运行.

乱码

乱码就是因为使用了不对应的字符集导致部分或所有字符没法被正确阅读.就比如我告诉你,打开新华字典的xx页的xx行,就是我想对你说的话.结果你拿了本牛津英汉字典,那你当然不能正确获取到我想对你说的话.

ASCII

ASCII 码表的全称是 American Standard Code for Information Interchange ,中文名叫美国信息交换标准代码,是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言.在五十多年前就已经发布了,后面更新过,但是最近的一版都已经是三十多年前了.其使用了7个二进制数字来表示一个字符,共定义了128个字符,从 00000000 - 01111111.这里为什么是128个字符呢?这是因为当时的那个年代,基础设施还很不完善,硬件条件也不行.为了节省空间,他们约定使用一个字节来保存字符编号.而一个字节(byte)是8位(bit),除去最高位符号位,剩下的7位,即00000001111111 ,这2的7次方,共128位就拿来使用了.下面这是我从百度down下来的图片.

ASCII
从图中可以看出,所有字符主要分为了 控制字符打印字符.至于控制字符这块内容,我就没用到过,大家看看就好了.而打印字符就是我们平时会用到的东西了.看着这张表,大家是不是发现了几组比较熟悉的数字.十进制48-57对应的字符0到9,65-90对应的大写字母A到Z,97到-122对应的小写字母a到z.我们先来看看下面的代码:

let str1 = '0'
console.log(str1.charCodeAt(0))  // 48
let str2 = 'A'
console.log(str2.charCodeAt(0))  // 65
let str3 = 'a'
console.log(str3.charCodeAt(0))  // 97

charCodeAt 方法可以返回指定位置的字符的 Unicode 编码,这个返回值是 0 - 65535 之间的整数.而在JS中, ASCII 编码值和 Unicode 是一样的.所以这里返回的结果和我们上图中对应起来了.而在实际开发中,有时候我们需要判断一个字符是否是小写英文的时候,就可以使用这个方法.

function fn(char){
	let charCode = char.charCodeAt(0)
	if(charCode >= 97 && charCode <= 122){
		return true
	}else{
		return false
	}
}
console.log(fn('d'))  // true
console.log(fn('D'))  // false

当然了,喜欢正则的同学直接使用正则表达式 /^[a-z]$/ 也是可以的,还更加方便.

GB2312

前面说到了老美发明了ASCII码表,可是随着计算机系统的越来越流行,全世界的人都开始使用计算机.但是世界上有好多国家不是基于拉丁字母语系的.就比如我们博大精深的汉字,自古以来就有仓颉造字的故事流传下来.汉字发展至今,文化底蕴深厚,影响源远流长.区区ASCII 并不能表示中文,于是乎,中国人就有了自己的编码.一个字节只能表示2的7次方,那我们中文使用2个字节来表示,那可以表示的范围不是就变成了2的15次方,即32768,足足比 ASCII 码可以表示的范围高了256倍.于是这个国人自己定制的最早标准就叫做 GB2312 ,在上世纪80年,由国家标准总局发布.虽然当时可以表示的范围有几万个,但是 GB2312 并没有收录那么多的汉字和标点符号.只是把一些常用的收录进来,这也是为什么以前有些办事机构打印不出某些生僻字的原因.当然了,因为老美先发明了 ASCII 而中国后有的 GB2312 标准,所以我们的 GB2312 在一定程度上是兼容 ASCII 码表的,即我们的0到127位是被保留下来的.

GBK

GBK 编码标准其实就是 GB2312 的升级版了,因为前面说到了,在原先的 GB2312 中只有几千个常用汉字和字符,打印不出那些生僻字.所以为了解决这个问题,新的 GBK 标准在上世纪90年代中发布了,比起之前的增加了2万多个汉字.下面是我从GBK编码表中随便找的一段,大家看看是不是有很多的字不认识,笑哭😂.

GBK

GB18030

在GBK之后,又陆陆续续的发布了几个版本,如 GB18030-2000GB18030-2005 等.统称为 GB18030 ,中文叫 信息技术 中文编码字符集.其对GB 2312-1980完全向后兼容,与GBK基本向后兼容,并支持Unicode(GB 13000)的所有码位.共计收录汉字70244个.

Unicode

随着计算机的推广,越来越多的国家使用上了计算机.又因为大家的语系不同,为了表示自己国家的语言,于是大家都制定了自己的编码格式,你一套,我一套,这在刚开始的单机世界并没有什么问题,大家各自玩各自的,也不需要和别人通信.但是随着互联网的浪潮兴起,人与人之间,计算机与计算机之间的通信就成为一个必不可少的过程了.可是此时人们却尴尬的发现,因为前期大家各自玩自己的,也只管收录自己用的到的.于是便导致各种标准满天飞,却没有一个可以通用的,像 ASCII 肯定就没法表示汉字.于是出现了 Unicode (又称统一码、万国码、单一码),包括字符集、编码方案等.它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求.1990年开始研发,1994年正式公布.注意了,Unicode 是采用的双字节对字符进行编码,即无论是一个英文还是中文,无论是英文标点还是中文标点,都是占据两个字节.

在前面我们提到过的 charCodeAt 方法,返回的就是 Unicode.我们在之前的代码中已经测试数字和字母.现在我们再来测试下汉字

console.log('哈'.charCodeAt(0))  // 21704

那么 21704 就是 这个字符的Unicode 数值了.

UTF-8

讲完了 Unicode 之后,我们终于可以来说 utf-8 了.关于这两者的关系,简单的来说就是 Unicode 是字符集,本身只规定了每个字符的数字编号是多少,并没有规定这个编号如何存储.而 utf-8 是编码规则.再说的通俗点,前者是个标准,后者是个方案,是个具体的实现.其实不止 utf-8 这么一种方案,还有 utf-16utf-32 等.不过至于后面这两方案,使用的并不多.因为它们有比较明显的缺点,像 utf-16 规定了必须用2个或4个字节,而 utf-32 则是用4个字节总32位去表示 Unicode.所以它们没法兼容一个字节的 ASCII码, 而且占用的空间还大,那要它们有何用.

因此 utf-8 才是目前使用最为广泛对 Unicode 支持最好的一种编码方式了.它的一个很明显的特点就是在编码过程中,它会使用不同的字节数量来表示一个字符.像原本的 ASCII 码,是用一个字节表示的,那现在还用一个字节来表示,做到了兼容.但是关于汉字,原先我们使用两个字节来表示,而现在的 utf-8 则是使用了三个字节.

总而言之, utf-8 是现在最为优秀的字符编码方案了.很多时候,我们在编写代码的时候,我们会声明我们代码的字符编码格式是 utf-8的.比如在html文件的头部声明 <meta charset="UTF-8"> ,在py文件的头部声明 # coding=utf-8.

编码规则

  1. 对于单字节的符号,字节的第一位设为0,后面7位为这个符号的 Unicode 码.因此对于英语字母,UTF-8 编码和 ASCII 码是相同的
  2. 对于n字节的符号(n > 1),第一个字节的前n位都设为1,第n + 1位设为0,后面字节的前两位一律设为10.剩下的没有提及的二进制位,全部为这个符号的 Unicode 码.

下面我们转换一个 字,使用 utf-8 的格式来表示.

console.log('张'.charCodeAt(0))  // 24352

通过 charCodeAt 方法获取 Unicode 数值为 24352 ,将其转换为 16进制,结果为 0x5f20 .我们去一个在线编码解码的工具网站 进行字符串编码,结果为 \u5f20 ,和我们前面的对应上了.

zhang

接下来继续转换,5f20 分别将每个数字转为二进制,结果为:0101 1111 0010 0000.再运用上面的编码规则2,一个汉字为3个字节,所以n为3.第一个字节的前3位都设为1,第4位设为0.那么结果就是 1110xxxx 10xxxxxx 10xxxxxx ,这里的xxx等是剩下的没有提及的二进制位,用 Unicode 码(即 0101 1111 0010 0000) 来填充.

0101 1111 0010 0000  # 从右往左,依次填入呀.
1110xxxx 10xxxxxx 10xxxxxx  # 最后一个x用上面最后一个数字0替换,倒数第二个x用上面倒数第二个数字0替换,以此类推.
11100101 10111100 10100000 # 填充完的二进制
e5 bc a0 # 将二进制转为16进制后 ,结果为 e5 bc a0 注意这个结果,我们后面还会遇到.

注:不会进制转换的童鞋可以使用在线转换.

URL编码解码

不知道大家有没有注意到一个现象,当我们在Chrome浏览器地址栏的url后面加上一个query字符串的时候,如下图所示

url_query

我输入的是 name=张三 .而当我复制了这个url,新开一个页面把刚刚复制的地址黏贴过去的时候

url_query

原本的中文字符 张三 变成了一堆看不懂的字符串 %E5%BC%A0%E4%B8%89 .这是因为这个字符串已经被浏览器进行了相对应的编码.附上一个网站 URL编码/解码,大家可以把这一长串字符串复制进去解码看看效果.这是因为现在大部分的浏览器中的字符都被当作 utf-8 处理了.而中文的 utf-8 编码,是一个汉字等于三个字节,也就是三组 %xx, 即 %xx%xx%xx.看到这里是不是又有点眼熟了.没错,这里url编码后的 正是 %E5%BC%A0 ,这个和我们上面提到填充完的二进制转换为十六进制的编码结果是一致的.

总结

关于字符编码的知识其实是蛮多的,我在这边只是提了下最简单的一些概念而已.大家有兴趣的可以自己去了解下.对了,推荐个书吧,以前看过的一本书,但是貌似没看完( 😂 ),英文名叫 code ,中文名叫 编码,有兴趣的童鞋可以去看看呀.

参考链接: