原文链接:http://www.juzicode.com/archives/948
桔子菌:你听说过微软30多年前推出Windows的时候,隐晦表达过联通不行移动行么子妹瞟了一眼桔子菌:瞎扯,我只听葛优说过“神州行我看行”。桔子菌有点炫耀:不可思议是吧看我用Windows7自带的记事本软件新建2个txt文件,在文件中分别输入“联通”和“移动” ,再次打开文件看,发现保存了“移动”的文件仍然可以看到“移动”2个字,但是保存“联通”的文件就不行了,看不到了。橙子妹稍微有点惊讶,又瞟了一眼桔子菌:你这是哪跟哪,不过我还是有点好奇这是怎么回事。桔子菌:请听我娓娓道来。
微软你想说某通不行可以直接点吗
在计算机的世界里,不管是内存、硬盘存储的内容都是“0”和“1”, 络上传输的数值也是“0”和“1”,CPU能处理的也是“0”和“1”。单个bit只能表示”0″和”1″的2种数值状态,通过bit位数的增加就可以表示更多的数值。通常用8个这样的”0″或”1″构成一个字节,这样一个字节就包含了256种数值,再进一步的2个字节就能表示65536种数值。这些数值表达什么含义,或者这些数值之间按照特定的关系进行”组织”,这个“表达”或者”组织”的过程就叫做编码。
到了这里我们清楚了不同的后缀可以对应特定的应用程序来打开,但是有些文件即使是相同的后缀,可能他内部的格式还是有差异的,这又是怎么区分的呢如一张bmp文件,到底表示的是黑白图片还是彩色图片、彩色图像是用16色(4位)还是24位表示的,仅仅通过后缀已经不能区分了。
不同格式的lena图
上面的3个lena图片都是bmp的后缀,从预览效果看它的内容是有差异的,这是图片文件内部编码方式存在差异导致的。2色图片中每个像素只需要1个bit表示,16色图中每个像素需要4个bit表示,而24位图中每个像素需要24bit表示,如果应用程序仅仅根据后缀来解码,就不能适应这种情况了。所以在文件内部的某个位置约定其具体的编码方式,这样应用程序就能根据这个约定决定使用何种解码方式。
这时候就出现了各种各样的字符集,比如汉字编码基本字符集GB2312,用2个字节表示一个字符,理论上就能表达65536(0xFFFF+1)种字符。当然咯,实际上GB2312只定义了94个区,每个区包含94个位,最多能表示94×94=8836个字符。GB2312用第1个字节表示区,第2个字节表示位,区的编 从1~94,位的编 也是从1~94,这是GB2312的字符集,但是表示字符时的编码需要在区和位的基础上分别加上0xA0,所以区和位的范围是0xA1~0xFE(0xA0+1~0xA0+94),这样2个字节中都没有使用小于0x7F的数值,也就能兼容ASCII编码。另外不知道是巧合还是故意为之,0xFF也不在其编码范围内,这一点貌似设计的比较巧妙,巧妙之处后面再说。虽然GB2312能表示8836个字符,但是实际存在一些空位,最终只收录了6763个汉字和682个非汉字图形字符,但是对付常用的汉字已经绰绰有余。
GB2312区位编码
但是GB2312只是简体中文字符的编码标准,在台湾地区的繁体字用的是BIG5字符集,在日本则有S-JIS 、EUCODE 等字符集,这样同一个数值表示的含义在不同的字符集里表示的实际字符就会不一样。后来为了改变这个混乱的局面,ISO组织搞了个ISO10646、统一码联盟搞了个Unicode字符集,后来他们俩讨论决定只做一样的字符集,二者保持同步,意图将全世界的所有字符放在一个统一的字符集里面,每个字符都有自己唯一的“身份”。Unicode的出现为世间的编码灾难带来了曙光,但是”存在即合理”,历史存在的字符集已经存在了,各种应用程序为了向前兼容,仍然要支持老的字符编码标准。比如在简体中文系统里面,虽说新的应用程序已经支持Unicode字符集,但是GB2312依然在简体中文世界里大行其道。
桔子菌看着橙子妹欲言又止的样子,赶紧主动收拢了话题,没有扯更远的GBK,GB18030之类的汉字字符集了,况且GB2312也能说明问题了。到现在背景也介绍的差不多了,该来点实际的东西了。
再回到刚才的“联通”问题,用Windows自带的记事本保存的时候,默认保存的方式是“ANSI”方式(中文操作系统里实际上用的是GB2312编码标准,只是称呼上叫ANSI),点击记事本的文件菜单下的另存为,可以看到能选择不同的编码方式保存:
记事本多种保存格式
继续按照剩下的Unicode、Unicode big endian、UTF-8 三种编码方式另存为三个文件。再双击打开文件的时候这3个文件都能正常显示”联通”2个字符了。这中间发生了什么神奇的事情/p>
好了,到了该祭出神器的时候了,winhex软件是一款强大的二进制分析工具,不但可以分析磁盘启动区、修复文件,还可以用来查看文件的二进制内容,我们用winhex软件把前面保存的4种格式的文件打开看看。
winhex解析文件的二进制数据
第一部分是用ANSI方式存储的文件,打开后显示存储的数值是C1,AA,CD,A8,查看下”联”、”通”的GB2312编码,其数值分别为C1AA,CDA8,说明用ANSI方式保存就是用GB2312的编码方式存储的,下面2张图是GB2312的码表截图。
“联”字GB2312编码
“通”字GB2312编码
再继续看下”联通”的Unicode编码数值是多少,找一个“ Unicode编码”这类的工具 站,输入“联通”字符查询编码:
“联通”的Unicode编码与UTF=8编码
从上面的转换可以看到,右边橙色框是Unicode数值,”联”、”通”分别对应了0x8054和0x901a的Unicode编码值,正好和按照Unicode big endian方式保存文件的第三个字节开始的内容是一样的。而按照Unicode方式(实际是little endian)保存的文件内容和前者的顺序正好是”交替”存储的。关于big endian和little endian的区别,简单点说big endian就是高位字节的数据存储在低地址的模式,little endian则正好相反。用winhex打开文件看到的越往前是低地址,越往后是高地址,在big endian文件中可以看到”联”的高字节内容0x80排在前面低地址,低字节内容0x54排在后面高地址,而little endian正好相反。这也体现在最开始的2个字节上,big endian的是FE和FF,而little endian是FF和FE。
再来看下UTF-8编码方式,UTF-8并不是一种字符集,而是实现Unicode字符集的一种编码方式,实际上表示的还是Unicode字符集,可以形象地理解为Unicode字符集本身是一种“信源”编码,而UTF-8则是一种“信道”编码。RFC3629规定的Unicode字符集和UTF-8编码的对应关系是下图这样的,Unicode编码范围分为4个,每个范围内的字符对应了一种编码方式,比如在0x0000 0000~0x0000 007F之间的值需要1个字节表示,这样对于ASCII字符就只需要1个字节表示,做到了和ASCII字符集的兼容,而大多的汉字字符落在第3行的0x0000 0800~0x0000 FFFF范围内,需要用3个字节表示:
Unicode与UTF-8映射关系
按照上面的规则计算下”桔子code”的UTF-8编码,首先查到“桔子code”的Unicode编码:
“桔子code”的Unicode和UTF-8编码
可以看到“code”这4个字符的Unicode编码范围在第1段0x0000 0000~0x0000 007F范围内,右侧橙色框中表示的Unicode编码和左侧橙色框的UTF-8编码是一样的,和ASCII编码也保持一致。“桔”的Unicode编码为0x6854,“子”的Unicode编码为0x5b50,下面来计算下是怎么对应到UTF-8编码的。首先找到”桔”的Unicode编码值落在第三行,将0x6854转换为二进制表示0b01101000 01010100,然后按照第三行范围对应右侧的方式进行切割,首先找到最右侧的6个bit:010100,再往左找6个bit:100001,然后还剩下左边的4个bit:0110。将这3段二进制数值从右往左依次填入,就可以得到0b11100110 10100001 10010100,换算成16进制,得到的结果为0xE6A194,换算的结果和 页上查到的UTF-8编码值是一样的。”子”的计算过程是一样的。
Unicode编码和UTF-8编码转换过程
桔子菌长长地叹了一口气:终于把背景介绍的差不多了,现在要来填最开始的那个坑了。
我们来看看可能的情况,只能说是可能,因为没有拿到程序的源码抓bug那是耍流氓。前面查到“联通”2个字的GB2132编码为C1AA,CDA8,转换成二进制就是0b1100000110101010 ,0b1100110110101000,恰好能被误解为2字节的UTF-8编码:
如果当成2字节UTF-8,提取其中有效bit位构成的Unicode编码为:0x006a,0x0368,按道理前者对应的是字母”j”,但是记事本程序可能并不能理解2字节宽度的0x006a,所以显示的是乱码,而后者查到的Unicode字符是下图这样的,可能操作系统中没有相应的字体也会显示出乱码来:
橙子妹回到电脑前也新建了个联通的txt文件玩一下,但是发现并没有出现不能正常显示的问题。桔子菌在一旁提醒道:这可是30年的老bug了,微软良心发现”终于做出这个决定”,在30年之后还是把这个bug修复了,所以在Windows10 1909之后的版本不存在这个问题了。
往期精彩:
Windows PATH变量、命令行、搜索路径
好冷的Python–源文件编码
用你的邮箱为你看家护院
好冷的Python– if __name__==’__main__’是啥东东
zbar:给我来10G打码图片
声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!