尚学堂.张志宇.乱码分析_04_读取硬盘上的文件.doc_JAVA_编程开发_程序员俱乐部

中国优秀的程序员网站程序员频道CXYCLUB技术地图
热搜:
更多>>
 
您所在的位置: 程序员俱乐部 > 编程开发 > JAVA > 尚学堂.张志宇.乱码分析_04_读取硬盘上的文件.doc

尚学堂.张志宇.乱码分析_04_读取硬盘上的文件.doc

 2012/1/31 9:22:37  bjrobin  程序员俱乐部  我要评论(0)
  • 摘要:为什么是?í?ó1结论?J2SE5.0用的是Unicode4.0?J2SE6.0用的也是Unicode4.0?Java编程语言用16位的编码代表文本。使用UTF-16编码.?一个char表示一个UTF-16编码单元?并不是一个char代表一个字符,因为一个增补字符需要2个char来代表?所有iso-8859-1字符都被收录到unicode?有些unicode字符(同时也被iso-8859-1收录的)没有对应的gbk编码,即使能找到对应的全角的字符。也已经是另外一个unicode字符了
  • 标签:文件 分析 硬盘

为什么是?í?ó
1 结论
? J2SE 5.0 用的是Unicode 4.0
? J2SE 6.0 用的也是Unicode 4.0
? Java编程语言用16位的编码代表文本。使用UTF-16编码.
? 一个 char 表示一个 UTF-16 编码单元
? 并不是一个char代表一个字符,因为一个增补字符需要2个char来代表
? 所有iso-8859-1字符都被收录到unicode
? 有些unicode字符(同时也被iso-8859-1收录的)没有对应的gbk编码,即使能找到对应的全角的字符。也已经是另外一个unicode字符了。

2 准备知识
2.1 “错误”这两个字符的gbk编码是B4ED CEF3
可以靠3个方法来得到gbk编码。
第一:java程序中可以靠下面这段代码得到
String s="错误";
byte [] bytes = s.getBytes("GBK");
for(byte b:bytes){
    System.out.format("%x",b);
}

第二:可以到GBK编码表去验证
http://www.microsoft.com/globaldev/reference/dbcs/936.mspx
http://www.microsoft.com/globaldev/reference/dbcs/936/936_B4.mspx



第三:也可以用记事本创建一个文件,输入这两个字符“错误”,然后存储成默认的ansi编码 (ansi在咱们的winxp操作系统就是指的gbk) 。再用ultraEdit软件打开,切换到16进制形式,也可以得到gbk编码

2.2 “错误”这两个字符的utf-16编码是9519 8bef
可以靠下面这段代码得到
这里得解释一下:输出的结果是feff95198bef
其中,feff指的是大头方式。后面的95198bef才是utf-16编码的内容
String s="错误";
byte [] bytes = s.getBytes("utf-16");
for(byte b:bytes){
    System.out.format("%x",b);
}
或者靠下面这段代码得到utf-16编码,这里直接把内存里面的内容输出。JVM内部,就是用utf-16编码来表示这个字符串的。

public static String getUnicodeFromStr(String s) {
String retS = "";
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
retS += String.format("%1$04x", (int) c) + " ";
}
return retS;
}

可以参考从unicode官方网站上下载下来的《U4E00.pdf》确认此事。
其实U4E00.pdf里面查到的只是代码点。
而utf-16编码,对于普通字符来说(即不是增补字符),和代码点是一致的。

3 现象描述
假设有个类叫TestFileInputStream_1,读取一个同目录下面的文本文件01.txt。这个文本文件只有两个汉字:“错误”
运行java TestFileInputStream_1
得到的结果是?í?ó
为什么?
//注意:类名不要和jdk里面的类名重复
import java.io.*;

class TestFileInputStream_1 {
public static void main(String[] args) throws Throwable {
FileInputStream in = new FileInputStream("01.txt");
int b = 0;
b = in.read();
System.out.print((char) b);
b = in.read();
System.out.print((char) b);
b = in.read();
System.out.print((char) b);
b = in.read();
System.out.print((char) b);
}
}

4 分析
4.1 文件01.txt是按照gbk编码存储的
FileInputStream in = new FileInputStream("01.txt");
首先,01.txt文件的内容是按照gbk编码存储的。所以硬盘上的内容应该是0xB4ED 0xCEF3
4.2 读入一个字节
其次看这两句话
int b = 0;
b = in.read();
这会读入“错”这个字符的gbk编码的前一个字节。B4
读进来的内容赋值给了int类型的b,内存里面是
b?00000000 00000000 00000000 10110100
4.3 转换成char
(char) b
接下来强制转换成char,char在内存里面是两个字节,把这4个字节的头2个截去。只留下后面2个字节。
内存?00000000 10110100 (0x00B4)

那么这两个字节0x00B4代表哪个字符呢?
JVM使用UTF-16编码代表一个unicode字符。普通字符的UTF-16编码和代码点是对应的。代码点是00B4的那个字符是 。可以到《U0080.pdf》里去看下。这个pdf文档是从unicode官方网站上下载下来的。
在unicode中代码点是00B4的这个字符在对应着iso-8859-1里面的字符。
unicode和iso-8859-1两种字符集的对应关系可以看这个文件来验证:
《8859-1.TXT》这是从官网上下载来的。
可以看到unicode的0x00B4字符对应iso-8859-1的0xB4字符
4.4 打印char
接下来
System.out.print((char) b);
System.out是PrintStream类型的,查看api文档,关于print方法是这样描述的:
中文:
打印字符。按照平台的默认字符编码将字符转换为一个或多个字节,并完全以 write(int) 方法的方式写入这些字节。
英文:
Print a character. The character is translated into one or more bytes according to the platform's default character encoding, and these bytes are written in exactly the manner of the write(int) method.

为了验证传到dos窗口的是一些gbk编码。可以运行这段代码
FileOutputStream fo = new FileOutputStream("02.txt");
PrintStream ps = new PrintStream(fo);
System.setOut(ps);
int b ;
b= 0xB4;
System.out.print((char)b);
b = 0xED;
System.out.print((char)b);
ps.close();
用ultraEdit软件打开02.ext,切换到16进制表示,看到的是3FA8AA,这些就是gbk编码!

也就是说,print方法不会默认把内存里面的字节内容传给dos窗口,而是会进行某种编码,(我们用的winxp操作系统默认编码就是gbk),把编码的结果(就是一些字节呗)传给dos窗口。
编码结果是3f,这个过程可以靠下面的代码来模拟。
char[] chars = new char[]{(char)0xb4};
byte[] bytes = new String(chars).getBytes("gbk");
for(byte b:bytes){
    System.out.format("%x",b);
}

可是按照gbk编码得到的结果是3f为什么呢?
这个问题也困扰了我很久,下面说原因。
首先,按照gbk编码的意思是到gbk编码表里面找找看有没有这个字符。只要gbk编码表没有收录这个字符。JVM不会抛异常,而是统统默认返回一个字节3f。
那么如何知道gbk里面有没有收录这个字符?gbk编码和unicode字符集中的代码点是有对应关系的。查看这个文件《CP936.TXT》,这是从官方网站上下载下来的。这个文件收录了所有的gbk编码和unicode字符的代码点的对照关系。
打开这个文件,在第二列搜索一下00B4,是不是没有这个unicode字符?也就是说这个字符没有被gbk编码表收录!
gbk编码表兼容ascii,编码得到的3f在ascii里面是“?”这个字符。所以您看到的是一个“?”
到这里验证:
http://msdn.microsoft.com/zh-cn/goglobal/cc305153(en-us).aspx


ISO 8859-1,正式编号为ISO/IEC 8859-1:1998,又称Latin-1或“西欧语言”,是国际标准化组织内ISO/IEC 8859的第一个8位字符集。它以ASCII为基础,在空置的0xA0-0xFF的范围内,加入192个字母及符号,藉以供使用附加符号的拉丁字母语言使用。
但这个unicode字符0x00B4居然在GBK没有被收录?
你可能感觉很意外。我也是。
再跟我做个实验。

打开这个地址:
http://zh.wikipedia.org/w/index.php?title=ISO/IEC_8859-1&variant=zh-cn
找到B4位置上的这个字符。拷贝这个字符。

新建一个文本文件。粘贴,存盘。

如果无法打开这个网址也可以这样。新建一个文本文件,打开,按住alt键,输入180,然后抬起alt键盘。呵呵,雕虫小技而已。(180是0xB4的10进制)


这是你会得到一个提示: 
不管这个提示。点击确定。然后关闭再打开。你会看到这个字符还在,只是样子有点怪,比刚才的大了些。



用ultrait软件打开,切换到16进制表示方式。看到了哪两个字节?
是A1E4。这是某个字符的gbk编码。再打开下面的网址去看看是哪个字符?
http://www.microsoft.com/globaldev/reference/dbcs/936/936_A1.mspx

好奇怪把。字符是有。但不是原来那个。因为原来拷贝粘贴的是ISO 8859-1里的0xB4这个字符,这个字符对应着unicode里面的0x00B4字符,gbk没有对应着unicode里面的0x00B4字符的字符。但是记事本帮你转化成了unicode里面的另外一个字符。0x2032字符!

打开《U0080.pdf》可以看到0x00B4字符。这个是半角的
打开《U2000.pdf》可以看到0x2032字符。这个是全角的。
记事本帮你做自动的转换,因为没法在gbk中找到对应的0x00B4字符,自动帮你转换为全角的0x2032字符。
换句话说,gbk只收录了全角的没有收录半角的。
但是java不会帮你做这个转换。如果gbk编码表没有收录这个字符。会默认返回一个字节3f。
3f在ascii里面是“?”这个字符。所以您看到的是一个“?”

4.5 来总结一下
? 文件存储为gbk编码0xB4ED 0xCEF3
? 读到第一个字节到内存里,0xB4
? 再转换成char,内存里面是0x00B4
? 再编码成gbk,3f
? dos窗口在解码成字符显示出来。?
unicode iso-8859-1 gbk
半角0x00B4 0xB4 未收录(3f)
全角0x2032 - A1E4


4.6 第二个读入的字节有所不同。
再执行这句话
b = in.read();
这会读入“错”这个字符的gbk编码的第二个字节。0xED
读进来的内容赋值给了int类型的b,内存里面是
b?00000000 00000000 00000000 11101101
4.7 转换成char
(char) b
接下来强制转换成char,char在内存里面是两个字节,把这4个字节的头2个截去。只留下后面2个字节。
内存?00000000 11101101 (0x00ED)

那么这两个字节00000000 11101101代表哪个字符呢?
JVM使用UTF-16编码代表一个unicode字符。普通字符的UTF-16编码和代码点是对应的。代码点是0x00ED的那个字符是 。可以到《U0080.pdf》里去看下。这个pdf文档是从unicode官方网站上下载下来的。
在unicode中代码点是0x00ED的这个字符在对应着iso-8859-1里面的字符。
unicode和iso-8859-1两种字符集的对应关系可以看这个文件来验证:
《8859-1.TXT》这是从官网上下载来的。
可以看到iso-8859-1的0xED字符对应unicode的0x00ED字符

4.8 打印char
接下来
System.out.print((char) b);
根据刚才的分析,print方法不会默认把内存里面的字节内容传给dos窗口,而是会按照gbk进行编码,把编码的结果(就是一些字节呗)传给dos窗口。
这次,得到的编码结果不再是3F,而是A8AA,前面可以找到验证代码
这个过程可以靠下面的代码来模拟。
char[] chars = new char[]{(char)0xED};
byte[] bytes = new String(chars).getBytes("gbk");
for(byte b:bytes){
    System.out.format("%x",b);
}

这次为什么是A8AA而不是3f呢?
因为gbk编码表收录了这个字符嘛。当然会找到相应的gbk编码!
如何证明?很简单,查看这个文件《CP936.TXT》,在第二列搜索一下00ED,是不是找到了!
又很意外?
再跟我做个实验。

打开这个地址:
http://zh.wikipedia.org/w/index.php?title=ISO/IEC_8859-1&variant=zh-cn
找到ED位置上的这个字符。拷贝这个字符。

新建一个文本文件。粘贴,存盘。
如果无法打开这个网址也可以这样。新建一个文本文件,打开,按住alt键,输入237,然后抬起alt键盘。 (237是0xED的10进制)

这次很顺利,再关闭,再打开好像还是刚才那个字符。
用ultraedit软件打开,切换到16进制表示方式。看到了A8AA两个字节。
这就是该unicode字符的gbk编码。再打开下面的网址去看看是哪个字符?
再打开gbk编码表来验证。http://www.microsoft.com/globaldev/reference/dbcs/936/936_A8.mspx
是不是看到了这个字符?


JVM会把编码的结果0xA8AA传给dos窗口,dos窗口再按照gbk解码,正确的显示出来。
由于gbk收录了这个字符,所以您能够看到正确的结果。
4.9 来总结一下
? 文件存储为gbk编码0xB4ED 0xCEF3
? 读第二个字节到内存里,0xED
? 再装换成char,内存里面是0x00ED
? 再编码成gbk,A8AA
? dos窗口在解码成字符显示出来。í
unicode iso-8859-1 gbk
半角0x00ED 0xED A8AA
全角 - -
好像unicode没有定义该字符对应的全角的字符。我不确定这个。如果哪个同学在unicode字符集中找到了对应的全角的字符。希望你告诉我一下。

后两个字节自己分析吧。


5 参考
http://zh.wikipedia.org/w/index.php?title=ISO/IEC_8859-1&variant=zh-cn
http://msdn.microsoft.com/zh-cn/goglobal/cc305153(en-us).aspx

  • 尚学堂.张志宇.乱码分析_04_读取硬盘上的文件.rar (181.5 KB)
  • 下载次数: 0
发表评论
用户名: 匿名