Java DOM 转换和解析具有无效 XML 字符的任意字符串?
Java DOM transforming and parsing arbitrary strings with invalid XML characters?
首先我想说的是,这不是 的副本,因为我没有给定的无效(或格式不正确的)XML 文件,而是给定的任意 Java String
可能包含也可能不包含无效的 XML 字符。我想创建一个 DOM Document
,其中包含一个具有给定 String
的 Text
节点,然后将其转换为一个文件。当文件被解析为 DOM Document
我想得到一个 String
等于初始给定的 String
。我使用 org.w3c.dom.Document#createTextNode(String data)
创建 Text
节点,并使用 org.w3c.dom.Node#getTextContent()
.
获取字符串
如您在 中所见,XML 文件中的 Text
个节点存在一些无效字符。实际上 Text
节点有两种不同类型的 "invalid" 字符。有一些预定义实体,例如 "
、&
、'
、<
和 >
,它们会被 DOM [=74= 自动转义] "
、&
、'
、<
和 >
在结果文件中被 DOM API 撤销解析文件时。现在的问题是,对于 '\u0000'
或 '\uffff'
等其他无效字符,情况并非如此。解析文件时出现异常,因为'\u0000'
和'\uffff'
是无效字符。
可能我必须实现一种方法,在将其提交给 DOM API 之前以独特的方式转义给定 String
中的那些字符,并在我得到时撤消它String
回来了,对吧?有一个更好的方法吗?过去是否有人实施过这些或类似的方法?
编辑: 此问题被标记为与 Best way to encode text data for XML in Java? 重复。我现在已经阅读了所有答案,但其中 none 解决了我的问题。所有答案都表明:
- 使用 XML 库,例如我已经在做的 DOM API 和这些库中的 none 实际上替换了
"
以外的无效字符, &
、'
、<
、>
等等。
- 将所有无效字符替换为
"&#number;"
,这会导致解析文件时出现无效字符异常,例如"�"
。
- 使用不支持
"�"
等非法字符的 XML 编码方法的第三方库(它们在某些库中被跳过)。
- 使用也不支持无效字符的 CDATA 部分。
正如@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<text&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 "�"}.
* <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("'");
} else if (c == '"') {
builder.append(""");
} else if (c == '&') {
builder.append("&");
} else if (c == '<') {
builder.append("<");
} else if (c == '>') {
builder.append(">");
} else if (c <= 0x1f) {
builder.append("&#" + ((int) c) + ";");
} else {
builder.append(c);
}
}
return builder.toString();
}
首先我想说的是,这不是 String
可能包含也可能不包含无效的 XML 字符。我想创建一个 DOM Document
,其中包含一个具有给定 String
的 Text
节点,然后将其转换为一个文件。当文件被解析为 DOM Document
我想得到一个 String
等于初始给定的 String
。我使用 org.w3c.dom.Document#createTextNode(String data)
创建 Text
节点,并使用 org.w3c.dom.Node#getTextContent()
.
如您在 中所见,XML 文件中的 Text
个节点存在一些无效字符。实际上 Text
节点有两种不同类型的 "invalid" 字符。有一些预定义实体,例如 "
、&
、'
、<
和 >
,它们会被 DOM [=74= 自动转义] "
、&
、'
、<
和 >
在结果文件中被 DOM API 撤销解析文件时。现在的问题是,对于 '\u0000'
或 '\uffff'
等其他无效字符,情况并非如此。解析文件时出现异常,因为'\u0000'
和'\uffff'
是无效字符。
可能我必须实现一种方法,在将其提交给 DOM API 之前以独特的方式转义给定 String
中的那些字符,并在我得到时撤消它String
回来了,对吧?有一个更好的方法吗?过去是否有人实施过这些或类似的方法?
编辑: 此问题被标记为与 Best way to encode text data for XML in Java? 重复。我现在已经阅读了所有答案,但其中 none 解决了我的问题。所有答案都表明:
- 使用 XML 库,例如我已经在做的 DOM API 和这些库中的 none 实际上替换了
"
以外的无效字符,&
、'
、<
、>
等等。 - 将所有无效字符替换为
"&#number;"
,这会导致解析文件时出现无效字符异常,例如"�"
。 - 使用不支持
"�"
等非法字符的 XML 编码方法的第三方库(它们在某些库中被跳过)。 - 使用也不支持无效字符的 CDATA 部分。
正如@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<text&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 "�"}.
* <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("'");
} else if (c == '"') {
builder.append(""");
} else if (c == '&') {
builder.append("&");
} else if (c == '<') {
builder.append("<");
} else if (c == '>') {
builder.append(">");
} else if (c <= 0x1f) {
builder.append("&#" + ((int) c) + ";");
} else {
builder.append(c);
}
}
return builder.toString();
}