乱码初识(下)

上节 中我们可以知道,二进制数据被多次改变造成的乱码恢复过程是比较繁琐的,而且一旦二进制数据被破坏,则乱码无法恢复。

编码格式,这篇文章讲得很详细,http://www.cnblogs.com/andy9468/p/7465489.html。除了里面讲到的几种编码,还有其他的编码,如 ISO 系列IBM系列windows系列x-IBM系列x-windows系列 等等,其实这些编码都是某个地区的编码,就像中国的编码有GBKGB18030GB2312BIG5

小结

​ 从最早的 ASCII 编码到各种各样的编码出现,主要是两个原因,1,早期制定标准不完善导致的。2,为了节省带宽。ASCII是单字节编码方案,只有8位,只能适用于拉丁文字字母。所以后面其他国家,地区推出了兼容ASCII编码的各种各样的方案。到了 UNICODE 的出现,才能够兼容世界各国的文字需求。这时有能够兼容各国的编码方式,但是各个国家,地区还是会尽可能用到自己的编码方式,这就要说到第二个原因,节省带宽。例如欧美地区,用ASCII编码方案只需要一个字节,而如果用UNICODE则需要四个字节(UTF-32,当然还有其他编码方案),可以看到如果欧美地区用UTF32则是用ASCII的四倍。

一,编码转换

​ 乱码恢复前,我们需要知道编码是如何转换的。我们知道 UNICODE 规定了某个字符代表的数字**(这句话很重要,只是规定了字符代表的数字而已,并不是该字符的二进制表示,例如 我 字,UINICODE中代表的数字是 25105,而这个字GBK的二进制数据是:11001110 11010010(十进制:52946),UTF8的二进制数据是:11100110 10001000 10010001(十进制:15108241))**。我们能看到同一个字,在不同的编码方案中却是不一样的编号,如果用GBK 我 字52946的编号去UTF8中查52946编号对应的字符为 ,这样就产生了乱码。从分析中我们可以看出,编码的转换其实就是以UNICODE为中心,即先将其他编码的字符串解码成unicode,再从unicode编码成另一种编码。那么一个GBK的编码是如何转成UNICODE的呢,UNICODE又是怎样转成UTF8呢?

1,GBK编码简介

​ GB2312码表查询地址:http://www.mytju.com/classCode/tools/QuWeiMa_FullList.asp

[GB2312]

在这里插入图片描述

[GBK]

在这里插入图片描述

[GB18030] 在这里插入图片描述

问:GB2312图中,为啥每个字节要加A0H?

答:因为原始区位码与通信控制码(00H~1FH)发生冲突,需要加上20H,其实就得到了国标码,为了兼容ASCII码,所以需要每个字节最高位为1,则每个字节加上80H,这样就相当于每个字节加上A0H。

问:我们总是说中国地区用GBK,GBK是中国的标准码吗?

答:不是的,中国的标准码是GB国标码,1980年,我国颁布了汉字编码的国家标准:GB2312-80。GBK是汉字内码扩展规范,只是因为window的流行而被广泛运用。

问:既然GB18030已经兼容了GB2312和GBK,GBK和GB2312为什么还存在?

答:GB18030是向前兼容GB2312和GBK,其字体库较为庞大,而一些嵌入式设备为了减少体积,往往会选择小体积的GBK字体库。

问:GBK二进制数据转换成UNICODE是否是有规律的

答:没有,GBK映射了UNICODE的编码范围,但是汉字在UNICODE中却不是连续的,例如GBK区位码表定义的第一个汉字是 ,UNICODE编码是 554A , 定义的第二个汉字是 ,UNICODE的编码是 963F

http://www.mytju.com/classCode/tools/QuWeiMa_FullList.asp

GBK是根据拼音定义,16区为汉字开始区

在这里插入图片描述

UNICODE应该是根据汉字结构定义的,4E00为首个汉字

在这里插入图片描述

2,UTF8编码简介

​ UTF8是UNICODE的编码方案,关系密切。

[UTF8]

在这里插入图片描述

[UTF16]

一开始UNICODE使用的是两个字节表示,这时的UTF16对应使用的也是两个字节表示,后面UNICODE扩展为使用四个字节表示,这时UTF16两个字节无法满足需求,则一部分也使用四个字节,偷了个图:

在这里插入图片描述

UNICODE编码转换UTF8:

​ 以U+1D306 在这里插入图片描述,这是一个辅助平面的字符,二进制 1 11010011 00000110

对应UTF8编码规则表,可知对应的是4字节,其结构如下:

1111 0xxx 10xxxxxx 10xxxxxx 10xxxxxx

将二进制数据1 11010011 00000110从左往右填充到结构里,得到如下:

1111 0xxx 10x11101 10001100 10000110

剩余的x填充为0,则得到UTF8二进制编码为:11110000 10011101 10001100 10000110

十六进制数据为:F09D8C86,用Python进行验证:

import binascii
import codecs
if __name__ == "__main__":
	print(binascii.a2b_hex("F09D8C86").decode("UTF8"))  # ?

UNICODE辅助平面字符编码转换UTF16,偷了个图:

在这里插入图片描述

分析:

​ 其中-0x10000是因为是辅助平面的字符,大于0xFFFF,0xFFFF+1就是0x100000,/0x400是因为前10位作为高位,+D800是表明该二进制是4字节读取。

二,代码实现

​ 一旦乱码了,一个个去查找显然是行不通的。那么如何通过代码的方式尽可能的进行乱码恢复呢?Java中对字符串取某个编码的二进制数据getBytes(),对某个二进制数据通过某个编码输出字符串String类第二个参数

String str = "ÎÒ";
String newStr = new String(str.getBytes("windows-1252"),"GBK");
System.out.println(newStr); // ”我“

分析:

​ 上面的代码对乱码进行了恢复,这里假设了原来的编码为GBK,错误的用windows-1252进行编码。有时可能会错误的进行多次编码解码,那么假设原来编码是A,错误编码是B,通过循环可尽可能恢复。

public void recover(str) throws UnsupportedEncodingException{
    String[] charsets = new String[]{"编码1","编码2","编码3","编码4","编码5", "编码6"};
    for(int i = 0; i < charsets.length; i++){
        for(int j = 0; j < charsets.length; j++){
            String newStr = new String(str.getBytes(charsets[i]),charsets[j]);
            System.out.println("原编码:" + charsets[i] + " 错误编码:" + 								charsets[j] + " 转换后字符串:" + newStr)
        }
    }
}
// 改方法有可能跑出异常,是因为有些乱码无法恢复,在编码解码过程中,某个二进制数据无法找到对应的字符,或是某个字符无法在编码中找到对应的二进制数据

Python实现方式

​ Python中也有两个函数是进行编码与解码的,encode与decode。

在这里插入图片描述

三,其他思考
  1. 不能恢复的乱码
if __name__ == "__main__":
    print("我".encode("UTF8")) # b'\xe6\x88\x91'
    print("我".encode("UTF8").decode("GBK","replace")) # 鎴�

字的UTF8二进制数据是3个字节,GBK是两个字节当做一个汉字,所以多出了一个自己会替换成�,如果此时对 鎴� 进行恢复。

print("鎴�".encode("GBK"))

运行后会出现:

在这里插入图片描述

分析:

​ 出现该报错是�在UNICODE中的定义为在这里插入图片描述,而GBK总体编码范围为 8140-FEFE,不包括FFFD。那么这时候可以在encode方法填入第二个参数"ignore"。

print("鎴�".encode("GBK","ignore")) # b'\xe6\x88'

但是此时就从原本的 b’\xe6\x88\x91’ 变为了 **b’\xe6\x88’ **了,二进制编码丢失,那么以后就无法进行恢复。

  1. 尽可能保证乱码恢复过程中,二进制数据不丢失

    上面通过设置“ignore"参数,使得二进制编码丢失了一个字节。

    print("鎴�".encode("GBK","ignore").decode("UTF8"))
    

    结果是报错,因为 e6 88 二进制为: 00001100 11100110 10001000,根据UTF8规则,这个数据是无法解析的。回顾上面的代码print(“我”.encode(“UTF8”).decode(“GBK”,“replace”)),我们可以知道,关键在于decode的第二个参数,“replace",当无法解析\x91,我们采用了”replace",替换成了�,而�的二进制数据是\xff\xfd,二进制数据被破坏了。那么有没有办法在恢复的过程中,尽量不丢失二进制数据呢?

    import binascii
    import codecs
    def encodeTest(exc):
        if not isinstance(exc, UnicodeEncodeError):
            raise TypeError("don't know how to handle %r" % exc)
        l = []
        for c in exc.object[exc.start:exc.end]:
            l.append("%x" % ord(c))
        return (binascii.a2b_hex("".join(l)), exc.end)
    
    def decodeTest(exc):
        if not isinstance(exc, UnicodeEncodeError):
            decode_str = exc.object[exc.start:exc.end].decode("ISO 8859-1", "ignore")
            if decode_str == "":
                raise TypeError("don't know how to handle %r" % exc)
            else:
                return decode_str, exc.end
        else:
            return "", exc.end
    
    if __name__ == "__main__":
        codecs.register_error("encodeTest", encodeTest)
        codecs.register_error("decodeTest", decodeTest)
        print("我".encode("UTF8")) # b'\xe6\x88\x91'
        print("我".encode("UTF8").decode("GBK","decodeTest")) # 鎴‘
        print("鎴‘".encode("GBK","encodeTest")) # b'\xe6\x88\x91'
        print("鎴‘".encode("GBK","encodeTest").decode("UTF8")) # 我
    
    

    *注意:*markdown的代码块无法正常显示 鎴‘

    总结:

    ​ 上面的代码在decode数据 b’\xe6\x88\x91’ 遇到 \x91时,调用了自定义的一个解码错误接受函数decodeTest,在该函数里,我们将\x91用编码ISO 8859-1 进行解码,为什么选用这个呢,因为ISO 8859-1 是单字节,那么即使是错误也是每个字节解码错误,不会导致增加或减少字节,然后encode时,遇到无法解析的字符,只需要正常返回其二进制数据即可(encodeTest函数实现),那么最后就可以正确恢复乱码。

四,结尾

​ 对编码,解码,乱码的学习就此告一段落了,出现那么多编码,让我们学习起来困难与复杂,无非就是前期的规范化做得不够好。本身写代码的过程其实也是,有时前期欠缺考虑的东西会成为后期的技术债务,也许这是无非避免了,就连世界组织定义编码也是一步一步规范化,但希望以后自己在写代码的过程中先多多思考,再实现具体逻辑。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值