如何查找和替换 XML 中的属性值

问题描述 投票:0回答:5

我正在用 Java 构建一个“XML 扫描器”,它可以查找以“!Here:”开头的属性值。该属性值包含稍后替换的指令。 例如,我有这个 xml 文件,里面充满了像

这样的记录
<bean value="!Here:Sring:HashKey"></bean>

如何查找并替换仅以

"!Here:"
开头的属性值?

java xml dom
5个回答
21
投票

为了修改 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
,您需要

  1. 将文件加载到 dom
    Document
    ,
  2. 使用 xpath 选择所需的节点。在这里,我搜索文档中具有包含字符串
    value
    属性
    !Here
    的所有节点。 xpath 表达式为
    //*[contains(@value, '!Here')]
    .
  3. 在每个选定的节点上进行所需的转换。这里我只是将

    !Here
    改为
    What?

  4. 将修改后的 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>

3
投票

我们在 Java 中有一些替代方案。

  • 首先,JAXP(从 1.4 版开始它就与 Java 捆绑在一起)。

假设我们需要在此 XML 中将属性

customer
更改为
false

<?xml version="1.0" encoding="UTF-8"?>
<notification id="5">
   <to customer="true">[email protected]</to>
   <from>[email protected]</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();

请注意,为了禁用 DocumentBuilderFactory 类的外部实体处理 (

XXE
),我们配置
XMLConstants.FEATURE_SECURE_PROCESSING
功能
。当我们解析不受信任的 XML 文件时配置它是一个很好的做法。查看此 OWASP 指南 了解更多信息。

  • 另一种选择是dom4j。它是一个处理 XML 的开源框架,与 XPath 集成,完全支持 DOM、SAX、JAXP 和 Java 平台(例如 Java Collections)。

我们需要将以下依赖项添加到 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.

请注意,使用此方法时,无论名称如何,如果给定名称的属性已存在,则该属性将被替换,否则将添加该属性。我们可以在here找到javadoc。

  • 另一个不错的选择是jOOX,这个库在jQuery中激发了它的API。

我们需要将以下依赖项添加到 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 |

如果您需要查看,我将示例放在此处


0
投票

Gounelle 的答案是正确的,但是,它是基于你事先知道属性名称的事实。

如果您想仅根据属性值查找所有属性,请使用以下 xpath 表达式:

NodeList attributes = (NodeList) xpath.evaluate(
    "//*/@*[contains(. , '!Here')]",
     doc, 
    XPathConstants.NODESET
)

在这里,您可以通过设置

//*/@*
选择所有属性。然后你可以像我上面提到的那样设置一个条件。

顺便说一下,如果搜索单个属性,可以使用

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>

然后你可以通过将表达式设置为

来选择第二个bean
"//*/@*[ . > 1000]"

0
投票

我最近在当前的项目中也遇到了类似的问题。 我意识到这个解决方案可能无法解决原来的问题,因为它没有考虑到关于

的部分

属性值包含稍后替换的指令

不过,有人可能会发现它很有用。 我们已经使用 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>

0
投票

使用(Element)nodes.item(i)时,我收到如下错误。 错误:无法从节点转换为元素 您能帮忙知道这里是否缺少任何东西吗?我们如何将节点类型转换为 Element。我已经使用了本次讨论中提到的其余代码。

© www.soinside.com 2019 - 2024. All rights reserved.