在Java中对文本数据进行XML编码的最佳方法是什么?

106

这个问题非常相似,只是针对Java。

在Java中,推荐使用什么方法来对字符串进行XML输出的编码?这些字符串可能包含像"&"、"<"等字符。

22个回答

130

3
дҪҝз”Ёcommons-langеә“дёӯзҡ„StringEscapeUtils.escapeXml(str)еҮҪж•°иҝӣиЎҢXMLеӯ—з¬ҰдёІиҪ¬д№үгҖӮжҲ‘еңЁApp Engineеә”з”ЁзЁӢеәҸдёӯдҪҝз”Ёе®ғпјҢж•ҲжһңйқһеёёеҘҪгҖӮиҝҷжҳҜиҜҘеҮҪж•°зҡ„Java DocгҖӮ - Oleg K
1
这个方法对XML内容和属性都适用吗?在我看来,它似乎不适用于属性。它似乎不能转义\t\n\r - Lii
@Lii,\t\n 或者 \r 需要进行转义吗? - Betlista
请注意,StringEscapeUtils.escapeXml() 不会转义控制字符,在许多情况下这些字符在 XML 中是无效的。 - Chin
4
请注意,它已经从 commons-lang 迁移到了 commons-text - Gregor
显示剩余2条评论

37

非常简单:使用一个XML库。这样它就会是正确的,而不需要详细了解XML规范的每个细节。


28
你能推荐这样的库吗?(我发现这不是Java第5版的标准部分,这很令人惊讶,因为这是一个很常见的任务。) - Tim Cooper
4
XML 标准Java框架的一部分 - 可以在org.w3c.sax和org.w3c.dom中查找。然而,还有一些更易于使用的框架,比如JDom。请注意可能没有“用于XML输出的编码字符串”方法 - 我更建议整个XML任务应该使用库来完成,而不仅仅是通过字符串操作逐个处理。 - Jon Skeet
5
这个问题标记为Java,而且Java有很多XML库。实际上,Java内置了XML API,所以不需要添加任何其他东西...但即使你这样做了,在移动设备之外,几百KB很少是一个问题。即使不是Java,我也会对没有任何XML API的平台进行非常谨慎的开发。 - Jon Skeet
2
@mice:DOM API 完全可以生成 XML。或者有一些相对较小的第三方库。(例如,JDom 的 jar 文件只有 114K。)使用 XML API 仍然是创建 XML 的推荐方式。 - Jon Skeet
1
@stonar96 是的,我一直假定一个有效的XML文档是期望的结果。如果你需要表达无法在XML中表达的事情,那就是一个更大的问题。很遗憾XML 1.1从未真正流行起来,因为它可以解决这个问题。 - Jon Skeet
显示剩余17条评论

20

只需使用即可。

<![CDATA[ your text here ]]>

这将允许任何字符,除了结尾的

]]>

因此,您可以包括一些本来是非法的字符,如&和>。例如:

<element><![CDATA[ characters such as & and > are allowed ]]></element>

然而,属性需要进行转义,因为CDATA块不能用于它们。


12
在大多数情况下,这不是你应该做的。太多人滥用了CDATA标签。CDATA的目的是告诉处理器不要将其作为XML处理,而只是直接传递。如果你想创建一个XML文件,那么你应该创建XML,而不仅仅是通过一些包装元素传递字节。 - Mads Hansen
2
@Mads,使用CDATA会生成一个有效的XML文件,因此与按照“正确的方式”操作相同。如果您不喜欢它,那么可以在之后解析它,进行标识转换并打印它。 - Thorbjørn Ravn Andersen
26
如果你将文本放在CDATA元素中,你需要转义CDATA的结束标记:" ]]>",但是你不能转义它。因此,你需要把代码分成两部分,在第一个CDATA元素中放一半的数据,在第二个CDATA元素中放另一半的数据: <![CDATA[这段数据包含CDATA结束标记:"]]><![CDATA[>",所以必须拆分。]]> 最终,直接转义字符'<', '>'和 '&'可能更简单。当然,许多应用程序忽略了数据中CDATA结束标记的潜在问题。我猜无知就是福吧。 :) - Stijn de Witt
3
@StijndeWitt 绝对正确。 CDATA 并不是转义特殊字符的万应钥匙。 - dnault
这是一个不好的想法。CDATA 不允许 XML 编码之外的任何字符。 - Florian F
在XML文件(Java和DOM解析器)中,“<”作为节点文本值存在,但是当使用node.getContentType获取此节点时,它会被转换为“<”。有没有办法检索“<”本身,而不是“<”? - Rohit Kumar

18
这个问题已经有八年了,但还是没有一个完全正确的答案!不,你不应该导入整个第三方API来完成这个简单的任务。这是错误的建议。
以下方法将会:
- 正确处理基本多语言平面之外的字符 - 转义XML中需要的字符 - 转义任何非ASCII字符,这是可选的但常见的 - 用Unicode替换XML 1.0中的非法字符。在这里没有最佳选择 - 删除它们同样有效。
我尽量优化了最常见的情况,同时确保你可以通过这个方法传输/dev/random并得到一个在XML中有效的字符串。
public static String encodeXML(String s) {
    StringBuilder sb = new StringBuilder();
    int len = s.length();
    for (int i=0;i<len;) {
        int c = s.codePointAt(i);
        if (c < 0x80) {      // ASCII range: test most common case first
            if (c < 0x20 && (c != '\t' && c != '\r' && c != '\n')) {
                // Illegal XML character, even encoded. Skip or substitute
                sb.append("&#xfffd;");   // Unicode replacement character
            } else {
                switch(c) {
                  case '&':  sb.append("&amp;"); break;
                  case '>':  sb.append("&gt;"); break;
                  case '<':  sb.append("&lt;"); break;
                  // Uncomment next two if encoding for an XML attribute
//                  case '\''  sb.append("&apos;"); break;
//                  case '\"'  sb.append("&quot;"); break;
                  // Uncomment next three if you prefer, but not required
//                  case '\n'  sb.append("&#10;"); break;
//                  case '\r'  sb.append("&#13;"); break;
//                  case '\t'  sb.append("&#9;"); break;

                  default:   sb.append((char)c);
                }
            }
        } else if ((c >= 0xd800 && c <= 0xdfff) || c == 0xfffe || c == 0xffff) {
            // Illegal XML character, even encoded. Skip or substitute
            sb.append("&#xfffd;");   // Unicode replacement character
        } else {
            sb.append("&#x");
            sb.append(Integer.toHexString(c));
            sb.append(';');
        }
        i += c <= 0xffff ? 1 : 2;
    }
    return sb.toString();
}

编辑:对于那些坚持认为在处理XML时自己编写代码是愚蠢的人,你可能会想知道,Oracle Java 8(我没有测试其他版本)附带的StAX API无法正确编码CDATA内容:它不会转义内容中的]]>序列。第三方库,即使是Java核心的一部分,也并不总是最佳选择。


仅限于独立代码的情况下加1。与guava实现进行比较,我想知道'\t','\n','\r'怎么办?请参阅guava文档中的注释。 - jschnasse
2
不需要转义 \n、\r 和 \t,它们是有效的,尽管它们会使格式有点丑陋。我已经修改了代码,以展示如何转义它们,如果这是你想要的话。 - Mike B
2
在CDATA中,没有任何方法可以“转义]]>”。 - kmkaplan
1
然后它应该通过抛出IllegalArgumentException来拒绝内容。在任何情况下,它都不应声称成功,但仍输出无效的XML。 - Mike B
你可以使用我在这里提供的方法 https://dev59.com/_bjna4cB1Zd3GeqP6z2D#59475093,而不是将XML 1.0中的非法字符替换为Unicode替换字符。 - stonar96
有用且受欢迎!但是代码很难读。 - Max M

13

试试这个:

String xmlEscapeText(String t) {
   StringBuilder sb = new StringBuilder();
   for(int i = 0; i < t.length(); i++){
      char c = t.charAt(i);
      switch(c){
      case '<': sb.append("&lt;"); break;
      case '>': sb.append("&gt;"); break;
      case '\"': sb.append("&quot;"); break;
      case '&': sb.append("&amp;"); break;
      case '\'': sb.append("&apos;"); break;
      default:
         if(c>0x7e) {
            sb.append("&#"+((int)c)+";");
         }else
            sb.append(c);
      }
   }
   return sb.toString();
}

9
我看到你至少有两个bug。其中一个比较微妙,另一个则不是。我不会有这样的bug - 因为我不会一开始就重新发明轮子。 - Jon Skeet
1
迭代Unicode字符串有些复杂。请参见此处:https://dev59.com/7nI_5IYBdhLWcg3wF_B3 - ceving
1
不确定这是否“微妙”,但最好考虑 t==null 的情况。 - Myobis
3
我对最终版本感到满意。Java SE紧凑、快速、高效。只做必要的事情,而不是下载另外100兆字节的臃肿软件,这在我看来总是更好的选择。 - Roger F. Gay
2
所有小于0x20的字符,除了0x09、0x0A和0x0D之外,在XML中都是无效的。这适用于它们是否被转义。处理它们的唯一正确方式是跳过它们或抛出异常。除此之外,这是一个很好的解决方案,类似于我们通常使用的解决方案。 - Mike B
显示剩余11条评论

13

以下方法可以给出一个已转义的文本字符串:

public class XMLHelper {

/**
 * Returns the string where all non-ascii and <, &, > are encoded as numeric entities. I.e. "&lt;A &amp; B &gt;"
 * .... (insert result here). The result is safe to include anywhere in a text field in an XML-string. If there was
 * no characters to protect, the original string is returned.
 * 
 * @param originalUnprotectedString
 *            original string which may contain characters either reserved in XML or with different representation
 *            in different encodings (like 8859-1 and UFT-8)
 * @return
 */
public static String protectSpecialCharacters(String originalUnprotectedString) {
    if (originalUnprotectedString == null) {
        return null;
    }
    boolean anyCharactersProtected = false;

    StringBuffer stringBuffer = new StringBuffer();
    for (int i = 0; i < originalUnprotectedString.length(); i++) {
        char ch = originalUnprotectedString.charAt(i);

        boolean controlCharacter = ch < 32;
        boolean unicodeButNotAscii = ch > 126;
        boolean characterWithSpecialMeaningInXML = ch == '<' || ch == '&' || ch == '>';

        if (characterWithSpecialMeaningInXML || unicodeButNotAscii || controlCharacter) {
            stringBuffer.append("&#" + (int) ch + ";");
            anyCharactersProtected = true;
        } else {
            stringBuffer.append(ch);
        }
    }
    if (anyCharactersProtected == false) {
        return originalUnprotectedString;
    }

    return stringBuffer.toString();
}

}

1
stringBuffer.append("&#" + (int) ch + ";"); 这种写法对于多字节字符是不起作用的。我现在遇到了一个表情符号字符,UTF8序列为F0 9F 98 8D。 - Kylar

9

StringEscapeUtils.escapeXml()方法不会转义控制字符(小于0x20)。XML 1.1允许控制字符,而XML 1.0则不允许。例如,XStream.toXML()会将Java对象的控制字符序列化为XML,但XML 1.0解析器将拒绝该XML。

如果想要使用Apache commons-lang转义控制字符,请使用以下方法:

NumericEntityEscaper.below(0x20).translate(StringEscapeUtils.escapeXml(str))

9
public String escapeXml(String s) {
    return s.replaceAll("&", "&amp;").replaceAll(">", "&gt;").replaceAll("<", "&lt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;");
}

7
链式调用replaceAll非常低效,尤其是针对大字符串。每次调用都会创建一个新的String对象,直到被垃圾回收。而且,每次调用都需要再次循环遍历字符串。这可以合并为一次手动循环,在每次迭代中与每个目标字符进行比较。 - daiscog
2
这应该是被接受的答案,即使它不够高效。它可以用一行代码解决问题。 - Stimpson Cat
它有很多漏洞。请参见上面的评论 - David Balažic
为了修复这些错误,您还可以使用我在此处提供的方法 https://dev59.com/_bjna4cB1Zd3GeqP6z2D#59475093。请注意,这不是替代品,但可以作为补充使用。 - stonar96

8

请注意,它已经从commons-lang移动到commons-text。 - Trisped

6

虽然理想主义者会建议使用XML库,但在我看来,如果您对XML有基本的了解,那么常识和性能都会告诉您应该一直使用模板。这样做可能更易读。不过,使用库的转义例程可能是一个好主意。

请考虑这一点:XML 原本就是为人类编写而设计的。

当您的XML作为“对象”更好地模拟您的问题时,请使用生成XML的库。例如,如果可插入模块参与构建此XML的过程。

编辑:至于如何在模板中实际转义XML,使用CDATA或JSTL中的escapeXml(string)是两个好方法,可以像这样使用escapeXml(string)

<%@taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>

<item>${fn:escapeXml(value)}</item>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接