从一个故事开始聊聊字符编码

联通不如移动的故事

在编码界一直流传着联通不如移动的一个故事。。。javascript

请不要误会,联通和移动和本篇文章所说的编码确实没什么关系,但请出联通和移动帮忙作个小实验,再来仔细说说编码。java

在Windows系统下,在桌面上右键新建一个记事本文件,打开它输入“联通”两个汉字,Ctrl+S保存并关闭。node

双击再次打开它,看到了什么?奇怪,文字怎么变成乱码了? 函数

好吧,再次新建一个文件,这回输入“移动”保存再试试。神奇,移动竟然完美显示。学习

好了,不说什么故事了,这个有趣的现象正是为了聊聊计算机中“编码”的那些事,以后再解释为何“联通不如移动”。ui

聊聊字符编码的发展史

在计算机中,全部存储的数据都由二进制表示。字母、数字、字符这些都不例外,计算机中最小的单位就是二进制位(0和1),8个位表示一个字节,所以8个二进制位就能够排列组合出256种状态,也就是理论上能够表示出256种字符,而由哪些二进制位表示哪些字符,这就是由人来决定的了,也就是人们制定出的各类“编码”。编码

电脑这种东西最先由老外发明,外国人使用的英语只有26个字母,再加上标点、数字和一些符号也不会太多,所以英文一般用ASCII编码来表示。spa

ASCII码

ASCII码最开始只在美国使用,组合出的256种状态中,第0~32中规定了特殊用途,一旦终端、打印机赶上约定好的这些字节被传过来时,就要作一些约定的动做,好比遇到0×10, 终端就换行等等。code

又把全部的空格、标点符号、数字、大小写字母分别用连续的字节状态表示,一直编到了第 127 号,这样计算机就能够用不一样字节来存储英语的文字了。cdn

记得当初学习C语言的时候,就清楚的知道了一些经常使用的ASCII码值,好比大写A是65,小写a是97等。

这128个符号(包括32个不能打印出来的控制符号),只占用了一个字节的后面7位,最前面的一位统一规定为0。

英文能够表示了,可是世界上除了英文还有不少语言。咱们的中文文字浩如烟海,仅仅靠这8个二进制位远远不够,怎么办?

GB2312

且不说中文,在欧洲有些国家的语言中也有一些特殊的字母,好比俄文希腊文等。因而便使用127号以后的空位继续表示他们的字母。固然,因为每一个国家的语言不一样,就愈来愈乱,好比130在法语中是字母 é,可是在希伯莱语中130倒是他们的字母 ג。

咱们的中文就更难办了,即便把全部的位都用上,也表示不完成千上万的汉字,因而咱们本身也制定了一套中文的编码GB2312。

中国为了表示汉字,把127号以后的符号取消了,规定:

  • 一个小于127的字符的意义与原来相同,但两个大于 127 的字符连在一块儿时,就表示一个汉字;
  • 前面的一个字节(他称之为高字节)从0xA1用到0xF7,后面一个字节(低字节)从 0xA1 到 0xFE;
  • 这样咱们就能够组合出大约7000多个(247-161)*(254-161)=(7998)简体汉字了。
  • 还把数学符号、日文假名和ASCII里原来就有的数字、标点和字母都从新编成两个字长的编码。这就是全角字符,127如下那些就叫半角字符。 把这种汉字方案叫作 GB2312。GB2312 是对 ASCII 的中文扩展。

GBK

再后来,发现了GB2312虽然解决了中文编码的问题,可是仍有不足。

GB2312表示的中文有时不够,有些字并非生僻字,可是没有收录其中,当时有个小插曲,我当时在高考报名的系统中查询成绩的时候报不出个人名字,只能报出个人姓,正是由于个人名字“玥”字不在GB2312的编码范围,所以没有。

因而干脆再也不要求低字节必定是 127 号以后的内码,只要第一个字节是大于 127 就固定表示这是一个汉字的开始,又增长了近 20000 个新的汉字(包括繁体字)和符号。

这就是更全面的GBK编码。

Unicode

随着发展,每一个国家都对本身的语言编出一套本身的编码,真是混乱不堪,咱们不知作别人用什么编码,别人也不知道咱们用什么编码,因而标准组织出手了。

ISO标准组织看到了乱象,制定了一套Unicode编码以解决这种混乱的局面,它的制定简单粗暴,不是全世界的语言多么,我干脆就规定,全部的字符都给我用两个字节表示(两个8位一共16位),对于 ASCII 里的那些 半角字符,Unicode 保持其原编码不变,只是将其长度由原来的 8 位扩展为16 位,而其余文化和语言的字符则所有从新统一编码。

从 Unicode 开始,不管是半角的英文字母,仍是全角的汉字,它们都是统一的一个字符。同时,也都是统一的两个字节。

UTF8

Unicode的制定是在1990年,正式使用在1994年,那个年代在如今来看简直是远古时期,那时因为互联网并不发达并无推广开。

随着互联网的发展,为了解决Unicode传输问题,于时面向众多的UTF标准出现了。

  • UTF-8 就是在互联网上使用最广的一种 Unicode 的实现方式
  • UTF-8就是每次以8个位为单位传输数据
  • 而UTF-16就是每次 16 个位
  • UTF-8 最大的一个特色,就是它是一种变长的编码方式
  • Unicode 一个中文字符占 2 个字节,而 UTF-8 一个中文字符占 3 个字节
  • UTF-8 是 Unicode 的实现方式之一

由于UTF8是Unicode的实现方式之一,它们之间是互通的,就是说Unicode编码能够传换为UTF8,它有一套对应规则:

Unicode符号范围(16进制) UTF8编码(2进制)
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

能够看到,对于单字节的符号,字节的第一位设为0,后面7位为这个符号的 Unicode 码。所以对于英语字母,UTF-8 编码和 ASCII 码是相同的(见上面表格的第一行)。

对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一概设为10。剩下的没有说起的二进制位,所有为这个符号的 Unicode 码。

说的有些抽象,举个例子吧,好比来了一个汉字,电脑是怎么知道的它是用UTF8编码的呢?

由于汉字用三个字节表示(别再问为何用三个字节表示了,这是规定),所以第一个字节的前三位都为1,第四位设为0,后面的位都以10开头,因此它确定长这个样子:1110xxxx 10xxxxxx 10xxxxxx。

OK,电脑按照这个规则一看明白了,来的是个汉字!

不如再举个例子,从Unicode编码表中查出一个汉字对应的编码,把它转换为UTF8试一试,就用个人名字“玥”字吧,它的Unicode编码为\u73a5

首先第一步把16进制转换为2进制,它的值是111001110100101,那怎么拆分这个2进制的值呢?由于UTF8都是后6位为这个字符的Unicode的码,因此咱们从右往左数6位给一一对应上,不足的位补0就行了。

这样就得出了“玥”字的UTF8编码: 11100111 10001110 10100101

做为开发人员彻底能够用代码实现一下,这里用node.js真实的实现一下转码:

function transferToUTF8(unicode) {
  code = [1110, 10, 10];

  let binary = unicode.toString(2); //转为二进制

  code[2] = code[2] + binary.slice(-6); //提取后6位
  code[1] = code[1] + binary.slice(-12, -6); //提取中间6位
  code[0] = code[0] + binary.slice(0, binary.length - 12).padStart(4, '0'); //取剩余开始的位,不够补0

  code = code.map(item => parseInt(item, 2)); //把字符串转换为二进制数值

  return Buffer.from(code).toString(); //利用Buffer转转为汉字
}

console.log(transferToUTF8(0x73a5));
复制代码

运行结果:

复制代码

以上代码定义了一个transfer函数,参数接收一个16进制值,它表明了一个Unicode字符,transfer函数内部先转换为二进制,并按照UTF-8的规则转换为相应的UTF-8编码,最后,利用node.js的Buffer最终转码成汉字,能够看到,已经正确输出了汉字“玥”。

以上,就是简单分析了Unicode和UTF-8的转换关系。

为何联通不如移动?

故事就要讲完了,说了这么多编码的事如今能够回头看看开篇为何联通变成了乱码,由于在Windows的记事本中文默认的保存编码为GB2312,经过查询能够查到汉字“联”对应的GB2312编码为\uc1aa,转换为二进制是1100000110101010,正好是16位两个字节,按8位拆成两组正好与UTF8的第二种编码格式对应上了:110xxxxx 10xxxxxx,这样再次打开记事本的时候Windows扫描文件内容,它就会认为这是UTF-8编码的文件,而不是GB2312!此时此刻按照UTF-8来解析文件内容固然出现了乱码。

这时能够从新另存为文件,把文件格式改成GB2312来保存,现次打开“联通”终于显示了。

这个例子很极端,能够说“联通”二字的编码正好是个巧合,可是搞明白了编码的细节,更有助于咱们在开发中遇到问题能够快速理解其实质,并加以解决,在此记下笔记,与你们共同窗习提升。

相关文章
相关标签/搜索