Java DOM 转换和解析具有无效 XML 字符的任意字符串?

Java DOM transforming and parsing arbitrary strings with invalid XML characters?

首先我想说的是,这不是 的副本,因为我没有给定的无效(或格式不正确的)XML 文件,而是给定的任意 Java String 可能包含也可能不包含无效的 XML 字符。我想创建一个 DOM Document,其中包含一个具有给定 StringText 节点,然后将其转换为一个文件。当文件被解析为 DOM Document 我想得到一个 String 等于初始给定的 String。我使用 org.w3c.dom.Document#createTextNode(String data) 创建 Text 节点,并使用 org.w3c.dom.Node#getTextContent().

获取字符串

如您在 中所见,XML 文件中的 Text 个节点存在一些无效字符。实际上 Text 节点有两种不同类型的 "invalid" 字符。有一些预定义实体,例如 "&'<>,它们会被 DOM [=74= 自动转义] &quot;&amp;&apos;&lt;&gt; 在结果文件中被 DOM API 撤销解析文件时。现在的问题是,对于 '\u0000''\uffff' 等其他无效字符,情况并非如此。解析文件时出现异常,因为'\u0000''\uffff'是无效字符。

可能我必须实现一种方法,在将其提交给 DOM API 之前以独特的方式转义给定 String 中的那些字符,并在我得到时撤消它String 回来了,对吧?有一个更好的方法吗?过去是否有人实施过这些或类似的方法?

编辑: 此问题被标记为与 Best way to encode text data for XML in Java? 重复。我现在已经阅读了所有答案,但其中 none 解决了我的问题。所有答案都表明:

正如@VGR 和@kjhughes 在问题下方的评论中指出的那样,Base64 确实是我的问题的可能答案。我现在有一个基于转义的问题的进一步解决方案。我写了两个函数 escapeInvalidXmlCharacters(String string)unescapeInvalidXmlCharacters(String string) 可以按以下方式使用。

    String string = "text#text##text#0;text" + '\u0000' + "text<text&text#";
    Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
    Element element = document.createElement("element");
    element.appendChild(document.createTextNode(escapeInvalidXmlCharacters(string)));
    document.appendChild(element);
    TransformerFactory.newInstance().newTransformer().transform(new DOMSource(document), new StreamResult(new File("test.xml")));
    // creates <?xml version="1.0" encoding="UTF-8" standalone="no"?><element>text##text####text##0;text#0;text&lt;text&amp;text##</element>
    document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new File("test.xml"));
    System.out.println(unescapeInvalidXmlCharacters(document.getDocumentElement().getTextContent()).equals(string));
    // prints true

escapeInvalidXmlCharacters(String string)unescapeInvalidXmlCharacters(String string):

/**
 * Escapes invalid XML Unicode code points in a <code>{@link String}</code>. The
 * DOM API already escapes predefined entities, such as {@code "}, {@code &},
 * {@code '}, {@code <} and {@code >} for
 * <code>{@link org.w3c.dom.Text Text}</code> nodes. Therefore, these Unicode
 * code points are ignored by this function. However, there are some other
 * invalid XML Unicode code points, such as {@code '\u0000'}, which are even
 * invalid in their escaped form, such as {@code "&#0;"}.
 * <p>
 * This function replaces all {@code '#'} by {@code "##"} and all Unicode code
 * points which are not in the ranges #x9 | #xA | #xD | [#x20-#xD7FF] |
 * [#xE000-#xFFFD] | [#x10000-#x10FFFF] by the <code>{@link String}</code>
 * {@code "#c;"}, where <code>c</code> is the Unicode code point.
 * 
 * @param string the <code>{@link String}</code> to be escaped
 * @return the escaped <code>{@link String}</code>
 * @see <code>{@link #unescapeInvalidXmlCharacters(String)}</code>
 */
public static String escapeInvalidXmlCharacters(String string) {
    StringBuilder stringBuilder = new StringBuilder();

    for (int i = 0, codePoint = 0; i < string.length(); i += Character.charCount(codePoint)) {
        codePoint = string.codePointAt(i);

        if (codePoint == '#') {
            stringBuilder.append("##");
        } else if (codePoint == 0x9 || codePoint == 0xA || codePoint == 0xD || codePoint >= 0x20 && codePoint <= 0xD7FF || codePoint >= 0xE000 && codePoint <= 0xFFFD || codePoint >= 0x10000 && codePoint <= 0x10FFFF) {
            stringBuilder.appendCodePoint(codePoint);
        } else {
            stringBuilder.append("#" + codePoint + ";");
        }
    }

    return stringBuilder.toString();
}

/**
 * Unescapes invalid XML Unicode code points in a <code>{@link String}</code>.
 * Makes <code>{@link #escapeInvalidXmlCharacters(String)}</code> undone.
 * 
 * @param string the <code>{@link String}</code> to be unescaped
 * @return the unescaped <code>{@link String}</code>
 * @see <code>{@link #escapeInvalidXmlCharacters(String)}</code>
 */
public static String unescapeInvalidXmlCharacters(String string) {
    StringBuilder stringBuilder = new StringBuilder();
    boolean escaped = false;

    for (int i = 0, codePoint = 0; i < string.length(); i += Character.charCount(codePoint)) {
        codePoint = string.codePointAt(i);

        if (escaped) {
            stringBuilder.appendCodePoint(codePoint);
            escaped = false;
        } else if (codePoint == '#') {
            StringBuilder intBuilder = new StringBuilder();
            int j;

            for (j = i + 1; j < string.length(); j += Character.charCount(codePoint)) {
                codePoint = string.codePointAt(j);

                if (codePoint == ';') {
                    escaped = true;
                    break;
                }

                if (codePoint >= 48 && codePoint <= 57) {
                    intBuilder.appendCodePoint(codePoint);
                } else {
                    break;
                }
            }

            if (escaped) {
                try {
                    codePoint = Integer.parseInt(intBuilder.toString());
                    stringBuilder.appendCodePoint(codePoint);
                    escaped = false;
                    i = j;
                } catch (IllegalArgumentException e) {
                    codePoint = '#';
                    escaped = true;
                }
            } else {
                codePoint = '#';
                escaped = true;
            }
        } else {
            stringBuilder.appendCodePoint(codePoint);
        }
    }

    return stringBuilder.toString();
}

请注意,这些函数可能效率很低,可以用更好的方式编写。欢迎在评论中提出 post 改进代码的建议。

一种技术是将整个字符串编码为 Base64 编码的 UTF8。

但是,如果 "special" 字符很少见,那么可读性和文件大小都会有很大的牺牲。

另一种技术是将特殊字符表示为处理指令,例如 <?U 0000?> 表示代码点 0。

另一种方法是使用反斜杠转义,例如 \u0000 用于代码点 0,当然还有 \ 用于反斜杠本身。这样做的好处是您可能可以找到为您执行此操作的现有库例程(例如 JSON 转换库)。我无法想象为什么您的要求说您不能使用此类库;但如果实在不行,自己写代码也不难。

我认为最简单的解决方案是使用 XML 1.1(org.w3c.dom 支持)通过使用此预处理器:

<?xml <b>version=1.1</b> encoding=UTF-8 standalone=yes?>

根据Wikipedia the only invalid characters in XML 1.1 are U+0000, surrogates, U+FFFE and U+FFFF

此代码片段可确保您始终获得正确的 XML 1.1 字符串,省略非法字符(如果您需要返回完全相同的字符串,则可能不是您要查找的内容):

public static String escape(String orig) {
    StringBuilder builder = new StringBuilder();

    for (char c : orig.toCharArray()) {
        if (c == 0x0 || c == 0xfffe || c == 0xffff || (c >= 0xd800 && c <= 0xdfff)) {
            continue;
        } else if (c == '\'') {
            builder.append("&apos;");
        } else if (c == '"') {
            builder.append("&quot;");
        } else if (c == '&') {
            builder.append("&amp;");
        } else if (c == '<') {
            builder.append("&lt;");
        } else if (c == '>') {
            builder.append("&gt;");
        } else if (c <= 0x1f) {
            builder.append("&#" + ((int) c) + ";");
        } else {
            builder.append(c);
        }
    }

    return builder.toString();
}