【问题标题】:Java DOM transforming and parsing arbitrary strings with invalid XML characters?Java DOM转换和解析带有无效XML字符的任意字符串?
【发布时间】:2019-12-22 19:53:35
【问题描述】:

首先我想提一下,这不是How to parse invalid (bad / not well-formed) XML? 的副本,因为我没有给定的无效(或格式不正确)XML 文件,而是给定的任意 Java String,它可能或不能包含无效的 XML 字符。我想使用给定的String 创建一个包含Text 节点的DOM Document,然后将其转换为文件。当文件被解析为 DOM Document 时,我想得到一个 String,它等于初始给定的 String。我使用org.w3c.dom.Document#createTextNode(String data) 创建Text 节点,并使用org.w3c.dom.Node#getTextContent() 获得字符串。

正如您在https://stackoverflow.com/a/28152666/3882565 中看到的,XML 文件中的Text 节点存在一些无效字符。实际上,Text 节点有两种不同类型的“无效”字符。有预定义的实体,例如 "&'<>,它们会被 DOM API 自动转义为 "&'<> 在结果文件中,在解析文件时由 DOM API 撤消。现在的问题是,对于其他无效字符,例如'\u0000''\uffff',情况并非如此。解析文件时出现异常,因为'\u0000''\uffff'是无效字符。

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

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

  • 使用 XML 库,例如我已经使用的 DOM API,除了 "&'<> 等之外,这些库实际上都没有替换无效字符。
  • 将所有无效字符替换为"&#number;",这会导致解析文件时出现"�" 等无效字符异常。
  • 使用具有 XML 编码方法的第三方库,该方法不支持非法字符,例如 "�"(在某些库中被跳过)。
  • 使用不支持无效字符的 CDATA 部分。

【问题讨论】:

  • 为什么需要转义其他字符?您能否证明引号、与号、小于和大于以外的字符不会原样返回?
  • 啊,你做到了。我知道这不是一个很好的答案,但我会假设数据根本不是真正的文本数据,所以我会将其存储为二进制格式,如base64Binary。如果您打算转换内容本身,那么您确实需要想出一些类似转义的机制来表示那些无效字符。
  • 此材料已被彻底覆盖多次。要么剥离控制等。人。非法字符,或以某种方式对它们进行编码,例如 Base64。有一些图书馆可以帮助您。抱歉,您的问题没有什么独特之处。添加了其他重复项。还有更多。
  • 如果它必须是 XML,我会放弃使用 CDATA 部分的想法,也许使用编码的 Base64。似乎与 XML 规范最接近。
  • @markspace CDATA 部分根本无法解决问题。无效的 XML 字符(不在 #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] 范围内的 Unicode 代码点)在CDATA 部分。结合 Base64 编码,它会起作用,真的。感谢您推荐 CDATA 部分。

标签: java xml dom escaping invalid-characters


【解决方案1】:

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

但如果“特殊”字符很少见,则会在可读性和文件大小方面做出重大牺牲。

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

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

【讨论】:

  • 感谢您的回答。我并不是说我不能使用其他库,但我不能使用其他库来代替 DOM API。你回答的最后一部分基本上就是我在回答中所做的,只是我使用#而不是\。你能告诉我一个如何使用这样一个库的例子,或者你能推荐一个特定的库吗?
  • 通常推荐使用 Apache Commons 中的 StringEscapeUtils。我自己不使用它,因为我们在撒克逊有自己的惯例。
  • StringEscapeUtils 对我来说看起来并不乐观。它具有某些特定语言的转义和取消转义方法,但我无法以我想要的方式转义特定字符。
  • 你是如何使用 StringEscapeUtils 的? StringEscapeUtils.escapeXml11(...) 和 StringEscapeUtils.unescapeXml(...) 取回原始值?
  • @mahieus 这些不是我正在寻找的功能。 DOM API 已经这样做了,这个函数将简单地删除不在范围内的无效字符 #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]。
【解决方案2】:

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

&lt;?xml <b>version=1.1</b> encoding=UTF-8 standalone=yes?&gt;

根据Wikipedia,XML 1.1 中唯一的无效字符是 U+0000、surrogatesU+FFFE and U+FFFF

此代码 sn-p 确保您始终获得正确的 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();
}

【讨论】:

  • 诚然,XML 1.1 已经支持比 XML 1.0 更多的字符,如本答案 stackoverflow.com/a/28152666/3882565 中所述,但是,它仍然不支持任意的 String。另请注意,链接答案中列出了更多无效字符。
  • 控制字符在XML中存储任意字符串时是一个真正的实际问题,这在1.1中是合法的;这 3 个非法字符太少见了。
  • 抱歉,我遗漏了代理项范围。它实际上超过 3 个字符,但仍然非常罕见。
  • 0xFFFE 和 0xFFFF 点是guaranteed to never be a Unicode character
  • 添加的功能可能工作正常,但它并不是我真正想要的。字符 "&amp;'&lt;&gt; 以及更多字符已经被 DOM API 转义和取消转义。在我的回答中,我对无效字符使用了另一种转义和反转义系统。您的代码与StringEscapeUtils.escapeXml11(String input) 基本相同
【解决方案3】:

正如@VGR 和@kjhughes 在问题下方的cmets 中指出的那样,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();
}

请注意,这些函数可能效率很低,可以用更好的方式编写。随时发布建议以改进 cmets 中的代码。

【讨论】:

    猜你喜欢
    • 2014-02-18
    • 2011-01-04
    • 1970-01-01
    • 2013-02-21
    • 2021-06-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多