从一个小故事聊聊字符编码那些事

发布于:2023-03-12 ⋅ 阅读:(53) ⋅ 点赞:(0)

联通不如移动的故事

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

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

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

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

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

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

聊聊字符编码的发展史

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

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

ASCII码

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

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

记得当初学习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来保存,现次打开“联通”终于显示了。

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

本文含有隐藏内容,请 开通VIP 后查看