如何处理 java 编码问题(尤其是 xml)?

How to deal with java encoding problems (especially xml)?

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

什么是字符串编码?

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

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

由于 Whosebug 鼓励自我回答,因此我尝试对自己做出回应。

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

简介

Stringencoding/decoding是一个byte[]转成String的过程,vice-versa。

乍一看你可能认为没有问题, 但如果我们更深入地审视这个过程,可能会出现一些问题。 最底层的信息是stored/transmitted字节:文件是一个字节序列,网络通信是通过发送和接收字节来完成的。 因此,每次您想要读取或写入具有纯可读内容的文件或每次提交 web form/read 网页时,都有一个底层编码操作。 下面从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

到目前为止一切顺利,全部 "simple"。字节的值取自 here,它显示了一种将字母和数字映射到字节的方法 让我们用一个简单的要求使示例复杂化 byte[] 包含 €(欧元)符号;糟糕,ascii table.

中没有欧元符号

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

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

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

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

如何"tell"向java转换使用哪种编码? String 具有构造函数

String(byte[] bytes, String charsetName)

允许指定 "the mapping" 如果您使用不同的字符集,您会得到不同的输出结果,如下所示:

    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 ¤

所以这解释了为什么你读取一些字符并读取不同的字符用于写入的编码(String 到 byte[])与用于读取的编码(byte[] 到 String)不同。 相同的字节可能映射到不同编码的不同字符,因此某些字符可能 "look strange".
这些是理解字符串编码所需的基本概念;让我们把事情复杂化一点。 可能需要在一个文本文档中表示超过 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 classes 在处理无效字符时都表现相同;让我们看看当我们处理 xml.

时会发生什么

管理 XML

在查看示例之前值得记住的是 Java InputStream/OutputStream read/write 字节和 Reader/Writer read/write 字符。

让我们尝试以不同的方式读取 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: Invalid byte 2 of 3-byte UTF-8 sequence

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

棘手的部分是,由于 UTF-8 与某些其他编码重叠,因此出现 Invalid byte 2 of 3-byte UTF-8 sequence 异常 "random" (即仅针对字符由一个字节以上表示的消息),因此在生产环境中,错误可能难以跟踪和重现。

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

Why do I get Invalid byte x of y-byte UTF-8 sequence Exception when reading/dealing with a xml file?

因为用于写入的编码(上面测试用例中的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 的示例,其中 header 声明为 UTF-8,但真正的编码是 ISO8859-15。 仅仅放在文件的第一行是不够的;文件必须使用 header.

  3. 中使用的编码保存
  4. 您正在读取未指定编码而创建的 reader 文件,平台编码与文件编码不同:

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

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

当你打电话时

"Hello €".getBytes();

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

    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 状态:

这个class的构造函数假设默认字符编码和默认byte-buffer大小是acceptable

As with String.getBytes() reading/writing同一个文件在不同平台上用reader/writer并且没有指定字符集可能会导致不同的字节序列由于不同的默认平台编码

java文档建议的解决方案是使用 OutputStreamReader/OutputStreamWriter 将 OutputStream/InputStream 与字符集规范包装在一起。

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

  1. 如果你传递 Reader 库依赖于 reader 进行编码(即它不检查 xml header说)并且对编码没有任何影响,因为它读取的是字符而不是字节。

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

数据库

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

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

如果您觉得缺少什么,请随时在评论中提出更多要求。