1.

当用一个软件(比如Windows记事本或Notepad++)打开一个文本文件时,它要做的第一件事是确定这个文本文件究竟是使用哪种编码方式保存的,以便于该软件对其正确解码,否则将显示为乱码。

一般软件确定文本文件编码方式的方法有如下三种:

  • 检测文件头标识;
  • 提示用户手动选择;
  • 根据一定的规则自行推断。

2.

文件头标识一般指的是字节顺序标记BOM(Byte Order Mark),位于文件的最开始。当打开一个文本文件时,就BOM而言,有如下几种情形:

  • BOM为:EF BB BF ——表示编码方式为UTF-8;
  • BOM为:FF FE ——表示编码方式为UTF-16LE(小端序);
  • BOM为:FE FF ——表示编码方式为UTF-16BE(大端序);
  • BOM为:FF FE 00 00 ——表示编码方式为UTF-32LE(小端序);
  • BOM为:00 00 FE FF ——表示编码方式为UTF-32BE(大端序);
  • 没有BOM ——要么显式地提示用户手动选择一种编码方式,要么隐式地由软件按规则自行推断出编码方式。

3.

接下来,是见证诡异怪事的时刻。

当你在简体中文版的Windows记事本里新建一个文件,输入“联通”两个汉字之后,保存为一个txt文件。然后关闭,再次打开该txt文件后,你会发现刚才输入并保存的“联通”两个汉字竟然莫名其妙地消失了,取而代之的是几个乱码。如下图所示。

这是为什么呢?难道是微软跟联通有仇吗?

原来,当你用Windows记事本新建一个文本文件时,其编码方式默认为ANSI编码(在简体中文版Windows中实际为GBK编码),没有BOM。

(注:Windows系统中的ANSI编码指的是在区域设置中所设置的系统默认编码方式,在简体中文版Windows系统中指的是GBK,即CP936代码页,具体可参看前文《刨根究底字符编码之七——ANSI编码与代码页》)

(笨笨阿林原创文章,转载请注明出处)

在这种编码方式下,该文本文件仅仅保存了“联通”两个汉字的GB内码的四个字节,如下所示(左边为十六进制,右边为二进制)。

  c1  1100 0001
  aa  1010 1010
  cd  1100 1101
  a8  1010 1000

通过Notepad++的HEX-Editor插件可查看内码(十六进制),如下图所示。

通过UltraEdit的“十六进制编辑”模式也可查看内码(十六进制),如下图所示。

4.

当用记事本再次打开该文本文件时,由于没有BOM,记事本又没有提供显式地提示用户手动选择编码方式的功能,于是就只能隐式地按其推断规则自行推断,推断的结果就是被误认为了这是一个UTF-8编码方式的文件。

为什么会推断错误呢?又为什么会将其编码方式错误地推断为UTF-8呢?

注意,“联通”两个汉字的GB内码,其第一第二个字节的起始部分分别是“110”和“10”,第三第四个字节的起始部分也分别是“110”和“10”,这刚好符合了UTF-8编码方式里的两码元序列的编码算法规则(即与UTF-8的两码元序列“110xxxxx 10xxxxxx”中的前缀码“110”和“10”刚好是完全一致的;详见本系列文章中《刨根究底字符编码之十二——UTF-8究竟是怎么编码的》一文的介绍)。

让我们按照UTF-8的编码算法规则,将第一个字节的前缀码110去掉,得到“00001”,将第二个字节的前缀码10去掉,得到“101010”,将两者组合在一起,得到“00001101010”,再去掉多余的前导的0,就得到了“0110 1010"(十六进制为6A),这正好是Unicode字符集里的U+006A,也就是小写字母“j”的码点值。

同理,之后的第三个字节与第四个字节按同样的方法用UTF-8解码之后正好是Unicode字符集里的U+0368,这个字符为“ͨ”(抱歉,这里的左双引号貌似被这个字符所影响,看起来像是半角左双引号,而无法正常显示为全角左双引号),很像是上标的一个小c,这应该是个组合字符(组合字符是Unicode字符集中的一种特殊字符,必须与其他字符组合在一起以形成一个新字符,一般不单独使用,可参看本系列文章前面相关文章中的介绍)。

这就是只有“联通”两个汉字的文本文件没有办法在记事本里被正确解码显示的原因。这里要特别说明的是,在记事本里打开时显示的不是“j”和“ͨ”,而是显示为了“��ͨ”(注意右上角是“ͨ”)。

而用UltraEdit打开,如果在设置中选择了“自动检测UTF-8文件”,显示的是“j”和“ͨ”组合在一起的字符“jͨ”。注意这个字符不是小写字母“j”,而是小写字母“j”上面的点变成了一个上标的小c,因为U+0368这个字符“ͨ”应该是个组合字符,与其前面的小写字母“j”组合在一起而形成了一个新字符——jͨ(再次提醒注意:小写字母“j”上面的点变成了“c”)。

(注意:在UltraEdit的早期版本中,没有“自动检测UTF-8文件”这一选项)

5.

这里还有一个问题:既然已经推断为了UTF-8,那为什么Windows记事本还是将前两个字节,亦即原本为“联”字的GB内码的那两个字节,显示为了“��”这样的乱码,而不是显示为小写字母“j”呢?

我想主要是因为小写字母“j”属于ASCII字符,在UTF-8编码中ASCII字符属于单字节编码,出现在双字节编码中是非正常的,因而被Windows记事本认为是错误编码,而UltraEdit则作了容错处理,仍然将其解读为了小写字母“j”。

而后两个字节,亦即原本为“通”字的GB内码的那两个字节,之所以Windows记事本将其按UTF-8编码的规则解读为了字符“ͨ”,那是因为字符“ͨ”的UTF-8编码正好就是双字节编码,因此按UTF-8编码的规则去解读的话不属于错误。

(笨笨阿林原创文章,转载请注明出处)

6.

其实,用记事本默认的编码方式(ANSI)分别单独保存“联”字和“通”字为两个独立的txt文件,则:

1) 再用记事本打开时,“联”字显示的是“��”,“通”字显示的是“ͨ”;

2) 用UltraEdit打开时,

  (1) 如果选择了“自动检测UTF-8文件”,“联”字显示的是小写字母“j”,“通”字显示的“ͨ”(不过看不清,我开始还以为是个空格);

  (2) 如果没有选择“自动检测UTF-8文件”,“联”字和“通”字均能正常显示(说明这种情况下UltraEdit正确地推断出了编码方式为GBK,从这一点来看,UltraEdit比Windows记事本要强);

3) 用NotePad++打开时,

  (1) 如果在“格式”中选择的是“以ANSI格式编码”(亦即显式地手动选择了正确的编码方式),“联”字和“通”字均能正常显示;

  (2) 如果编码方式选择的是UTF-8、UTF-8无BOM、UCS-2 Big Endian或UCS-2 Little Endian时,则“联”字均显示为“xC1xAA”(有意思的是,直接复制“xC1xAA”然后粘贴到Word里,则显示为了小写字母“j”),“通”字均显示为“ͨ”。

而如果是用记事本默认的编码方式(ANSI)保存“联通通信”四个字,则用记事本、UltraEdit(即便选择的是“自动检测UTF-8文件”的情况下)打开后都可正常显示。

这充分说明,Windows记事本在文件头没有BOM的情况下,只能自行推断,由于“联通”两个汉字保存为ANSI编码方式时,内码只有四个字节,在信息不够充足的情况下(尤其是其内码又刚好符合了UTF-8的编码算法规则),于是被错误地推断为了UTF-8编码方式;当以ANSI编码方式保存的是“联通通信”四个汉字时,内码有八个字节,这时信息较为充足,因此被正确地推断为了ANSI编码方式(在简体中文版Windows中ANSI编码默认为GBK编码)。

7.

上面分析的是Windows系统中采用ANSI编码时没有添加BOM的情况。那么,对于采用非ANSI编码时添加了BOM的情况,是否就万事大吉了呢?其实,添加BOM来标记字符编码表面看起来貌似不错,但实际上经常会带来麻烦,因为它和很多协议、规范并不兼容。

Windows里的软件在采用非ANSI编码时,即便对于根本不存在字节序问题的UTF-8编码默认也会添加BOM(详见之前文章《刨根究底字符编码之十一——UTF-8编码方式与字节序标记》的介绍),而像Unix、Linux、Mac OS等*nix系统对于UTF-8编码都默认不添加BOM。

既然*nix系统都可以不添加BOM,那为什么Windows系统却非要添加BOM呢?这很可能是因为Windows系统有大量普通用户使用,在必须兼容传统ANSI编码的情况下,从用户体验角度考虑而没有采用显式地要求用户手动选择字符编码方式的做法,因此特别依赖于通过BOM来防止隐式地自行推断字符编码方式而出错。

微软这种为了照顾广大普通用户而从用户体验角度出发“好心办坏事”的例子其实还有很多。

8.

因此,在Windows系统中,尽量不要使用记事本来打开并编辑文本文件,尤其是作为程序员,应使用Notepad++或UltraEdit等更为专业的文本文件编辑软件。

这一方面是可以避免出现上述这样的“诡异”错误,另一方面也是为了避免Windows记事本“多此一举”地添加BOM(详见下面附文中的解释),从而给在与其他系统(比如*nix系统)交流时带来不必要的麻烦。

附:Windows记事本中对常用编码方式自行其是的“奇葩”命名

Windows记事本中,对常用编码方式的命名非常“奇葩”,微软这种自行其是的非标准命名,很是令人费解,现解释如下。

  1) ANSI指的是对应当前系统区域设置(即系统locale)中的默认ANSI编码,不带BOM。在简体中文版Windows系统中默认ANSI编码指的就是GBK编码,即CP936,具体可参看前文《刨根究底字符编码之七——ANSI编码与代码页》。

  2) Unicode指的是带有BOM的小端序UTF-16(即UTF-16LE with BOM)。

  3) Unicode big endian指的是带有BOM的大端序UTF-16(即UTF-16BE with BOM)。

  4) UTF-8指的是带有BOM的UTF-8(即UTF-8 with BOM)。UTF-8编码方式实际上并不存在字节序的问题,之所以仍然“多此一举”地添加BOM,应该是由于要兼容不添加BOM的ANSI编码,从用户体验角度考虑,避免用户显式地手动选择编码方式。

  (注:如果UTF-8编码不添加BOM,则有两种不添加BOM的编码方式,从而导致隐式地自行推断编码方式更容易出错,上文所介绍的对“联通”推断出错即是明证。当然反过来也说明了Windows记事本对于不添加BOM的UTF-8编码其实同样是支持的,而并非简单粗暴地直接提示错误,这应该是为了兼容*nix系统不添加BOM的做法而不得不采取的策略。只是这样一来,就很难避免陷入左右为难的困境。)

(笨笨阿林原创文章,转载请注明出处)

(未完待续)

【转】刨根究底字符编码之十六——Windows记事本的诡异怪事:微软为什么跟联通有仇?相关推荐

  1. 【转】刨根究底字符编码之十二——UTF-8究竟是怎么编码的

    UTF-8究竟是怎么编码的 1. UTF-8编码是Unicode字符集的一种字符编码方式(CEF),其特点是使用变长字节数(即变长码元序列或称变宽码元序列)来编码.目前一般是1到4个字节,当然,也可以 ...

  2. 【转】刨根究底字符编码之十——Unicode字符集的字符编码方式

    一.字符编码方式CEF的选择 1. 由于Unicode字符集非常大(并且作为开放字符集还在不断扩展之中),有些字符的编号(即码点值)需要两个或两个以上字节来表示,而要对这样的编号进行编码,也必须使用两 ...

  3. 【转】刨根究底字符编码之十五——UTF-32编码方式

    1. UTF-32在UTF目前常用的三种编码方式(UTF-8.UTF-16.UTF-32)中,是最为简单的一种编码方式.UTF-32编码方式不使用任何编码算法将Unicode字符码点值(即编号字符集C ...

  4. 【转】刨根究底字符编码之十四——UTF-16究竟是怎么编码的

    1. 首先要注意的是,代理Surrogate是专属于UTF-16编码方式的一种机制,UTF-8和UTF-32是不用代理的. 如前文所述,为了让UTF-16能继续编码基本平面后面的增补平面中的码点值,于 ...

  5. 刨根究底字符编码之十四——UTF-16究竟是怎么编码的(“代理区(Surrogate Zone)”,范围为0xD800~0xDFFF(十进制55296~57343),共2048个码点未定义。UTF8和

    1. 首先要注意的是,代理Surrogate是专属于UTF-16编码方式的一种机制,UTF-8和UTF-32是不用代理的. 如前文所述,为了让UTF-16能继续编码基本平面后面的增补平面中的码点值,于 ...

  6. 【转】刨根究底字符编码之零——前言

    前言 一. 字符编码是计算机世界里最基础.最重要的一个主题之一.不过,在计算机教材中却往往浮光掠影般地草草带过,甚至连一本专门进行深入介绍的著作都找不到(对这一点我一直很困惑,为什么就没有哪位大牛对这 ...

  7. 刨根究底字符编码之六——简体汉字编码中区位码、国标码、内码、外码、字形码的区别及关系

    简体汉字编码中区位码.国标码.内码.外码.字形码的区别及关系 GB2312.GBK.GB18030等GB类汉字编码方案的具体实现方式是怎样的?区位码是什么?国标码是什么?内码.外码.字形码又是什么意思 ...

  8. 【转】刨根究底字符编码【2.0版】(1):开篇

    首先跟大家分享一个有趣的亲身经历.有一次,在网上我看到有程序员发了一个帖子,帖子题目乍一看让人感到惊愕,但细一想又让我会心一笑. 这个帖子的题目大致上是这样的:字符编码是不是让程序员最感到恶心的问题? ...

  9. 【转】刨根究底字符编码之十三——UTF-16编码方式

    1. UTF-16编码方式源于UCS-2(Universal Character Set coded in 2 octets.2-byte Universal Character Set).而UCS- ...

最新文章

  1. 中科院遗传发育所王秀杰团队鉴定出10种潜在的2019-nCoV蛋白酶抑制剂
  2. Access中出现改变字段“自己主动编号”类型,不能再改回来!(已解决)
  3. flume数据采集:js埋点
  4. ApkTool反编译出错brut.common.brutexception及java.io.filenotfoundexception 之一
  5. mysql主从不同步 tar_Mysql主从不同步问题处理案例
  6. SpringMVC+uploadify文件上传
  7. 【Python】科赫雪花绘制
  8. 大于等于0小于等于100的正数用正则表达式表示
  9. Laravel自学第一课:laravel下载与安装
  10. mysql 视频教程下载_最全138节Mysql数据库+PHP零基础到精通视频教程【云盘下载】...
  11. 金融衍生品软件产品设计必备知识——外汇相关知识
  12. 从零开始学习 JD CHAIN(一)- 快速部署 JD CHAIN
  13. 暴力解决个localhost跨域问题
  14. 《MultiPoseNet: Fast Multi-Person Pose Estimation using Pose Residual Network》论文阅读
  15. C语言字节对齐规则总结
  16. EL表达式和JSTL标签库学习笔记
  17. qq邮箱日历同步服务器,科技教程:qq邮箱客户端怎么使用exchange服务同步日历?...
  18. 在iPhone上使用3D Touch
  19. Java数组实现进制转换
  20. 超强 Python 数据可视化库,一文全解析

热门文章

  1. Linux下安装和配置solr/tomcat/IK分词器 详细实例二.
  2. js 高阶函数之柯里化
  3. css用hover制作下拉菜单
  4. strcpy、memcpy和memset的区别
  5. Java学习笔记三——数据类型
  6. 每天CookBook之JavaScript-059
  7. uva11991 Easy Problem from Rujia Liu?
  8. 用多媒体库 Bass.dll 播放 mp3 [15] - 设置与获取播放速度
  9. [Leetcode][第117题][JAVA][填充每个节点的下一个右侧节点指针][BFS]
  10. cad钣金展开插件_钣金高级工考试大小头手工展开图步骤教程