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

标签 java xml file encoding

我搜索了有关Java和编码的信息,但没有找到说明如何处理编码和解码字符串时Java出现的公共问题的资源。
关于单个错误,有很多特定的问题,但是我没有找到有关该问题的广泛答复/参考指南。
主要问题是:

什么是字符串编码?

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

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

最佳答案

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

编码是将数据从一种格式转换为另一种格式的过程,此响应详细说明了String编码在Java中的工作方式
(您可能需要阅读这篇文章,以获取关于文本结尾编码的更一般性介绍)。

简介

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

乍一看,您可能会认为没有问题,
但是如果我们对流程进行更深入的了解,可能会出现一些问题。
在最低级别上,信息以字节为单位存储/传输:文件是字节序列,网络通信通过发送和接收字节来完成。
因此,每次您想要读取或写入具有简单可读内容的文件时,或者每次您提交Web表单/阅读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(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 ¤

因此,这解释了为什么您读取某些字符并读取不同的字符的原因,用于写入的编码(从String到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

如您所见,尝试打印代码会打个招呼,即UTF-8和ISO8859-1中表示打招呼的字节是相同的。

但是,如果您尝试使用带有€符号的样本,则会得到一个?
    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的字节序列,即读取文件以获取String与读取文件以获取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序列的三个字节的char,而第二个字节的值无效(因为采用了编码字符的UTF-8方式)。

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


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

为什么在读取/处理xml文件时出现y字节UTF-8序列无效字节x异常?

因为用于写入的编码(上述测试案例中的ISO8859-15)与用于读取的编码(上述测试案例中的UTF-8)不匹配;不匹配可能有一些不同的原因:
  • 您在字节和char之间进行了一些错误的转换:例如,如果您正在使用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"); )
  • 您将InputStream直接传递到xml库,但是该文件文件不正确(如第一个管理xml的示例,其中标头声明UTF-8,但实际编码为ISO8859-15。
    仅仅放在文件的第一行是不够的。必须使用标头中使用的编码保存文件。
  • 您正在使用未指定编码而创建的读取器读取文件,并且平台编码与文件编码不同:
    FileReader fileReader=new FileReader("text.xml");
    

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

    您打电话的时候
    "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()或新的String(byte [] ...)而不指定编码,这是第一个检查

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

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

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

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

    关于某些xml库如何读取XML内容的最后说明:
  • (如果您通过读取器,则该库将依靠读取器进行编码(即,它不会检查xml标头中的内容),并且与编码无关,因为它读取的是字符而不是字节。
  • 如果您传递InputStream或File库,则依赖xml标头进行编码,并且它可能会抛出一些编码异常

  • 数据库

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

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

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

    关于java - 如何处理Java编码问题(尤其是xml)?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/29434896/

    相关文章:

    javascript - 优化 Javascript 中的函数

    c# - 使用 Asp-C 写入 Xml 文件#

    javascript - onChange 输入与 Javascript

    java - 一个 java 包中的几个 main() 方法

    java - 消息卡在 activemq 队列中

    java - 设置可输入 JFormattedTextField 的最大值

    python - SSL 错误 : routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed

    java - Kotlin Koans 与 EduTools 插件 : "Failed to launch checking"

    android - 将一个文本放在另一个文本旁边

    python - "OSError: telling position disabled by next() call"错误的含义?