Java学习日记~2020年10月27日
数据背后的二进制
十进制与二进制的转换
十进制转化成二进制主要是通过每次除以2然后取余数,直到商小于1为止,然后再把结果倒过来就是相对应的二进制了。就拿666为例子
非Unicode编码
编码有两大类:一类是非Unicode编码;另一类是Unicode编码。一些主要的非Unicode编码,包括ASCII、ISO 8859-1、Windows-1252、GB2312、GBK、GB18030和Big5。
ASCll
在计算机发明之初,由于计算机是美国发明的,所以美国只考虑了自己的需求,规定了128个字符的二进制表示方法。对于美国人而言,128个字符已经够了~
这个方法是一个标准,称为ASCII编码,全称是American Standard Code for Information Interchange,即美国信息互换标准代码。128个字符用7位刚好可以表示,计算机存储的最小单位是byte,即8位,ASCII码中最高位设置为0,用剩下的7位表示字符。这7位可以看作数字0~127,ASCII码规定了从0~127的每个数字代表什么含义。
ASCII码对美国是够用了,但对其他国家而言却是不够的,于是,各个国家的各种计算机厂商就发明了各种各种的编码方式以表示自己国家的字符,为了保持与ASCII码的兼容性,一般都是将最高位设置为1。也就是说,当最高位为0时,表示ASCII码,当为1时就是各个国家自己的字符。在这些扩展的编码中,在西欧国家中流行的是ISO 8859-1和Windows-1252,在中国是GB2312、GBK、GB18030和Big5
ISO 8859-1
ISO 8859-1又称Latin-1,它也是使用一个字节表示一个字符,其中0~127与ASCII一样,128~255规定了不同的含义。
Windows-1252
ISO 8859-1虽然号称是标准,用于西欧国家,但实际中使用更为广泛的是Windows-1252 编码,这个编码与ISO 8859-1基本是一样的,区别只在于数字128~159。Windows-1252使用其中的一些数字表示可打印字符 这个编码中加入了欧元符号以及一些其他常用的字符。基本上可以认为,ISO 8859-1已被Windows-1252取代,在很多应用程序中,即使文件声明它采用的是ISO 8859-1编码,解析的时候依然被当作Windows-1252编码。在HTML5里面,如果文件声明的是ISO 8859-1编码,它应该被看作Win-dows-1252编码。
GB2312
是GB2312是中文的第一个标准。GB2312标准主要针对的是简体中文常见字符,包括约7000个汉字和一些罕用词和繁体字。
GB2312固定使用两个字节表示汉字,在这两个字节中,最高位都是1,如果是0,就认为是ASCII字符。在这两个字节中,其中高位字节范围是0xA1~0xF7,低位字节范围是0xA1~0xFE。
GBK
GBK建立在GB2312的基础上,向下兼容GB2312,也就是说,GB2312编码的字符和二进制表示,在GBK编码里是完全一样的。GBK增加了14000多个汉字,共计约21000个汉字,其中包括繁体字。
GB18030
GB18030向下兼容GBK,增加了55000多个字符,共76000多个字符,包括了很多少数民族字符,以及中日韩统一字符。GB18030使用变长编码,有的字符是两个字节,有的是四个字节。在两字节编码中,字节表示范围与GBK一样。在四字节编码中,第一个字节的值为0x81~0xFE,第二个字节的值为0x30~0x39,第三个字节的值为0x81~0xFE,第四个字节的值为0x30~0x39。
Big5
Big5是针对繁体中文的,广泛用于我国台湾地区和我国香港特别行政区等地。Big5包括13000多个繁体字,和GB2312类似,一个字符同样固定使用两个字节表示。在这两个字节中,高位字节范围是0x81~0xFE,低位字节范围是0x40~0x7E和0xA1~0xFE。
Unicode编码
Unicode给世界上所有字符都分配了一个唯一的数字编号。这个编号范围从0x000000~0x10FFFF,包括110多万。但大部分常用字符都在0x0000~0xFFFF之间,即65536个数字之内。每个字符都有一个Unicode编号,这个编号一般写成十六进制,在前面加U+。大部分中文的编号范围为U+4E00~U+9FFF
Unicode主要是给所有字符分配了唯一数字编号。但它并没有规定这个编号怎么对应到二进制表示。Unicode本身只是规定了每个字符的数字编号,而对应二进制表示的方案则有三种,分别是:UTF-32,UTF-16和UTF-8。
UTF-32
UTF-32就是字符编号的整数二进制形式,4个字节。如果第一个字节是整数二进制中的最高位,最后一个字节是整数二进制中的最低位,那这种字节序就叫“大端”(Big Endian,BE),否则,就叫“小端”(Little Endian,LE)。对应的编码方式分别是UTF-32BE和UTF-32LE。
由于每个字符都用4个字节表示,非常浪费空间,所以实际采用的也比较少。
UTF-16
UTF-16使用边长字节表示。常用于系统内部编码,UTF-16比UTF-32节省了很多空间。对于编号在U+0000~U+FFFF的常用字符集内,直接使用两个字节表示。字符值在U+10000~U+10FFFF的字符(也叫做增补字符集),需要用4个字节表示。前两个字节叫高代理项,范围是U+D800~U+DBFF;后两个字节叫低代理项,范围是U+DC00~U+DFFF。数字编号和这个二进制表示之间会有一个转换算法。高位存放在前面就叫大端(BE),编码就叫UTF-16BE,否则就叫小端,编码就叫UTF-16LE。
UTF-8
UTF-8也是使用边长字节表示,每个字符使用的字节个数与其Unicode编号的大小有关,编号小的使用字节就少,编号大的使用字节就多。使用字节的个数是1~4之间不等。小于128的,编码与ASCII码一样,最高位为0。其他编号的第一个字节有特殊含义,最高位有几个连续的1就表示用几个字节表示,而其他字节都以10开头。
编码转换
有了Unicode之后,每一个字符就有了多种不兼容的编码方式。编码转换的具体过程可以是:一个字符从A编码转到B编码,先找到字符的A编码格式,通过A的映射表找到其Unicode编号,然后通过Unicode编号再查B的映射表,找到字符的B编码格式。
乱码的原因
1.解析错误
就和密码的加密解密一样。假如我设置了一个密码,这个密码是521,通过每个数字加一的方法加密后变成了632。那么如果要解密就把它逆着来就行了。假如还有一种加密的方法是把每个数字减一,那么521就是410,解密的时候也自然是用相对应的方法来解密。
当我用第二种加密的解密方案来解密第一种,那么632只会变成743,因此就解析错误了。那么,这个编码的解析错误也是这个道理。
就好比假如我用Windows-1252发送Pékin,然后我们解码的时候用GB18030则会变成“P閗in”,那这就成乱码了。
解决乱码的方式可以先知道它是用什么方式进行编码,然后切换到正确的解码格式查看就可以了。
错误的解析和编码转换
有时候,虽然改变了查看方式,但依然显示的还是乱码。那就有可能不是解析乱码的方式不对,而是文本在错误的解析基础上还进行了编码转换。在计算机程序中,为了便于统一处理,经常会将所有编码转换为一种方式,比如UTF-8,在转换的时候,需要知道原来的编码是什么,但可能会搞错,而一旦搞错并进行了转换,就会出现这种乱码。这种情况下,无论怎么切换查看编码方式都是不行的
从乱码中恢复
恢复的基本思路是尝试进行逆向操作,假定按一种编码转换方式B获取乱码的二进制格式,然后再假定一种编码解读方式A解读这个二进制,查看其看上去的形式,这要尝试多种编码,如果能找到看着正常的字符形式,应该就可以恢复。
Java中处理字符串的类有String,而String中有两个重要的方法。
//这个方法可以获取一个字符串的给定编码格式的二进制形式。
public byte[] getBytes(Charset charset) {
if (charset == null) throw new NullPointerException();
return StringCoding.encode(charset, value, 0, value.length);
}
//这个构造方法以给定的二进制数组bytes按照编码格式charsetName解读为一个字符串
public String(byte bytes[], String charsetName)
throws UnsupportedEncodingException {
this(bytes, 0, bytes.length, charsetName);
}
乱码的恢复方法
public static void function(String str)throws Exception{
String[]charsets = new String[]{
"windows-1252","GB18030","Big5","UTF-8"
};
for(int i = 0; i < charsets.length; i++){
for(int j = 0; j < charsets.length; j++){
if(i != j){
String s = new String(str.getBytes(charsets[i]),charsets[j]);
System.out.println("原来编码 : " + charsets[j] + "被错误解读成 : " + charsets[i]);
System.out.println("s = " + s);
}
}
}
}
不是所有的乱码形式都是可以恢复的,如果形式中有很多不能识别的字符(如?),则很难恢复。另外,如果乱码是由于进行了多次解析和转换错误造成的,也很难恢复。