源代码中文注释出现:“烫烫烫”,“屯屯屯”,“锟斤拷”等中文乱码字符
摘要
在开发中,经常打开一个有中文注释的源代码,注释中经常会出现诸如“烫烫烫”,“屯屯屯”,“锟斤拷”等很奇怪的中文字符,毫无疑问,你遇到中文乱码了,为什么会乱码字符呢?那就得从文件的编码方式说起了。
典型乱码
乱码、问号、方块:用文本编辑器打开一个文本文件,如果文件的编码方式不兼容,有时候会看到???的东西,有时候会看到很混乱或者不认识的文字,通常我们就统称乱码了。怎么用编码的知识来理解呢?
很多编码方式都用的是变长字节编码,很多字节都要结合它的上下文去解释才是对的。例如:用UTF-8的算法去解析GBK的文件,就很容易发些这么些种情况:
- 一个字节序列并不是合法的UTF-8字符,比如以11111110开头的字节序列。
- 一个字节序列碰巧符合UTF-8规则。
- 反过来看,用GBK的算法去解析UTF-8的文件其实也差不多,遇到第一种情况在显示的时候可能就用问号代替,而遇到第二种情况就是出现一些风马牛不相及的杂乱文字。
方块其实和问号本质上一样的,但方块在现代浏览器里还有个很常见的情况,就是一个字符的编号在字体当中并没有定义,于是在排版和渲染的适合“智能”地用一个方块来表示它了。看到方块可以结合上下文,如果上下文当中的非英字符显示正确的,那么方块可能是一些特殊符号,比如Emoji。
在写服务端程序的时候“半个字符”的问题要小心处理。例如,我们在前级对超长的数据进行截断处理,刚好将一个变长编码的字节序列截断掉了,就会出现“半个字符”。一般半个字符都是肯定会乱码的,一些容错比较差的程序甚至会直接挂掉,比如一些做的不好的PHP的C扩展,严重的时候会出core。所以程序不懂编码就别瞎截,甚至考虑到某些语言文字里的组合字符,就是知道编码也别瞎截(真是细思恐极);
BOM
BOM(Byte-Order Mark,字节序标记)是Unicode码点U+FEFF。它被定义来放在一个UTF-16文件的开头,如果字节序列是FEFF那么这个文件就是大端序,如果字节序列是FFFE那么这个文件就是小端序。
UTF-8本身是没有字节序的问题的(因为它是以单个字节为最小单位),但是Windows里面诸如记事本之类的很多编辑器会多此一举的在UTF-8文件开头加入EF BB FF也就是U+FEFF的UTF-8编码。
如果你的源代码文件里面有一个这东西你就倒了大霉了,可能会:
- 什么也看不见,可能是PHP引擎根本处理不了这个源代码。
- 页面展现错乱的情况,一般是因为在
之前输出的非空格内容造成了浏览器选择错误的doctype。 - 页面上面有及格乱七八糟的字符,浏览器把它当字符展示出来了。
于是建议在Windows上做开发的同学,一定要选择“使用UTF-8无BOM格式”保存,所以用记事本写代码装X就不好使了,用Notepad++的可以注意选一下,它支持的文件编码格式挺丰富的,用一些比较先进的跨平台编辑器比如WebStorm、SublimeText它们都是没BOM的。
锟斤拷
乱码之所以叫乱码,就是因为它是“乱”的,或完全无含义的。但是乱码当中最出名的就是“锟斤拷”,他出现次数太多了以至于看起来根本就没那么“乱”。这就纳了闷了,为什么全中国的网站乱码里面都会有这个?
原因是,在将一些国家语言编码体系,比如GB、BIG-5、EUC-JP等,转换为Unicode的过程中,多少有一些字符是不在Unicode中的(比如一些偏旁部首在Unicode里是后来才收录的),甚至它本身在原来的编码体系里面就是非法字符的情况。
Unicode规定了U+FFFD当作一个占位符用来表示这些字符,用UTF-8编码它就是EF BF BD。连续多个这样的字节序列出现就成了EF BF BD EF BF BD。如果是一个UTF-8的解析程序还好,而如果用一个GB的解析程序去打开,一个汉字2字节,就成了“锟斤拷”。这里就是一个例子,用UTF-8编码打开是问号,用GBK编码打开的话就会看到锟斤拷,用hexdump或者UltraEdit这类任何16进制编辑器看的话就能看到里面都是EF BF BD。
要避免锟斤拷一个重要的点就是尽量减少程序当中的编码转换。比如输入是UTF-8,但是一个旧的模块是GBK,把UTF-8转成GBK交给旧的模块处理,处理过程中旧模块多多少少有些BUG的可能,再转回来的时候就容易锟斤拷了。一个项目的源代码在团队里面被不同的人(他们编辑器配置不尽相同)开来开去,存来存去,也很容易出现锟斤拷。
烫烫烫、屯屯屯
这两个汉字和编码转换其实并没有关系,在VC的DEBUG模式下,会把未初始化的栈内存全部填成0xCC,未初始化的堆内存填成0xCD,这样做是让你一眼就能看出来你开了内存没初始化。而用GBK编码时,CC CC就是“烫”,CD CD就是“屯”。
URL Encode和Base64
URL Encode
URL Encode又称为“百分号编码”它主要用来在URI里面将特殊字符进行转义,因为像/、、=等等这类字符在URI里面本身是有功能性的。
对于ASCII字符的编码很简单就是用%后跟ASCII编码的16进制表示,例如/的ASCII char code是47,16进制表示是2F,于是它的URL Encode结果就是%2F。
对于非ASCII字符,将它的每个字节进行相同规则的转换,例如中文“编码”的Unicode char code是U+7F16 7801,UTF-8编码的字节序列是E7 BC 96 E7 A0 81,所以它按照UTF-8编码的URL Encode结果就是%E7%BC%96%E7%A0%81。
可以看出,URL Encode编码非ASCII字符的时候,结果与使用的字符编码有关。因此,在页面上提交表单、发起Ajax请求等操作的时候需要注意编码。浏览器会按照当前页面所使用的字符编码对表单体提交进行URL Encode,但使用JavaScript的encodeURI和encodeURIComponent的时候则总是会使用UTF-8(参考MDN)。
表单提交的时候编码是非常非常重要的,一旦错了服务端解开数据的时候就会跪。比如Github在它们的搜索表单里面放了一个,其中那个对钩✓是U+2713,UTF-8编码是E2 9C 93,他们可以在服务端检测这个参数的值对不对从而对URL里用的编码进行一个初步检测。虽然我没有看到他们使用其他编码的情况,不过这样也算是一个编码协商和Check的手段吧。
在JavaScript中使用escape也可以达到URL Encode的效果,但是它对于非ASCII字符使用了一种非标准的的实现,例如“编码”会被escape成%u7F16%u7801这种%uxxxx奇怪的表示,W3C把这个函数废弃了,身为一名前端还用是打脸的哦。
Base64
Base64是一种用可见字符表示二进制数据的方法。它用了64个可见字符[A-Za-z0-9+/]。
Base64的编码程序非常简单,由于64=2^6,6和8的最小公倍数是24,也就是3byte,因此对输入数据以3byte为一个单位,查表把它转换成4个可见字符。
如果输入末尾不足3byte,那就补足,补1个byte就在输出末尾添加一个=,补2个byte同理。
Base64经常用来在一些文本协议里面保存二进制数据,比如HTTP协议,或者电子邮件的附件啊什么的。同时因为它的输出对于人类而言不可读,可以起到一些“混淆加密”的作用,事实上就有修改64个字符的排布来做一个变形Base64实现一个简单加密算法的例子。从密码学的角度看它基本上没什么强度可言,但是足够简单,可以起到防君子不防小人的作用。
由于一个字符只能编码6bit,自身却占了8bit,8/6=1.33,因此使用Base64来表示数据的时候会浪费1/3的体积。对于在CSS里面用Base64的data-url方式表示图片,用之前不妨简单估算一下,膨胀的体积和一个HTTP请求头比起来会相差多少,说不定涨太多了已经损失掉省一个请求的收益了。
后记
最后,还是要感谢原文作者的辛苦整理和解释。佩服并感谢一下ISO和Unicode联盟,做了这么伟大的事情将全世界的语言文字统一收录和编码,而其中包括了那么多我们根本没听说过的奇怪的语言文字。正是因为他们的努力,才奠定了互联网是一个无国界的领域,每天我们都能通过它获得来自任何地方任何语言的信息。
更新于:3个月前相关文章
- 【说站】python统计不同字符的个数
- 【说站】Java如何在PDF添加注释
- 【说站】Java在PDF中删除注释
- .Net Core HttpClient读取GB2312网页乱码
- 【说站】php包含字符
- .NET Core MVC页面输出中文被编码了
- HTML转义字符对照表
- git status 查看状态文件名乱码
- Linux sed命令关键字匹配文件中整行的任意字符然后替换整行
- 计算机中的注释
- PHP通过CURL的方式读取图片接口输出乱码
- vscode打开代码中文显示乱码的问题
- 用于从字符串中删除最后一个指定字符的 Python 程序
- 用指定字符替换字符串的 Python 程序
- 使用 Python 对相似的开始和结束字符单词进行分组
- Python 程序:查找字符串中的单词和字符数
- rust语言处理含有中文内容文档的处理方法
- 奔驰GitHub私钥外泄 完整源代码和机密数据泄露
- 如何在 Bash 脚本中添加注释
- 解决FTP上传到宝塔面板的文件不显示或乱码