前言
编码问题是导致BUG的常见因素之一,尤其是在日常开发的数据处理方面,十个问题七八个是编码导致的。
接下来的篇幅较长,算是阶段性的突发奇想的总结分享吧
未尽事宜或错误可私信或评论指正,谢谢
未授权禁止搬运
涉及知识点
UTF-8 BOM、UTF-8、GBK编码区别
- UTF-8_BOM:Win系统下文件采用UTF8格式时默认使用BOM
- UTF-8:UTF8编码的原生格式,且为Linux系统默认配置
- GBK[全/半角] :windows中国区系统默认配置
这三个都只是字节流的一种编码方式,所以是没有哪一个操作系统支不支持的这一说法,最终都是可以采用直接读取字节流解析的方式来读取识别,所以每次谈及支不支持的问题都只是在说系统或者框架有没有提供便捷接口来读取相应编码格式的字符串。
UTF-8带BOM也没有什么太过特别的,Bom全称Byte order mark,就是在文件开头标记三个字节EF_BB_BF,用来做UTF格式的特殊区分,由于这个格式是Windows推出的,所以在Linux相关平台上的系统软件,不一定默认支持,会因为解析不了这个签名而丢失第一行数据。
这三种编码对于英文/数字的编码都是一致的都是1字节(GBK全角强制2字节),中文UTF-8是2/3字节,GBK为2字节。有个特殊点,为什么在UTF-8里是会有中文字符是2字节的,其实并不是中文字符,而是某些看着像是中文的特殊符号,例如"¥"这个字符就是两字节,但在某种程度上来说,他是可以不被看做中文字符的。再归结到本质上的时候,仅仅只是标准制定者在字符排序的时候,将这个特殊符号排在了前面的两字节区而已,同时刚好这是中国区常用字符,知晓有这样的一个概念即可,在遇到的时候不必惊讶。
GBK和UTF-8是如何识别出中文
UTF8在对字符进行编码时候,采取了位标记的方法,在只有一个字节时,最高的比特位设0。如果有多个字节,那么第一个字节从最高位开始,连续有几个比特位的值为1,就使用几个字节编码,剩下的字节均以10开头。
例(UTF-8):
0xxxxxxx:单字节编码形式,完全等同于ASCII原生编码;
110xxxxx 10xxxxxx:双字节编码形式;
1110xxxx 10xxxxxx 10xxxxxx:三字节编码形式;
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx:四字节编码形式。
GBK策略是中文、英文(全角模式)使用双字节来表示,其中为了区分中文它将中文最高位都设定成1,全角的英文/数字/特殊符号使用0填充高位,半角即ASCII码部分依旧采用的是单字节编码。
例(GBK):
0xxxxxxx:0~127的ASCII部分(半角英文/数字)
1xxxxxxx xxxxxxxx:中文、全角英文/数字
所有的通用编码格式都是基于ASCII的扩展,也就是说都会去做ASCII的兼容,再扩展的所有字符都是各自编码自身的规则。
还有全角和半角,全角[test$%#]都是两字节,半角[test$%#]都是一字节,虽然可能在最终的意思表征上是相同的,但很明显在视觉感受上已经有所不同了,所以完全可以理解为两种字符,并且在内存里对”t”字符全角的记录是#F4A3,半角的记录是#74两个这么看就完全不是一个东西了,所以说无论是UTF-8还是GBK对于0~127序号的ASCII符号都是直接用的1字节存储的,UTF是自然排序为1字节,GBK也不会去做强制2字节转换。
VS这个IDE所采用的编码格式
Visual Studio是MicroSoft的亲儿子,所以理所应当的自身设置会默认使用系统本地编码"system",那么问题就在系统编码是什么样子了,众所周知Windows采取地区划分编码。在中国区会采用GBK编码格式,那么VS就是默认使用GBK编码格式,同时通过VS创建的文件默认的也是GBK字符格式,文件内所写入的内容都是按GBK格式保存,当然VS也提供了改文件保存格式的功能,但是并不建议用,会破坏整体对VS-GBK的公认性和统一性。
简单的例子Qt里面的QString读取字符串,QString(“example中文”)和QString::fromLocal8Bit(“example中文”),在没有一些特殊设置纯默认格式的时候,后者fromLocal8Bit才能正常被识别,因为在代码送进MSVC编译器的时候,是以文本流形式作为输入,所以"example中文"这个字符串就是一串GBK的字节流,如果不用QString::fromLocal8Bit指定一下告诉Qt接下来的字符串是以本地编码格式的,Qt就会默认它是Unicode格式进行解析用Unicode规则去解析GBK字符串怎么想都是会乱码的。
QtCreatorIDE所采用的编码格式
Qt自Qt5开始,全线默认使用UTF-8格式,所以创建文件也是UTF-8(无Bom)格式的。还是举例QString字符串,QString(“example中文”)和QString::fromLocal8Bit(“example中文”),此时乱码的就是后一个,因为文件是UTF-8格式的数据,所以"example中文"这个字符串就是一串UTF-8的字节流。但是fromLocal8Bit确告诉Qt接下来字符串是本地编码格式的,如果本地系统也是UTF8的编码(例如Linux系统编码就是UTF8)还好,那么读取一切正常,但在Windows系统下解析就会异常,因为上面所讲Win平台策略在中国区使用的是GBK编码,这时候就会呈现一个使用GBK规则去转码Unicode的字符串那么就会和上一个情况同质,一定会数据异常。
如何辨别带有中文的字符串是GBK编码还是UTF8编码
在数据处理的项目中,一般情况下都是发送的字符按某一编码格式的二进制字节数组,同时如果你已经知道了解析的正确内容是什么,最简单的办法就是打开断点,进入调试模式查看储存数据的变量的内存值,英文不会乱码,中文部分看是占用几个字节,这个办法不通用,也无法覆盖所有情况,仅是一种快速判断方法而已,但在大部分情况下就已经通用了。
一般来说,很少会有项目用到全角,以及通篇两字节的中文特殊符号,也不会使用Unicode码进行传输,如果遇到这些情况就要结合工程需求,同项目组协作者的字节序列化规则进行特性分析了。
Window和Linux系统各自采用的编码格式
Window系统编码策略采取的区域划分策略,在中国区就会识别(系统内置的)采用GBK编码。在Win系统平台下创建的UTF8文件都会带Bom,同时不带Bom签名的标准UTF8也是完全支持的。UTF-8 Bom编码由来的背景故事可以自行去了解一下,仅是微软由于自身系统历史原因弄出来的一个不得已的妥协做法罢了。
总之在Win平台上要么使用GBK编码,不然就用Utf-8_BOM编码。如果非要用Utf8无Bom来守护标准的话,Win平台一些特定的情况可能会有问题,具体会出什么问题暂时不是这里面的研究范畴,可另开篇幅。(据未证实消息,msvc编译器工程师们是知道有utf8偶然异常这个问题,但是回复是“建议使用utf8-bom”,各自细品吧)
Linux没啥特别的,统一就是Utf-8无Bom编码格式。
OSG对中文数据的编码支持
(另开篇)osg接收的是宽字符,Unicode系列都是属于宽字符之一,它在接收中文时需要使用freetype字体插件,来让他能够解析输出中文,目前来看平常使用的时候只要转换正确了将UTF-8字符(Unicode码)传进去就不会有什么问题。
Unicode和UTF-8和UTF-16和UTF-32
Unicode就是这几个共同的基础标准,然后这三个就是Unicode的不同实现形式。Unicode规定了一百多万个字符的序号,是要实际体现在内存里的,那么内存上存储的形式,就是UTF-8和UTF-16和UTF-32他们各自的规则了。
在很多的自称支持UTF-8的字符数据类型的实现里,它们存储的并不是UTF-8的标准字节数组格式,而是存储的是Unicode值,最终是通过转换出UTF-8字符数组,其实这么做也没什么歧义,标准是标准,但是归结于具体代码实现上就会有多多少少的不同,好比QString需要输入是UTF-8,它的输出也是基于UTF-8,但是它内部实际存的值是Unicode码。
总之标准总是要归到代码实现上的,输入输出是标准格式就好,内部实现随便怎么样都可,无论存的是Unicode码也好,还是说定义了数组来真实存储值也好,没有冲突。另外在我们工作中通用的就是UTF-8,其他的几个可以作为扩展自行了解一下,UTF-16/32都有不同的大小端解释规则,所以又会分两种出来,平常我们几乎用不到,这两个不是太重要。
代码解析1
说明:文件为GBK格式,在VS中编译运行,"test中文"GBK十六进制内存数据为:74 65 73 74 d6 d0 ce c4
例1:
wchar_t* wcht = L"test中文";
数据内存:74 00 65 00 73 00 74 00 2d 4e 87 65
解析:
C语言的宽字符标志’L’,同时还有"_T",但最终底层用的还是’L’,它的作用就是将字符串内的字符使用两字节表示。查看内存发现,'L’将英文使用空位填充为两字节,但是中文的长度没有变,没有做所谓的填充,不过值是变了的,结果还是正确的。结合使用的环境来说,VsIDE-Msvc编译器-VisualC++语言,文本流进入MSVC编译器的时候,MSVC也是一定做了GBK编码指定,所以才能够识别出来"test中文"这样一个本来是GBK的字符串,因为只有识别出来了才能进行正确转换,然后本身GBK的中文就是使用的两字节保存,然后正好符合两字节保存,转换的时候转成2字节的Unicode字符串(非常怪异的描述)。
看似这么想好像逻辑没什么问题,但是研究一下发现 L"test中文" 所返回的数据类型是wchar_t宽字符串,虽然名头上叫的是宽字符串但是底层是用的unsigned short进行的typedef,它只是一个short值,联想一下,这个内存空间里存储的会不会只是对应的Unicode码?
结合上面的内存查看可知#0074->116=‘t’、#0065->101=‘e’、#0073->115=‘s’、#0074->116=‘t’、#4e2d->20013=‘中’、#8765->34661=‘文’
这样得到了一个结果,wchar_t虽然叫做宽字符,但是他并不是一个字符串,里面存储的是每个字符对应的Unicode码,因此对其直接采用memcpy存复制,再用char数组进行文件写入的时候,写入的只是一个Unicode码,文本编辑器不一定能够识别的出来。
实际工程中一般使用C++标准提供的接口,或者一些其他库提供的接口,来对他做一个合理转换得到正确的char*字符串
char test[15] = { 0 };
int iSize = WideCharToMultiByte(CP_ACP, 0, wcht, -1, NULL, 0, NULL, NULL);
WideCharToMultiByte(CP_ACP, 0, wcht, -1, test, iSize, NULL, NULL);
查看内存可知,得到的是74 65 73 74 d6 d0 ce c4,本地windows编码(GBK)字符串结果,至于为什么不是 74 00 ***这样的双字节,见上文对GBK的解释,test属于ASCII部分字符,不会进行0位扩充。
若将CP_ACP换成CP_UTF8,查看内存可知,得到的是74 65 73 74 e4 b8 ad e6 96 87标准的UTF-8字符串结果。
备注: 不同的C库表现不同,目前MSVC上wchar_t是采用的UTF-16的一个子集,只用了2字节,原生的UTF-16是变长的有可能是4字节。同时在Linux的GCC上wchar_t是采用的UCS-4也就是UTF-32,四字节存储,所以合理猜测wchar_t在跨平台上需要特别注意一下。
代码解析2
例2:
char test1[15] = { 0 };
memcpy(test1, “test中文”, sizeof(“test中文”));
数据内存:74 65 73 74 d6 d0 ce c4
解析:
无特殊点,标准GBK字符串内存
代码解析3
例3:
QString str0 = QString::fromLocal8Bit(“test中文”);
数据内存:74 00 65 00 73 00 74 00 2d 4e 87 65
解析:
这我们发现这个值是和wchar_t的值一模一样,一下就想到了,QString内部实现就不是存储标准的UTF-8字符串数组,也难快如此,因为它输出的时候毕竟是可以输出很多编码格式的结果的,如果存储了UTF-8格式数据,每次都要解析一个Unicode码出来,为了效率就不如只存一个Unicode码,在开发中要输出字节的时候在按对应标准组合一个出来。
这里也充分的说明了,只要保证了输入输出符合标准,内部实现怎么样都可以。
代码解析4
例4:
QString str1(“test中文”);
数据内存:74 00 65 00 73 00 74 00 fd ff fd ff fd ff fd ff
解析:
这里我们看到属于中文的那段字符是#fdff,这是Qt对于不可识别字符的特殊处理,结合上面的用例说明,我们输入的是GBK字符串,然后Qt使用了Utf-8格式进行解析,而且内部只记录Unicode码。
结果是只能识别出前面属于ASCII码部分的字符,后面的”中”:11010000 11010110”、文”:11000100 11001110,这四个字节无法和UTF-8的解析规则符合上。然后用单字节解释,又不能被解释成ASCII码,但解析还得进行,所以结果就是我们看到结果就是,每个字节都被记录成了2位的无意义字节#fdff,内存上表现就是4*2=8个字节了。
代码解析5
例5:
QByteArray ba = QString::fromLocal8Bit(“test中文”).toUtf8();
数据内存:74 65 73 74 e4 b8 ad e6 96 87
解析:
这边就呼应了上面所说,内部存的形式和输出无关,在我们调用.toUtf8()的时候,就是告诉QString将内部存储的数据以Utf-8形式进行输出。因此得到的byteArray里面的数据就是标准的UTF-8的字符格式了。
代码解析6
例6:
QString str2(QString::fromLocal8Bit(ba.data()));//ba来自例5
数据内存:74 00 65 00 73 00 74 00 93 6d 5f e1 83 67
解析:
这乍一看我们貌似得到了一个没有什么问题的值,反正骗过了笔者的第一反应,test正常的,中文各三字节,位数也对。但是仔细一想就有不对劲的,QString里面存的是Unicode码,怎么会有三位一字符,所以6位就是有三个字符了!
接下来我们看为什么会出现三个字符,这里面的ba的内存是:74 65 73 74 e4 b8 ad e6 96 87,上文描述这已经是一个UTF-8的字节数组了,然后我们这里指定了fromLocal8Bit,使用本地的编码格式也就是GBK的编码格式来解析一个已经是UTF-8的字符数组,这样就能合理解释这样的结果了,74 65 73 74 ASCII码的翻译没有问题,GBK和UTF-8在这一块都是一致的,但是 e4 b8 ad e6 96 87这个就不对了,fromLocal8Bit告诉了QString接下来的字节流是GBK中文使用2字节解析,于是就得到了#b8e4、#e6ad、#8796,这三个中文然后就是对三个中文进行记录Unicode码,结果就是#6d93=28051->‘涓’、#e15f=57695->‘’(hhCSDN的编辑器也没有识别,我还不太熟悉这个文章编辑器用法,将就着看吧)、#6783=26499->'枃’这三个字了。
备注:
呼应上面讲的,即使解乱码了,当你知晓原理了之后,就还是有对应的方法进行纠错的,因为不管怎么样都是字节流,只要数据没被破坏,就还是完全可控的。虽然是没有什么必要这么做,但是我认为还是需要知晓一下。
现在我们知道这是一个被GBK规则错误组合了的UTF8字符串,就可以逆向还原回去。
QTextCodec*textcode= QTextCodec::codecForName(“GB18030”);
QByteArray ba3 = textcode->fromUnicode(str2);
str2.append(ba3);
只需要这三句,当然这是取巧了使用Qt的Unicode和GBK的对照接口,在原理上就是原本我的错误发生的原因就是我为了得到一个UTF-8的字符串,使用了GBK去解析了一个本来已经是UTF-8的字符串,那么逆向过来就是我将这个结果使用Utf-8的规则解析出一个GBK的字符串出来,实际上这个字符串就已经是Utf-8的了。
通篇讲了很多的按什么什么规则解析,在本质上就是得到一个序号,就是得到一个中文在GBK或者在UTF-8里面的序号值。
原始数据:test中文
GBK编码原始内存:74 65 73 74 d6 d0 ce c4
错误的认为这是一个UTF-8字符串进行解析,就是无法识别,原因在例四讲了,位标志还有序号对不上,只能是#fdff无效值。
UTF-8编码原始内存:74 65 73 74 e4 b8 ad e6 96 87
错误的认为这是一个GBK字符串进行解析,74 65 73 74 e4 b8 ad e6 96 87 会进行两字节解析,然后得到了在GBK里表征的中文序号,
#b8e4=47332->‘涓’、#ade6=44518->‘’、#9687=38535->‘枃’
通过字库表对照,GBK里的中文序号<–>UTF-8里的中文序号对应,得到一个UTF-8里对应的中文序号,这样就是
‘涓’->28051=#6d93、‘’->57695=#e15f、‘枃’->26499=#6783
展开就是74 00 65 00 73 00 74 00 93 6d 5f e1 83 67,这就是本例开头看到的结果。
由此可见,两者进行互相转化的时候,除了按照各自的编码规则将内存里面的数据解析成序号外,最重要的一步就是表对照。即这个实际意义上的中文或者序号在另一个编码内代表了什么,然后根据字库表对照查找到的结果再处理得到一个二进制的值。
综上:”中文”在GBK内就是#d0d6和#c4ce,对照至UTF-8里面就是 #e4bad8和#e69687,注意平常大家说的那个字符编码转换里提到的”解析”(见例7),仅是在一个字节流里按规则取到对应的一个一个值可被称之为解析,然后就进入GBK表和Unicode字库表对照的环节了。
代码解析7
例7:
QString str3(QString::fromLocal8Bit(“test中文”));
数据内存:74 00 65 00 73 00 74 00 2d 4e 87 65
解析:
根据分析代码,这个值是没有问题的,而且我们也知道了QString里面存储的是字符的Unicode值。
UTF-8编码原始内存是74 65 73 74 e4 b8 ad e6 96 87
数据内存是74 00 65 00 73 00 74 00 2d 4e 87 65
这时候来看两者是怎么对照的,第一步先将原始内存用二进制扩展开,现在是16进制的(以下使用e4 b8 ad 作为举例)。
UTF-8:11100100 10111000 10101101
见上文可知1110**** 10****** 10******是用来位标记的,所以我们直接去除掉这几位来看,0100 111000 101101,进行8位组合可知01001110 00101101,翻译成16进制就是#4e2d,在内存中就是2d 4e。
这样就可以很直观的看出来,”2d 4e”代表的是QString的内部实现它就是存了一个确切的序号值,直接翻译就是对应的Unicode码。而”e4 b8 ad”才是标准的UTF-8的内存格式,”2d 4e”就藏在”e4 b8 ad”中。
结合上一个例子说的”转换、解析”环节,就只是如何将这序号本来的2字节对应的二进制码,按规则转换为标准的3字节流进入交互。反过来解析就是如何从3字节的Utf-8的字节数组里读出它真实的2字节Unicode码。