【问题标题】:How to deal with java encoding problems (especially xml)?如何处理java编码问题(尤其是xml)?
【发布时间】:2015-04-03 15:14:55
【问题描述】:

我搜索了有关 java 和 encoding 的信息,但没有找到解释如何处理 java 在编码和解码字符串时出现的公共问题的资源。 有很多关于单个错误的具体问题,但我没有找到针对该问题的广泛响应/参考指南。 主要问题是:

什么是字符串编码?

为什么在 Java 中我可以读取字符错误的文件?

为什么在处理 xml 时出现 Invalid byte x of y-byte UTF-8 sequence 异常?主要原因是什么以及如何避免?

【问题讨论】:

    标签: java xml file encoding


    【解决方案1】:

    由于 Stackoverflow 鼓励自我回答,因此我尝试回应自己。

    编码是将数据从一种格式转换为另一种格式的过程,这个回复我详细介绍了字符串编码在 Java 中的工作原理 (您可能想阅读这篇文章以获得对文本结束编码的更一般的介绍)。

    简介

    字符串编码/解码是将 byte[] 转换为字符串的过程,反之亦然。

    乍一看你可能认为没有问题, 但如果我们更深入地研究这个过程,可能会出现一些问题。 在最低级别,信息以字节存储/传输:文件是字节序列,网络通信是通过发送和接收字节来完成的。 因此,每次您要读取或写入具有普通可读内容的文件时,或者每次提交 Web 表单/读取网页时,都会进行底层编码操作。 我们先从java中基本的String编码操作说起;从字节序列创建一个字符串。 以下代码将 byte[](字节可能来自文件或套接字)转换为 String。

        byte[] stringInByte=new byte[]{104,101,108,108,111};
        String simple=new String(stringInByte);
        System.out.println("simple=" + simple);//prints simple=hello
    

    到目前为止一切顺利,一切都“简单”。字节的值取自here,它显示了一种将字母和数字映射到字节的方法 让我们通过一个简单的要求来使示例复杂化,即 byte[] 包含 €(欧元)符号;糟糕,ascii 表中没有欧元符号。

    这可以大致概括为问题的核心,人类可读的字符(以及一些其他必要的字符,如回车、换行等)超过 256 个, 即它不能只用一个字节表示。 如果由于某种原因您必须坚持使用单字节表示(即第一个编码表仅使用 7 个字节的历史原因、空间限制原因、 如果磁盘上的空间有限,并且您只为英语人编写文本文档,则不需要包含带有重音符号的意大利字母,例如 è,ì) 您会遇到选择哪个的问题 要表示的字符。

    选择编码就是选择字节和字符之间的映射。

    回到欧元示例并坚持使用一个字节 --> 映射 ISO8859-15 编码表的一个字符具有 € 符号; 表示字符串“hello €”的字节序列如下

    byte[] stringInByte1=new byte[]{104,101,108,108,111,32,(byte)164};
    

    你如何“告诉”java 使用哪种编码进行转换? String 有构造函数

    String(byte[] bytes, String charsetName)
    

    允许指定“映射” 如果您使用不同的字符集,您会得到不同的输出结果,如下所示:

        byte[] stringInByte1=new byte[]{104,101,108,108,111,32,(byte)164};
        String simple1=new String(stringInByte1,"ISO8859-15");
        System.out.println("simple1=" + simple1);  //prints simple1=hello €     
    
        String simple2=new String(stringInByte1,"ISO8859-1");
        System.out.println("simple2=" + simple2);   //prints simple1=hello ¤
    

    所以这解释了为什么您读取一些字符并读取不同的用于写入的编码(字符串到字节 [])与用于读取的编码(字节 [] 到字符串)不同。 相同的字节可能会映射到不同编码的不同字符,因此某些字符可能“看起来很奇怪”。
    这些是理解字符串编码所需的基本概念;让我们把事情复杂一点。 可能需要在一个文本文档中表示超过 256 个符号,以实现已创建的这种多字节编码。

    多字节编码不再有一个字节 --> 一个字符映射但有字节序列 --> 一个字符映射

    最著名的多字节编码之一是 UTF-8; UTF-8 是一种可变长度编码,有些字符用一个字节表示,有些则用多个字节表示;

    UTF-8 与一些单字节编码重叠,例如 us7ascii 或 ISO8859-1;它可以看作是单字节编码的扩展。

    让我们看看第一个示例中 UTF-8 的实际应用

        byte[] stringInByte=new byte[]{104,101,108,108,111};
        String simple=new String(stringInByte);
        System.out.println("simple=" + simple);//prints simple=hello
    
        String simple3=new String(stringInByte, "UTF-8");
        System.out.println("simple3=" + simple3);//also this prints simple=hello
    

    正如您在尝试打印 hello 的代码时看到的那样,即在 UTF-8 和 ISO8859-1 中表示 hello 的字节是相同的。

    但是,如果您尝试带有 € 符号的示例,您会得到一个?

        byte[] stringInByte1=new byte[]{104,101,108,108,111,32,(byte)164};
        String simple1=new String(stringInByte1,"ISO8859-15");
        System.out.println("simple1=" + simple1);//prints simple1=hello
    
        String simple4=new String(stringInByte1, "UTF-8");
        System.out.println("simple4=" + simple4);//prints simple4=hello ?
    

    表示无法识别字符并且存在错误。 请注意,即使在转换过程中出现错误,也不会出现异常。

    不幸的是,并不是所有的 java 类在处理无效字符时的行为方式都是一样的。让我们看看当我们处理 xml 时会发生什么。

    管理 XML

    在阅读示例之前,值得记住的是在 Java InputStream/OutputStream 中读取/写入字节和 Reader/Writer 读取/写入字符。

    让我们尝试以不同的方式读取 xml 的字节序列,即读取文件以获取字符串与读取文件以获取 DOM。

        //Create a xml file
        String xmlSample="<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<specialchars>àèìòù€</specialchars>";
        try(FileOutputStream fosXmlFileOutputStreame= new FileOutputStream("test.xml")) {
            //write the file with a wrong encoding
            fosXmlFileOutputStreame.write(xmlSample.getBytes("ISO8859-15"));
        }
    
        try (
                FileInputStream xmlFileInputStream= new FileInputStream("test.xml");
                //read the file with the encoding declared in the xml header
                InputStreamReader inputStreamReader= new InputStreamReader(xmlFileInputStream,"UTF-8");
        ) {
            char[] cbuf=new char[xmlSample.length()];
            inputStreamReader.read(cbuf);
            System.out.println("file read with UTF-8=" + new String(cbuf)); 
            //prints
            //file read with UTF-8=<?xml version="1.0" encoding="UTF-8"?>
            //<specialchars>������</specialchars>
        }
    
    
        File xmlFile = new File("test.xml");
        DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
        DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
        Document doc = dBuilder.parse(xmlFile);     
        //throws  
    

    com.sun.org.apache.xerces.internal.impl.io.MalformedByteSequenceException: 3 字节 UTF-8 序列的字节 2 无效

    在第一种情况下,结果是一些奇怪的字符,但没有异常,在第二种情况下,你得到一个异常(无效序列....) 发生异常是因为您正在读取 UTF-8 序列的三个字节字符,而第二个字节的值无效(因为 UTF-8 编码字符的方式)。

    棘手的部分是,由于 UTF-8 与其他一些编码重叠,3 字节 UTF-8 序列异常的无效字节 2 出现“随机” (即仅适用于字符超过一个字节的消息),因此在生产环境中,错误可能难以跟踪和重现。

    通过所有这些信息,我们可以尝试回答以下问题:

    为什么在读取/处理 xml 文件时会出现 Invalid byte x of y-byte UTF-8 sequence 异常?

    因为写入的编码(上面测试用例中的 ISO8859-15)和读取用的编码(上面测试用例中的 UTF-8)不匹配;不匹配可能有一些不同的原因:

    1. 您在字节和字符之间进行了一些错误的转换:例如,如果您正在使用 InputStream 读取文件并转换为 Reader 并将 Reader 传递给 xml 库 您必须在以下代码中指定字符集名称(即您必须知道用于保存文件的编码)

      try ( FileInputStream xmlFileInputStream= new FileInputStream("test.xml"); //this is the reader for the xml library (DOM4J, JDOM for example) //UTF-8 is the file encoding if you specify a wrong encoding or you do not apsecify any encoding you may face Invalid byte x of y-byte UTF-8 sequence Exception InputStreamReader inputStreamReader= new InputStreamReader(xmlFileInputStream,"UTF-8"); )

    2. 您将 InputStream 直接传递给 xml 库,但该文件不正确(如第一个管理 xml 的示例,其中标头状态为 UTF-8,但实际编码为 ISO8859-15。 仅仅放入文件的第一行是不够的;文件必须使用标头中使用的编码保存。

    3. 您正在使用未指定编码的阅读器读取文件,并且平台编码与文件编码不同:

      FileReader fileReader=new FileReader("text.xml");
      

    这导致一个方面,至少对我而言,它是 java 中大多数字符串编码问题的根源:使用默认平台编码

    当你打电话时

    "Hello €".getBytes();
    

    你可以在不同的操作系统上得到不同的结果;这是因为在 Windows 上默认编码是 Windows-1252,而在 linux 上可能是 UTF-8; € char 的编码方式不同,因此您不仅会获得不同的字节,还会获得不同的数组大小:

        String helloEuro="hello €";
        //prints hello euro byte[] size in iso8859-15 = 7
        System.out.println("hello euro byte[] size in iso8859-15 = " + helloEuro.getBytes("ISO8859-15").length);
        //prints hello euro byte[] size in utf-8 = 9
        System.out.println("hello euro byte[] size in utf-8 = " + helloEuro.getBytes("UTF-8").length);
    

    遇到编码问题时,首先要检查的是使用 String.getBytes() 或 new String(byte[] ...) 而不指定编码

    第二个是检查你是否正在使用 FileReader 或 FileWriter 读取或写入文件;在这两种情况下,documentation 状态:

    这个类的构造函数假定默认的字符编码和默认的字节缓冲区大小是可以接受的

    与 String.getBytes() 一样,在不同平台上使用读写器读取/写入同一个文件,并且不指定字符集可能会由于不同的默认平台编码而导致不同的字节序列

    正如 javadoc 建议的那样,解决方案是使用将 OutputStream/InputStream 与字符集规范包装在一起的 OutputStreamReader/OutputStreamWriter。

    关于某些 xml 库如何读取 XML 内容的一些最后说明:

    1. 如果您传递 Reader,则库依赖于 reader 进行编码(即,它不检查 xml 标头的内容)并且不会对编码进行任何处理,因为它读取的是字符而不是字节。

    2. 如果你传递一个 InputStream 或一个文件库依赖于 xml 标头进行编码,它可能会抛出一些编码异常

    数据库

    处理数据库时可能会出现不同的问题;创建数据库时,它具有用于保存 varchar 和字符串列(作为 clob)的编码属性。 如果数据库是使用 8 位编码(例如 ISO8859-15)创建的,当您尝试插入编码不允许的字符时,可能会出现问题。 保存在 db 上的内容可能与在 Java 级别指定的字符串不同,因为 Java 字符串在内存中以 UTF-16 表示,它比在数据库级别指定的字符串“更宽”。 最简单的解决方案是:使用 UTF-8 编码创建数据库。

    网络 this 是一个很好的起点。

    如果您觉得缺少某些内容,请随时在 cmets 中要求更多内容。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-06-11
      • 1970-01-01
      • 2010-10-05
      • 1970-01-01
      • 2019-03-13
      • 1970-01-01
      • 2020-10-12
      • 1970-01-01
      相关资源
      最近更新 更多