`
wxyfighting
  • 浏览: 191098 次
  • 性别: Icon_minigender_1
  • 来自: 上海
文章分类
社区版块
存档分类
最新评论

java处理字符集-第二部分-文件字符集

 
阅读更多

前面有一篇文章提及到乱码的产生:http://blog.csdn.net/xieyuooo/article/details/6919007

那么知道主要原因是编码和解码方式不一样,那么有些时候如果我们知道编码方式,那么解码自然很好搞,例如输出的contentType会告诉浏览器我输出的内容是什么编码格式的,否则浏览器会才用一个当前默认的字符集编码来处理;本文要将一些java如何处理没有带正常协议头部的字符集应当如何来处理。

这里就说的是文件字符集,在了解字符集之前,回到上一篇文章说到默认字符集,自定义字符集,系统字符集,那么当前环境到底用的什么字符集呢?

System.out.println(Charset.defaultCharset());

当前java应用可以支持的所有字符集编码列表:

Set<String> charsetNames = Charset.availableCharsets().keySet();
for(String charsetName : charsetNames) {
System.out.println(charsetName);
}

因为java的流当中并没有默认说明如何得知文件的字符集,很神奇的是,一些编辑器,类似window的记事本、editplus、UltraEdit他们可以识别各种各样的字符集的字符串,是如何做到的呢,如果面对上传的文件,需要对文件内容进行解析,此时需要如何来处理呢?


首先,文本文件也有两种,一种是带BOM的,一种是不带BOM的,GBK这系列的字符集是不带BOM的,UTF-8、UTF-16LE、16UTF-16BE、UTF-32等等不一定;所谓带BOM就是指文件【头部有几个字节】,是用来标示这个文件的字符集是什么的,例如:

UTF-8 头部有三个字节,分别是:0xEF、0xBB、0xBF

UTF-16BE 头部有两个字节,分别是:0xFE、0xFF

UTF-16LE 头部有两个字节,分别是:0xFF、0xFE

UTF-32BE 头部有4个字节,分别是:0x00、0x00、0xFE、0xFF

貌似常用的字符集我们都可以再这得到解答,因为常用的对我们的程序来讲大多是UTF-8或GBK,其余的字符集相对比较兼容(例如GB2312,而GB18030是特别特殊的字符才会用到)。

我们先来考虑文件有头部的情况,因为这样子,我们不用将整个文件读取出来,就可以得到文件的字符集方便,我们继续写代码:

通过上面的描述,我们不难写出一个类来处理,通过inputStream来处理,自己写一个类:

import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;


public class UnicodeInputStream extends InputStream {
    PushbackInputStream internalIn;    
    boolean isInited = false;    
    String defaultEnc;    
    String encoding;   
    private byte[]inputStreamBomBytes;
    
    private static final int BOM_SIZE = 4;    
    
    public UnicodeInputStream(InputStream in) {
        internalIn = new PushbackInputStream(in, BOM_SIZE);    
        this.defaultEnc = "GBK";//这里假如默认字符集是GBK 
        try {    
                init();    
            } catch (IOException ex) {    
                IllegalStateException ise = new IllegalStateException(    
                        "Init method failed.");    
                ise.initCause(ise);    
                throw ise;    
            }    
    }
    
    public UnicodeInputStream(InputStream in, String defaultEnc) {    
        internalIn = new PushbackInputStream(in, BOM_SIZE);    
        this.defaultEnc = defaultEnc;    
    }    
    
    public String getDefaultEncoding() {    
        return defaultEnc;    
    }    
    
    public String getEncoding() {  
        return encoding;    
    }    
    
    /**  
     * Read-ahead four bytes and check for BOM marks. Extra bytes are unread  
     * back to the stream, only BOM bytes are skipped.  
     */    
    protected void init() throws IOException {    
        if (isInited)    
            return;    
    
        byte bom[] = new byte[BOM_SIZE];    
        int n, unread;    
        n = internalIn.read(bom, 0, bom.length);
        inputStreamBomBytes = bom;
    
        if ((bom[0] == (byte) 0x00) && (bom[1] == (byte) 0x00)    
                && (bom[2] == (byte) 0xFE) && (bom[3] == (byte) 0xFF)) {    
            encoding = "UTF-32BE";    
            unread = n - 4;    
        } else if ((bom[0] == (byte) 0xFF) && (bom[1] == (byte) 0xFE)    
                && (bom[2] == (byte) 0x00) && (bom[3] == (byte) 0x00)) {    
            encoding = "UTF-32LE";    
            unread = n - 4;    
        } else if ((bom[0] == (byte) 0xEF) && (bom[1] == (byte) 0xBB)    
                && (bom[2] == (byte) 0xBF)) {    
            encoding = "UTF-8";    
            unread = n - 3;    
        } else if ((bom[0] == (byte) 0xFE) && (bom[1] == (byte) 0xFF)) {    
            encoding = "UTF-16BE";    
            unread = n - 2;    
        } else if ((bom[0] == (byte) 0xFF) && (bom[1] == (byte) 0xFE)) {    
            encoding = "UTF-16LE";    
            unread = n - 2;    
        } else {//没有捕获到的字符集
            //encoding = defaultEnc; //这里暂时不用默认字符集   
            unread = n;    
            //inputStreamBomBytes = new byte[0];
        }    
        // System.out.println("read=" + n + ", unread=" + unread);    
    
        if (unread > 0)    
            internalIn.unread(bom, (n - unread), unread);    
    
        isInited = true;    
    }    
    
    public byte[] getInputStreamBomBytes() {
        return inputStreamBomBytes;
    }


    public void close() throws IOException {    
        isInited = true;    
        internalIn.close();    
    }    
    
    public int read() throws IOException {    
        isInited = true;    
        return internalIn.read();    
    }
}



好了,下面来看看是否OK,我们测试一个文件,用【记事本】打开一个文件,编写一些中文,将文件分别另存为几种字符集,如下图所示:



通过这种方式保存的文件是有头部的,windows里面也保存了这个标准,但是并不代表,所有的编辑器都必须要写这个头部,因为文件上并没有定义如果不写头部,就不能保存文件,其实所谓的字符集,是我们逻辑上抽象出来的,和文件本身无关,包括这些后缀的.txt|.sql等等,都是人为定义的;

好,不带头部的,我们后面来讲,若带有头部,我们用下面的代码来看看是否正确(用windows自带的记事本、UE工具另存为是OK的,用EditPlus是不带头部的,这里为了测试,可以用前两种工具来保存):

我们这里写个组件类,方便其他地方都来调用,假如我们自己定义个叫FileUtils的组件类,里面定义一个方法:getFileStringByInputStream,传入输入流,和是否关闭输入流两个参数(因为有些时候就是希望暂时不关闭,由外部的框架来关闭),再定义一个重载方法,第二个参数不传递,调用第一个方法是,传入的是true(也就是默认情况下我们认为是需要关闭的)。

代码如下(其中closeStream是一个自己编写的关闭Closeable实现类方法,这里就不多说了):

public static String getFileStringByInputStream2(InputStream inputStream , boolean isCloseInputStream) 
              throws IOException {
     if(inputStream.available() < 2) return "";
     try {
          UnicodeInputStream in = newUnicodeInputStream(inputStream);
          String encoding = in.getEncoding();
          int available = inputStream.available();
          byte []bomBytes = in.getInputStreamBomBytes();
          int bomLength = bomBytes.length;
          byte []last = new byte[available + bomLength];
          System.arraycopy(bomBytes , 0 , last , 0 , bomLength);//将头部拷贝进去
          inputStream.read(last , bomBytes.length , available);//抛开头部位置开始读取
          String result = new String(last , encoding);
          if(encoding != null && encoding.startsWith("GB")) {
             return result;
          }else {
             return result.substring(1);
          }
      }finally {
          if(isCloseInputStream) closeStream(inputStream);
      }
}



此时找了几个文件果然OK,不论改成什么字符集都是OK的,此时欣喜了一把,另一个人给了我一个Editplus的文件悲剧了,然后发现没有头部,用java默认的OuputStream输出文件也不会有头部,除非自己写进去才会有,或者说,如果你将头部乱写成另一种字符集的头部,通过上述方面就直接悲剧了


但是如果是不带BOM的,这个方法是不行的,因为没有头部,就没法判定,可以这样说,目前没有任何一种编辑器可以再任何情况下保证没有乱码(一会我们来证明下),类似Editplus保存没有头部的文件,为什么记事本、UE、Editplus都可以认识出来呢(注意,这里指绝大部分情况,并非所有情况);

首先来说下,如果没有头部,只有咋判定字符集,没办法哈,只有一个办法,那就是读取文件字符流,根据字符流和各类字符集的编码进行匹配,来完成字符集的匹配,貌似是OK的,不过字符集之间是存在一个冲突的,若出现冲突,那么这就完蛋了。

做个实验:

写一个记事本或EditPlus,打开文件,在文件开始部分,输入两个字“联通”,然后另存为GBK格式,注意,windows下ASNI就是GBK格式的,或者一些默认,就是,此时,你用任何一种编辑器打开都是乱码,如下所示:


重新打开这个文件,用记事本:


用Editplus打开:


用UE打开:


很悲剧吧,这里仅仅是个例子,不仅仅这个字符,有些其他的字符也有可能,只是正好导致了,如果多写一些汉字(不是从新打开后写),此时会被认出来,因为多一些汉字绝大部分汉字还是没有多少冲突的,例如:联通公司现在表示OK,这是没问题的。


回到我们的问题,java如何处理,既然没有任何一种东西可以完全将字符集解析清楚,那么,java能处理多少,我们能否像记事本一样,可以解析编码,可以的,有一个框架是基于:mozilla的一个叫:chardet的东西,下载这个包可以到http://sourceforge.net/projects/jchardet/files/ 里面去下载,下载后面有相应的jar包和源码,内部有大量的字符集的处理。


那么如何使用呢,他需要扫描整个文件(注意,我们这里没考虑超过2G以上的文件)。

简单例子,在他的包中有个文件叫:HtmlCharsetDetector.java的测试类,有main方法可以运行,这个我大概测试过,大部分文本文件的字符集解析都是OK的,在使用上稍微做了调整而已;它的代码我这就不贴了,这里说下基于这个类和原先基于头部判定的两种方法结合起来的样子;

首先再写一个基于第三包的处理方法:

/**
     * 通过CharDet来解析文本内容
     * @param inputStream 输入流
     * @param bomBytes     头部字节,因为取出来后,需要将数据补充回去
                                          因为先判定了头部,所以头部4个字节是传递进来,也需要判定,而inputStream的指针已经指在第四个位置了
     * @param bomLength    头部长度,即使定义为4位,可能由于程序运行,不一定是4位长度
  这里没有使用bomBytes.length直接获取,而是直接从外部传入,主要为了外部通用
     * @param last          后面补充的数据
     * @return              返回解析后的字符串
     * @throws IOException  当输入输出发生异常时,抛出,例如文件未找到等
     */
    private static String processEncodingByCharDet(InputStream inputStream, 
                                                   byte[] bomBytes,
                                                   int bomLength, 
                                                   byte[] last) throws IOException {
        byte []buf = new byte[1024];
        nsDetector det = new nsDetector(nsPSMDetector.ALL);
        final String []findCharset = new String[1];//这里耍了点小聪明,让找到字符集的时候,写到外部变量里面来下,继承下也可以
        det.Init(new nsICharsetDetectionObserver() {
            public void Notify(String charset) {
                if(CHARSET_CONVERT.containsKey(charset)) {
                    findCharset[0] = CHARSET_CONVERT.get(charset);
                }
            }
        });
        int len , allLength = bomLength;
        System.arraycopy(bomBytes, 0, last, 0, bomLength);


        boolean isAscii = det.isAscii(bomBytes , bomLength);
        boolean done = det.DoIt(bomBytes , bomLength , false);
        BufferedInputStream buff = new BufferedInputStream(inputStream);


        while((len = buff.read(buf , 0 , buf.length)) > 0) {
            System.arraycopy(buf , 0 , last , allLength , len);
            allLength += len;
            if (isAscii) {
                isAscii = det.isAscii(buf , len);
            }
            if (!isAscii && !done) {
                done = det.DoIt(buf , len , false);
            }
        }
        det.Done();
        if (isAscii) {//这里采用默认字符集
            return new String(last , Charset.defaultCharset());
        }
        if(findCharset[0] != null) {
            return new String(last , findCharset[0]);
        }
        String encoding = null;
        for(String charset : det.getProbableCharsets()) {//遍历下可能的字符集列表,取到可用的,跳出
            encoding = CHARSET_CONVERT.get(charset);
            if(encoding != null) {
                break;
            }
        }
        if(encoding == null) encoding = Charset.defaultCharset();//设置为默认值
        return new String(last , encoding);
    }



CHARSET_CONVERT的定义如下,也就是返回的字符集仅仅是可以被解析的字符集,其余的字符集不考虑,因为有些时候,chardet也不好用:

private final static Map<String , String> CHARSET_CONVERT = new HashMap<String , String>() {
        {
            put("GB2312" , "GBK");
            put("GBK" , "GBK");
            put("GB18030" , "GB18030");
            put("UTF-16LE" , "UTF-16LE");
            put("UTF-16BE" , "UTF-16BE");
            put("UTF-8" , "UTF-8");
            put("UTF-32BE" , "UTF-32BE");
            put("UTF-32LE" , "UTF-32LE");
        }
    };


这个方法写好了,我们将原来的那个方法和这个方法进行合并:

   /**
     * 获取文件的内容,包括字符集的过滤
     * @param inputStream    输入流
     * @param isCloseInputStream 是否关闭输入流
     * @throws IOException IO异常
     * @return String 文件中的字符串,获取完的结果
     */
    public static String getFileStringByInputStream(InputStream inputStream , boolean isCloseInputStream) throws IOException {
        if(inputStream.available() < 2) return "";
        UnicodeInputStream in = new UnicodeInputStream(inputStream);
        try {
            String encoding = in.getEncoding();//先获取字符集
            int available = inputStream.available();//看下inputStream一次性还能读取多少(不超过2G文件,就可以认为是剩余多少)
            byte []bomBytes = in.getInputStreamBomBytes();//取出已经读取头部的字节码
            int bomLength = bomBytes.length;//提取头部的长度
            byte []last = new byte[available + bomLength];//定义下总长度
            if(encoding == null) {//如果没有取到字符集,则调用chardet来处理
                return processEncodingByCharDet(inputStream, bomBytes, bomLength, last);
            }else {//如果获取到字符集,则按照常规处理
                System.arraycopy(bomBytes , 0 , last , 0 , bomLength);//将头部拷贝进去
                inputStream.read(last , bomBytes.length , available);//抛开头部位置开始读取
                String result = new String(last , encoding);
                if(encoding.startsWith("GB")) {
                    return result;
                }else {
                    return result.substring(1);
                }
            }
        }finally {
            if(isCloseInputStream) closeStream(in);
        }
    }



外部再重载下方法,可以传入是否关闭输入流;

这样,通过测试,绝大部分文件都是可以被解析的;

注意,上面有个substring(1)的操作,是因为如果带BOM头部的文件,第一个字符(可能包含2-4个字节),但是转换为字符后就1个,此时需要将他去掉,GBK没有头部。

分享到:
评论

相关推荐

    java源码包---java 源码 大量 实例

     Java波浪文字,一个利用Java处理字符的实例,可以设置运动方向参数,显示文本的字符数组,高速文本颜色,显示字体的 FontMetrics对象,得到Graphics实例,得到Image实例,填充颜色数组数据,初始化颜色数组。...

    125集专攻JAVA基础 JAVA零基础入门学习视频教程 动力节点JAVA视频教程.txt

    北京动力节点-Java编程零基础教程-011-Java语言概述-Java的工作原理-源文件及字节码文件.mp4 北京动力节点-Java编程零基础教程-012-Java语言概述-Java的工作原理-JVM分类.mp4 北京动力节点-Java编程零基础教程-...

    JAVA程序开发大全---上半部分

    第2章 MyEclipse集成开发环境的使用 6 2.1 MyEclipse集成开发工具界面 6 2.1.1 MyEclipse的菜单栏 7 2.1.2 MyEclipse的工具栏 13 2.1.3 MyEclipse的透视图 14 2.1.4 MyEclipse的视图 17 2.1.5 MyEclipse的编辑器 20 ...

    Java案例开发锦集

    案例八 飞行文字 案例九 聚光灯效果 案例十 伸展文 第二章 Java与特效 案例1 火焰招牌 案例2 闪电中的城市 案例3 激光绘画 案例4 水面倒影 案例5 图片放大镜 案例6 浮动的气泡...

    java nio 中文版

    第二章 缓冲区 第三章 通道 第四章 选择器 第五章 正则表达式 第六章 字符集 --------------------- 作者:仓鼠洞 来源:CSDN 原文:https://blog.csdn.net/zhiyong499/article/details/78711911 版权声明...

    Java Web编程宝典-十年典藏版.pdf.part2(共2个)

    第2篇为范例演练篇,主要包括基础知识相关、数据库相关、图像与文件相关、图表与报表相关、Ajax等相关的范例;第3篇为项目实战篇,主要包括讯友联系人管理模块、播客视频管理模块、博客管理模块、明日知道论坛管理...

    基于opencv3.1库的JAVA源码

    第2章 OpenCV概述、安装及设定 42 2-2 关于OpenCV 3.0及3.1 43 2-3 使用Java开发OpenCV的缺点 45 2-4 OpenCV的下载及安装 45 2-5 Eclipse设定OpenCV开发环境 46 2-6 整合Java之Eclipse与OpenCV 49 2-7 开发第一个...

    JAVA上百实例源码以及开源项目

     Java波浪文字,一个利用Java处理字符的实例,可以设置运动方向参数,显示文本的字符数组,高速文本颜色,显示字体的 FontMetrics对象,得到Graphics实例,得到Image实例,填充颜色数组数据,初始化颜色数组。...

    成百上千个Java 源码DEMO 4(1-4是独立压缩包)

    Java波浪文字制作方法及源代码 1个目标文件 摘要:Java源码,初学实例,波浪文字 Java波浪文字,一个利用Java处理字符的实例,可以设置运动方向参数,显示文本的字符数组,高速文本颜色,显示字体的 FontMetrics对象,...

    java源码包2

     Java波浪文字,一个利用Java处理字符的实例,可以设置运动方向参数,显示文本的字符数组,高速文本颜色,显示字体的 FontMetrics对象,得到Graphics实例,得到Image实例,填充颜色数组数据,初始化颜色数组。...

    Java程序设计习题集下载

    习题集内容覆盖面广,包括:Java言的基本常识、基本语法、面向对象的基本概念、数组、字符串、异常处理、文件和数据流、图形用户界面设计、小应用程序、线程、编程规范、网络程序设计、多媒体民图形学程序设计以及...

    java源码包4

     Java波浪文字,一个利用Java处理字符的实例,可以设置运动方向参数,显示文本的字符数组,高速文本颜色,显示字体的 FontMetrics对象,得到Graphics实例,得到Image实例,填充颜色数组数据,初始化颜色数组。...

    java源码包3

     Java波浪文字,一个利用Java处理字符的实例,可以设置运动方向参数,显示文本的字符数组,高速文本颜色,显示字体的 FontMetrics对象,得到Graphics实例,得到Image实例,填充颜色数组数据,初始化颜色数组。...

    JAVA WEB 开发详解:XML+XSLT+SERVLET+JSP 深入剖析与实例应用.part2

    21.1.1 常用字符集 610 21.1.2 对乱码产生过程的分析 612 21.2 中文乱码问题的解决方案 614 21.3 使用过滤器解决中文问题 616 21.4 让tomcat支持中文文件名 620 21.5 国际化与本地化 621 21.5.1 locale 621.. ...

    java连接sqlserver示例

    import java.sql.*; import jdbc.DBManager; import jdbc.DBManagerTest; /** * &lt;p&gt;Title: * &lt;p&gt;Description: * &lt;p&gt;Copyright: Copyright (c) 2002 * &lt;p&gt;Company: * @author * @version 1.0 */ /** *...

    JAVA上百实例源码以及开源项目源代码

     Java波浪文字,一个利用Java处理字符的实例,可以设置运动方向参数,显示文本的字符数组,高速文本颜色,显示字体的 FontMetrics对象,得到Graphics实例,得到Image实例,填充颜色数组数据,初始化颜色数组。...

    成百上千个Java 源码DEMO 3(1-4是独立压缩包)

    Java波浪文字制作方法及源代码 1个目标文件 摘要:Java源码,初学实例,波浪文字 Java波浪文字,一个利用Java处理字符的实例,可以设置运动方向参数,显示文本的字符数组,高速文本颜色,显示字体的 FontMetrics对象,...

    Java面试宝典-经典

    1. 判断第二个日期比第一个日期大 82 2. 用table显示n条记录,每3行换一次颜色,即1,2,3用红色字体,4,5,6用绿色字体,7,8,9用红颜色字体。 83 3、HTML 的 form 提交之前如何验证数值文本框的内容全部为数字? ...

    自考04747《Java语言程序设计》课后习题答案全集.doc

    1.6 JAVA语言使用什么字符集?共有多少个不同的字符? 5 1.7 JAVA语言标识符的命名规则是什么? 6 1.8 JAVA有那些基本数据类型,它们的常量又是如何书写的? 6 1.9 指出下列内容哪些是JAVA语言的整型常量,哪些是...

Global site tag (gtag.js) - Google Analytics