如何在 XML 中查找和替换属性值
How to find and replace an attribute value in a XML
我正在 Java 中构建一个 "XML scanner",用于查找以“!Here:”开头的属性值。属性值包含稍后替换的说明。
例如,我有这个 xml 文件,其中包含像
这样的记录
<bean value="!Here:Sring:HashKey"></bean>
如何查找和替换只知道以 "!Here:"
开头的属性值?
为了修改 XML 文件中的某些元素或属性值,同时仍然尊重 XML 结构,您将需要使用 XML 解析器。它比 String$replace()
...
涉及更多
举个例子 XML 比如:
<?xml version="1.0" encoding="UTF-8"?>
<beans>
<bean id="exampleBean" class="examples.ExampleBean">
<!-- setter injection using -->
<property name="beanTwo" ref="anotherBean"/>
<property name="integerProperty" value="!Here:Integer:Foo"/>
</bean>
<bean id="anotherBean" class="examples.AnotherBean">
<property name="stringProperty" value="!Here:String:Bar"/>
</bean>
</beans>
要更改 2 个标记 !Here
,您需要
- 将文件加载到 dom
Document
,
- select 使用 xpath 获取想要的节点。在这里,我搜索文档中具有包含字符串
!Here
的属性 value
的所有节点。 xpath 表达式为 //*[contains(@value, '!Here')]
.
在每个 selected 节点上执行您想要的转换。这里我只是把!Here
改成What?
.
将修改后的domDocument
保存到新文件中
static String inputFile = "./beans.xml";
static String outputFile = "./beans_new.xml";
// 1- Build the doc from the XML file
Document doc = DocumentBuilderFactory.newInstance()
.newDocumentBuilder().parse(new InputSource(inputFile));
// 2- Locate the node(s) with xpath
XPath xpath = XPathFactory.newInstance().newXPath();
NodeList nodes = (NodeList)xpath.evaluate("//*[contains(@value, '!Here')]",
doc, XPathConstants.NODESET);
// 3- Make the change on the selected nodes
for (int idx = 0; idx < nodes.getLength(); idx++) {
Node value = nodes.item(idx).getAttributes().getNamedItem("value");
String val = value.getNodeValue();
value.setNodeValue(val.replaceAll("!Here", "What?"));
}
// 4- Save the result to a new XML doc
Transformer xformer = TransformerFactory.newInstance().newTransformer();
xformer.transform(new DOMSource(doc), new StreamResult(new File(outputFile)));
生成的 XML 文件是:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<beans>
<bean class="examples.ExampleBean" id="exampleBean">
<!-- setter injection using -->
<property name="beanTwo" ref="anotherBean"/>
<property name="integerProperty" value="What?:Integer:Foo"/>
</bean>
<bean class="examples.AnotherBean" id="anotherBean">
<property name="stringProperty" value="What?:String:Bar"/>
</bean>
</beans>
我们在 Java 中有一些替代方案。
- 首先,JAXP(从 1.4 版本开始就与 Java 捆绑在一起)。
假设我们需要在 XML:
中将属性 customer
更改为 false
<?xml version="1.0" encoding="UTF-8"?>
<notification id="5">
<to customer="true">john@email.com</to>
<from>mary@email.com</from>
</notification>
使用 JAXP(此实现基于 @t-gounelle 示例)我们可以这样做:
//Load the document
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
Document input = factory.newDocumentBuilder().parse(resourcePath);
//Select the node(s) with XPath
XPath xpath = XPathFactory.newInstance().newXPath();
NodeList nodes = (NodeList) xpath.evaluate(String.format("//*[contains(@%s, '%s')]", attribute, oldValue), input, XPathConstants.NODESET);
// Updated the selected nodes (here, we use the Stream API, but we can use a for loop too)
IntStream
.range(0, nodes.getLength())
.mapToObj(i -> (Element) nodes.item(i))
.forEach(value -> value.setAttribute(attribute, newValue));
// Get the result as a String
TransformerFactory factory = TransformerFactory.newInstance();
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
Transformer xformer = factory.newTransformer();
xformer.setOutputProperty(OutputKeys.INDENT, "yes");
Writer output = new StringWriter();
xformer.transform(new DOMSource(input), new StreamResult(output));
String result = output.toString();
请注意,为了禁用外部实体处理(XXE) for the DocumentBuilderFactory
class, we configure the XMLConstants.FEATURE_SECURE_PROCESSING
feature. It’s a good practice to configure it when we parse untrusted XML files. Check this OWASP guide 附加信息。
- 另一种选择是dom4j。它是一个用于处理 XML 的 open-source 框架,它与 XPath 集成并完全支持 DOM、SAX、JAXP 和 Java 平台,例如 Java 集合。
我们需要将以下依赖项添加到我们的 pom.xml 中才能使用它:
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
<version>1.2.0</version>
</dependency>
实现与 JAXP 等价物非常相似:
// Load the document
SAXReader xmlReader = new SAXReader();
Document input = xmlReader.read(resourcePath);
// Features to prevent XXE
xmlReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
xmlReader.setFeature("http://xml.org/sax/features/external-general-entities", false);
xmlReader.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
// Select the nodes
String expr = String.format("//*[contains(@%s, '%s')]", attribute, oldValue);
XPath xpath = DocumentHelper.createXPath(expr);
List<Node> nodes = xpath.selectNodes(input);
// Updated the selected nodes
IntStream
.range(0, nodes.getLength())
.mapToObj(i -> (Element) nodes.get(i);)
.forEach(value -> value.addAttribute(attribute, newValue));
// We can get the representation as String in the same way as the previous JAXP snippet.
请注意,尽管有名称,但使用此方法时,如果给定名称的属性已经存在,它将被替换,否则将添加它。我们可以找到 javadoc here.
我们需要将以下依赖项添加到 pom.xml 才能使用 jOOX。
用于 Java 9+:
<dependency>
<groupId>org.jooq</groupId>
<artifactId>joox</artifactId>
<version>1.6.2</version>
</dependency>
用于 Java 6+:
<dependency>
<groupId>org.jooq</groupId>
<artifactId>joox-java-6</artifactId>
<version>1.6.2</version>
</dependency>
我们可以像这样实现我们的属性转换器:
// Load the document
DocumentBuilder builder = JOOX.builder();
Document input = builder.parse(resourcePath);
Match $ = $(input);
// Select the nodes
$
.find("to") // We can use and XPATH expresion too.
.get()
.stream()
.forEach(e -> e.setAttribute(attribute, newValue));
// Get the String reprentation
$.toString();
正如我们在此示例中所见,语法比 JAXP 和 dom4j 示例更简洁。
我将这 3 个实现与 JMH 进行了比较,得到以下结果:
| Benchmark Mode Cnt Score Error Units |
|--------------------------------------------------------------------|
| AttributeBenchMark.dom4jBenchmark avgt 5 0.167 ± 0.050 ms/op |
| AttributeBenchMark.jaxpBenchmark avgt 5 0.185 ± 0.047 ms/op |
| AttributeBenchMark.jooxBenchmark avgt 5 0.307 ± 0.110 ms/op |
我放了例子here如果你需要看一下
Gounelle 的回答是正确的,但是,这是基于您事先知道属性名称的事实。
如果您只想根据属性值查找所有属性,请对 xpath 使用此表达式:
NodeList attributes = (NodeList) xpath.evaluate(
"//*/@*[contains(. , '!Here')]",
doc,
XPathConstants.NODESET
)
在这里,你select通过设置//*/@*
的所有属性。然后你可以像我上面提到的那样设置一个条件。
顺便说一句,如果搜索单个属性,可以使用Attr
代替Node
Attr attribute = (Attr) xpath.evaluate(
"//*/@*[contains(. , '!Here')]",
doc,
XPathConstants.NODE
)
attribute.setValue("What!");
如果要按特定值查找属性,请使用
"//*/@*[ . = '!Here:String:HashKey' ]"
如果您使用数字比较搜索属性,例如,如果您有
<bean value="999"></bean>
<bean value="1337"></bean>
然后你可以通过将表达式设置为
来select第二个bean
"//*/@*[ . > 1000]"
最近在做项目的时候遇到了类似的问题。
我意识到这个解决方案可能无法解决原始问题,因为它没有考虑到关于
的部分
The attribute value contains instructions to replace later
不过,有人可能会觉得它有用。
我们已经使用来自 apache commons 的 StringSubstitutor.java 来替换 JSON 文件中的值。
事实证明,在我们的案例中,它与 XML 文本一样有效。
它确实对字符串进行操作,这可能并不适用于所有情况。
给定一个简单的 XML 像这样:
<?xml version="1.0" encoding="UTF-8"?>
<Foo>
<Bar>${replaceThis:-defaultValueHere}</Bar>
<bean value="${!Here}:Sring:HashKey"></bean>
</Foo>
StringSubstitutor
让您可以用任何东西替换 ${replaceThis:-defaultValueHere}
。
在 Java 11 中,简单示例可能如下所示:
// Read the file as a string. (Java 11+)
String xml = Files.readString(path, StandardCharsets.US_ASCII);
// Specify what to replace
Map<String, String> replacementMappings = Map.of(
"replaceThis", "Something else",
"!Here","Bean"
);
String xmlWithStringsReplaced = new StringSubstitutor(replacementMappings).replace(testFile);
那么 xmlWithStringsReplaced
应该是这样的:
<?xml version="1.0" encoding="UTF-8"?>
<Foo>
<Bar>Something Else</Bar>
<bean value="Bean:Sring:HashKey"></bean>
</Foo>
我正在 Java 中构建一个 "XML scanner",用于查找以“!Here:”开头的属性值。属性值包含稍后替换的说明。 例如,我有这个 xml 文件,其中包含像
这样的记录<bean value="!Here:Sring:HashKey"></bean>
如何查找和替换只知道以 "!Here:"
开头的属性值?
为了修改 XML 文件中的某些元素或属性值,同时仍然尊重 XML 结构,您将需要使用 XML 解析器。它比 String$replace()
...
举个例子 XML 比如:
<?xml version="1.0" encoding="UTF-8"?>
<beans>
<bean id="exampleBean" class="examples.ExampleBean">
<!-- setter injection using -->
<property name="beanTwo" ref="anotherBean"/>
<property name="integerProperty" value="!Here:Integer:Foo"/>
</bean>
<bean id="anotherBean" class="examples.AnotherBean">
<property name="stringProperty" value="!Here:String:Bar"/>
</bean>
</beans>
要更改 2 个标记 !Here
,您需要
- 将文件加载到 dom
Document
, - select 使用 xpath 获取想要的节点。在这里,我搜索文档中具有包含字符串
!Here
的属性value
的所有节点。 xpath 表达式为//*[contains(@value, '!Here')]
. 在每个 selected 节点上执行您想要的转换。这里我只是把
!Here
改成What?
.将修改后的dom
Document
保存到新文件中
static String inputFile = "./beans.xml";
static String outputFile = "./beans_new.xml";
// 1- Build the doc from the XML file
Document doc = DocumentBuilderFactory.newInstance()
.newDocumentBuilder().parse(new InputSource(inputFile));
// 2- Locate the node(s) with xpath
XPath xpath = XPathFactory.newInstance().newXPath();
NodeList nodes = (NodeList)xpath.evaluate("//*[contains(@value, '!Here')]",
doc, XPathConstants.NODESET);
// 3- Make the change on the selected nodes
for (int idx = 0; idx < nodes.getLength(); idx++) {
Node value = nodes.item(idx).getAttributes().getNamedItem("value");
String val = value.getNodeValue();
value.setNodeValue(val.replaceAll("!Here", "What?"));
}
// 4- Save the result to a new XML doc
Transformer xformer = TransformerFactory.newInstance().newTransformer();
xformer.transform(new DOMSource(doc), new StreamResult(new File(outputFile)));
生成的 XML 文件是:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<beans>
<bean class="examples.ExampleBean" id="exampleBean">
<!-- setter injection using -->
<property name="beanTwo" ref="anotherBean"/>
<property name="integerProperty" value="What?:Integer:Foo"/>
</bean>
<bean class="examples.AnotherBean" id="anotherBean">
<property name="stringProperty" value="What?:String:Bar"/>
</bean>
</beans>
我们在 Java 中有一些替代方案。
- 首先,JAXP(从 1.4 版本开始就与 Java 捆绑在一起)。
假设我们需要在 XML:
中将属性customer
更改为 false
<?xml version="1.0" encoding="UTF-8"?>
<notification id="5">
<to customer="true">john@email.com</to>
<from>mary@email.com</from>
</notification>
使用 JAXP(此实现基于 @t-gounelle 示例)我们可以这样做:
//Load the document
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
Document input = factory.newDocumentBuilder().parse(resourcePath);
//Select the node(s) with XPath
XPath xpath = XPathFactory.newInstance().newXPath();
NodeList nodes = (NodeList) xpath.evaluate(String.format("//*[contains(@%s, '%s')]", attribute, oldValue), input, XPathConstants.NODESET);
// Updated the selected nodes (here, we use the Stream API, but we can use a for loop too)
IntStream
.range(0, nodes.getLength())
.mapToObj(i -> (Element) nodes.item(i))
.forEach(value -> value.setAttribute(attribute, newValue));
// Get the result as a String
TransformerFactory factory = TransformerFactory.newInstance();
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
Transformer xformer = factory.newTransformer();
xformer.setOutputProperty(OutputKeys.INDENT, "yes");
Writer output = new StringWriter();
xformer.transform(new DOMSource(input), new StreamResult(output));
String result = output.toString();
请注意,为了禁用外部实体处理(XXE) for the DocumentBuilderFactory
class, we configure the XMLConstants.FEATURE_SECURE_PROCESSING
feature. It’s a good practice to configure it when we parse untrusted XML files. Check this OWASP guide 附加信息。
- 另一种选择是dom4j。它是一个用于处理 XML 的 open-source 框架,它与 XPath 集成并完全支持 DOM、SAX、JAXP 和 Java 平台,例如 Java 集合。
我们需要将以下依赖项添加到我们的 pom.xml 中才能使用它:
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
<version>1.2.0</version>
</dependency>
实现与 JAXP 等价物非常相似:
// Load the document
SAXReader xmlReader = new SAXReader();
Document input = xmlReader.read(resourcePath);
// Features to prevent XXE
xmlReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
xmlReader.setFeature("http://xml.org/sax/features/external-general-entities", false);
xmlReader.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
// Select the nodes
String expr = String.format("//*[contains(@%s, '%s')]", attribute, oldValue);
XPath xpath = DocumentHelper.createXPath(expr);
List<Node> nodes = xpath.selectNodes(input);
// Updated the selected nodes
IntStream
.range(0, nodes.getLength())
.mapToObj(i -> (Element) nodes.get(i);)
.forEach(value -> value.addAttribute(attribute, newValue));
// We can get the representation as String in the same way as the previous JAXP snippet.
请注意,尽管有名称,但使用此方法时,如果给定名称的属性已经存在,它将被替换,否则将添加它。我们可以找到 javadoc here.
我们需要将以下依赖项添加到 pom.xml 才能使用 jOOX。
用于 Java 9+:
<dependency>
<groupId>org.jooq</groupId>
<artifactId>joox</artifactId>
<version>1.6.2</version>
</dependency>
用于 Java 6+:
<dependency>
<groupId>org.jooq</groupId>
<artifactId>joox-java-6</artifactId>
<version>1.6.2</version>
</dependency>
我们可以像这样实现我们的属性转换器:
// Load the document
DocumentBuilder builder = JOOX.builder();
Document input = builder.parse(resourcePath);
Match $ = $(input);
// Select the nodes
$
.find("to") // We can use and XPATH expresion too.
.get()
.stream()
.forEach(e -> e.setAttribute(attribute, newValue));
// Get the String reprentation
$.toString();
正如我们在此示例中所见,语法比 JAXP 和 dom4j 示例更简洁。
我将这 3 个实现与 JMH 进行了比较,得到以下结果:
| Benchmark Mode Cnt Score Error Units |
|--------------------------------------------------------------------|
| AttributeBenchMark.dom4jBenchmark avgt 5 0.167 ± 0.050 ms/op |
| AttributeBenchMark.jaxpBenchmark avgt 5 0.185 ± 0.047 ms/op |
| AttributeBenchMark.jooxBenchmark avgt 5 0.307 ± 0.110 ms/op |
我放了例子here如果你需要看一下
Gounelle 的回答是正确的,但是,这是基于您事先知道属性名称的事实。
如果您只想根据属性值查找所有属性,请对 xpath 使用此表达式:
NodeList attributes = (NodeList) xpath.evaluate(
"//*/@*[contains(. , '!Here')]",
doc,
XPathConstants.NODESET
)
在这里,你select通过设置//*/@*
的所有属性。然后你可以像我上面提到的那样设置一个条件。
顺便说一句,如果搜索单个属性,可以使用Attr
代替Node
Attr attribute = (Attr) xpath.evaluate(
"//*/@*[contains(. , '!Here')]",
doc,
XPathConstants.NODE
)
attribute.setValue("What!");
如果要按特定值查找属性,请使用
"//*/@*[ . = '!Here:String:HashKey' ]"
如果您使用数字比较搜索属性,例如,如果您有
<bean value="999"></bean>
<bean value="1337"></bean>
然后你可以通过将表达式设置为
来select第二个bean"//*/@*[ . > 1000]"
最近在做项目的时候遇到了类似的问题。 我意识到这个解决方案可能无法解决原始问题,因为它没有考虑到关于
的部分The attribute value contains instructions to replace later
不过,有人可能会觉得它有用。 我们已经使用来自 apache commons 的 StringSubstitutor.java 来替换 JSON 文件中的值。
事实证明,在我们的案例中,它与 XML 文本一样有效。 它确实对字符串进行操作,这可能并不适用于所有情况。
给定一个简单的 XML 像这样:
<?xml version="1.0" encoding="UTF-8"?>
<Foo>
<Bar>${replaceThis:-defaultValueHere}</Bar>
<bean value="${!Here}:Sring:HashKey"></bean>
</Foo>
StringSubstitutor
让您可以用任何东西替换 ${replaceThis:-defaultValueHere}
。
在 Java 11 中,简单示例可能如下所示:
// Read the file as a string. (Java 11+)
String xml = Files.readString(path, StandardCharsets.US_ASCII);
// Specify what to replace
Map<String, String> replacementMappings = Map.of(
"replaceThis", "Something else",
"!Here","Bean"
);
String xmlWithStringsReplaced = new StringSubstitutor(replacementMappings).replace(testFile);
那么 xmlWithStringsReplaced
应该是这样的:
<?xml version="1.0" encoding="UTF-8"?>
<Foo>
<Bar>Something Else</Bar>
<bean value="Bean:Sring:HashKey"></bean>
</Foo>