如何处理java编码问题(特别是xml)?

Gio*_*nni 2 java xml encoding file

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

什么是字符串编码?

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

为什么在处理xml时我得到了无效字节x的y字节UTF-8序列异常?主要原因是什么以及如何避免它们?

Gio*_*nni 17

由于Stackoverflow鼓励自我回答,我试着回应自己.

编码是将数据从一种格式转换为另一种格式的过程,这个响应我详细说明了字符串编码在Java中的工作原理(您可能希望阅读本文以获得更为通用的文本结束编码介绍).

介绍

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

初看起来你可能认为没有问题,但如果我们对这个过程有更深入的了解,可能会出现一些问题. 在最低级别,以字节存储/传输信息:文件是字节序列,并且通过发送和接收字节来完成网络通信.因此,每当您想要读取或写入具有简单可读内容的文件时,或者每次提交Web表单/读取网页时,都会有基础编码操作.让我们从java中的基本String编码操作开始; 从字节序列创建一个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
Run Code Online (Sandbox Code Playgroud)

到目前为止这么好,都"简单".字节的值取自这里,它显示了将字母和数字映射到字节的一种方法让我们用简单的要求使样本复杂化,byte []包含€(欧元)符号; 哎呀,ascii表中没有欧元符号.

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

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

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

byte[] stringInByte1=new byte[]{104,101,108,108,111,32,(byte)164};
Run Code Online (Sandbox Code Playgroud)

你如何"告诉"java用于转换的编码?String具有构造函数

String(byte[] bytes, String charsetName)
Run Code Online (Sandbox Code Playgroud)

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

    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 ¤
Run Code Online (Sandbox Code Playgroud)

这就解释了为什么你读取一些字符并读取不同的字符,用于写入的字符串(String to byte [])与用于读取的字节(byte []到String)不同.相同的字节可能会映射到不同编码的不同字符,因此某些字符可能"看起来很奇怪".
这些是理解String编码所需的基本概念; 让这个问题复杂化一点.可能需要在一个文本文档中表示超过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
Run Code Online (Sandbox Code Playgroud)

正如您所看到的那样,它会尝试打印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 ?
Run Code Online (Sandbox Code Playgroud)

意味着不识别char并且存在错误. 请注意,即使转换过程中出现错误,也不会出现异常.

不幸的是,在处理无效字符时,并非所有java类的行为都相同; 让我们来看看处理xml时会发生什么.

管理XML

在通过示例之前值得记住的是,在Java InputStream/OutputStream中读/写字节和Reader/Writer读/写字符.

让我们尝试以某种不同的方式读取xml的字节序列,即读取文件以获取String vs读取文件以获取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  
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

为什么在读取/处理xml文件时,得到y字节UTF-8序列的无效字节x?

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

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

    try(FileInputStream xmlFileInputStream = new FileInputStream("test.xml"); //这是xml库的读者(例如DOM4J,JDOM)//如果指定了错误的编码,UTF-8就是文件编码不apsecify您可能面临的任何编码无效字节x的y字节UTF-8序列异常InputStreamReader inputStreamReader = new InputStreamReader(xmlFileInputStream,"UTF-8");)

  2. 你正在将InputStream直接传递给xml库,但文件不正确的文件(如第一个管理xml的例子,其中头部表示UTF-8,但真正的编码是ISO8859-15. 简单地放入第一行文件是不够的;必须使用标头中使用的编码保存文件.

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

    FileReader fileReader=new FileReader("text.xml");
    
    Run Code Online (Sandbox Code Playgroud)

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

你打电话的时候

"Hello €".getBytes();
Run Code Online (Sandbox Code Playgroud)

您可以在不同的操作系统上获得不同的结果; 这是因为在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);
Run Code Online (Sandbox Code Playgroud)

在不指定编码的情况下使用String.getBytes()或new String(byte [] ...)是遇到编码问题时要做的第一项检查

第二个是检查您是使用FileReader还是FileWriter读取或写入文件; 在这两种情况下,文档都说明:

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

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

正如javadoc建议的那样,解决方案是使用OutputStreamReader/OutputStreamWriter将OutputStream/InputStream与charset规范一起包装起来.

关于一些xml库如何读取XML内容的最后一些注释:

  1. 如果您传递一个Reader,那么库依赖于读取器进行编码(即它不会检查xml标题所说的内容)并且没有关于编码的任何内容,因为它正在读取字符而不是字节.

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

数据库

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

网络 是一个非常好的起点.

如果您觉得缺少某些东西,请随意在评论中提出更多要求.