`
wxyfighting
  • 浏览: 199482 次
  • 性别: 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没有头部。

分享到:
评论

相关推荐

    JAVA2学习资料--第二部分ppt教程

    【JAVA2学习资料--第二部分ppt教程】 Java作为全球最流行的编程语言之一,因其跨平台、面向对象的特点,被广泛应用于各种领域,如企业级应用开发、移动应用(Android)、大数据处理等。本教程主要针对Java2阶段的...

    2021最新Java面试题全集-2021第二版(20210805版).pdf

    【Java面试题全集2021版】涵盖...以上是部分Java面试中可能涉及的关键知识点,准备面试时应深入理解和实践这些内容,以便能够熟练应对各种面试场景。同时,不断学习和关注最新的技术趋势也是提升自身竞争力的重要方式。

    Java判断文件编码格式 - 明明是悟空 - 博客园1

    在Java编程中,判断文件编码格式是一项常见的任务,特别是在处理包含多国语言或者用户自定义内容的文件时。本文将探讨如何使用Java进行文件编码格式的识别,主要介绍两种方法:一种是简单的UTF-8判断,另一种是使用...

    【IT十八掌徐培成】Java基础第09天-005.String-字符集编码.zip

    另外,`Charset`类是Java提供的一套接口和类,用于处理字符集编码,如获取所有可用的字符集,检测文件的编码等。 此外,`InputStreamReader`和`OutputStreamWriter`类是处理字符流的桥梁,它们允许我们在字节流和...

    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教学ppt--第2-1章_Java_语言基础!!

    Java使用Unicode字符集,提供对非拉丁字符的支持。标识符用于命名变量、常量、类等,应遵循一定的命名规则,如首字符不能为数字,且应有明确的含义。关键字是Java预定义的,具有特殊含义,如`abstract`, `default`, ...

    Web下Java语言如何访问不同字符集的Oracle数据.pdf

    第二种情况更复杂,Java Applet通过宿主机建立数据库链接(Database Link)访问另一台使用西文字符集的Oracle数据库时,需要在两个层次上处理字符集问题:一是宿主机Oracle数据库的字符集,二是远程Oracle数据库的...

    字符集编码的识别(zz)

    字符集编码是将字符与二进制数字之间建立映射关系的一种方法,使得计算机能够理解和处理各种语言的文字。常见的字符集编码有ASCII、GBK、UTF-8等。 这篇由博主Leo在CSDN.NET上发表的文章可能详细阐述了如何识别和...

    JAVA面试题集-整理

    例如,对于两个字符串字面量`"abc"`,第一个`"abc"`会被添加到常量池中,而第二个`"abc"`将直接引用第一个`"abc"`,从而避免了不必要的重复创建。 #### 4. 同步与睡眠方法的区别 在多线程编程中,`wait()`、`sleep...

    java教学ppt--第2-2章_Java_语言基础

    2. **Java的词符集(Token Set)**:Java的词法结构包括关键字、标识符、常量、运算符等,它们构成了程序的基本组成部分。 3. **基本数据类型**:Java有八种基本数据类型,包括整型(byte、short、int、long)、...

    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开发实战经典-课后习题答案-李兴华.pdf

    - **字节流和字符流**: Java提供了字节流(InputStream和OutputStream)和字符流(Reader和Writer)两种方式处理IO。 #### Java 网络编程 - **网络通信**: Java提供了基于Socket的通信方式。 - **URL处理**: Java...

    ISOLatin-1字符集[借鉴].pdf

    2. 完整性:ISOLatin-1字符集包含了大多数拉丁语系文字和标点符号,可以满足大多数文本处理需求。 3. 兼容性:ISOLatin-1字符集与其他字符集如UTF-8、ASCII等具有很好的兼容性。 然而,ISOLatin-1字符集也存在一些...

    JAVA2实用教程-2.pdf

    - **Unicode 字符集**: Java 使用 Unicode 字符集,包含 65535 个字符,支持各种语言文字。 - **关键字**: Java 语言中有 50 个关键字,每个关键字都有特定的意义。 - **示例**: `abstract`、`boolean`、`break` ...

    javabase64-1.3.1.jar

    在Java中,Base64编码的过程是将每三个字节的数据转换为四个Base64字符,不足三个字节的数据会用零填充,然后按照Base64字符集进行转换。Base64字符集包括大小写字母(A-Z,a-z)、数字(0-9)以及"+"和"/"两个特殊...

    Java中的字符集编码入门(六)Java中的增补字符.doc

    本文将深入探讨 Java 对 Unicode 字符集的支持,特别是对增补字符的处理方式。 #### 二、Java 对 Unicode 的支持 Java 一直以来都宣称支持 Unicode,然而在 Java 早期版本中,这种支持并不完善。直到 JDK 5.0 版本...

Global site tag (gtag.js) - Google Analytics