联通不如移动的故事
在编码界一直流传着联通不如移动的一个故事。。。
请不要误会,联通和移动和本篇文章所说的编码确实没什么关系,但请出联通和移动帮忙做个小实验,再来仔细说说编码。
在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来保存,现次打开“联通”终于显示了。
这个例子很极端,可以说“联通”二字的编码正好是个巧合,但是搞明白了编码的细节,更有助于我们在开发中遇到问题可以快速理解其实质,并加以解决,在此记下笔记,与大家共同学习提高。