使用xPath修改XML文件

7

我希望使用xPath修改一个现有的XML文件。如果节点不存在,则应创建它(以及必要的父节点)。以下是示例:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <param0>true</param0>
  <param1>1.0</param1>
</configuration>

以下是我想要插入/修改的两个XPath:

/configuration/param1/text()         -> 4.0
/configuration/param2/text()         -> "asdf"
/configuration/test/param3/text()    -> true

XML文件应该长这个样子:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <param0>true</param0>
  <param1>4.0</param1>
  <param2>asdf</param2>
  <test>
    <param3>true</param3>
  </test>
</configuration>

我尝试了这个:
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

try {
  DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();
  Document doc = domFactory.newDocumentBuilder().parse(file.getAbsolutePath());
  XPath xpath = XPathFactory.newInstance().newXPath();

  String xPathStr = "/configuration/param1/text()";
  Node node = ((NodeList) xpath.compile(xPathStr).evaluate(doc, XPathConstants.NODESET)).item(0);
  System.out.printf("node value: %s\n", node.getNodeValue());
  node.setNodeValue("4.0");

  TransformerFactory transformerFactory = TransformerFactory.newInstance();
  Transformer transformer = transformerFactory.newTransformer();
  transformer.transform(new DOMSource(doc), new StreamResult(file));
} catch (Exception e) {
  e.printStackTrace();
}

运行此代码后,文件中的节点已更改。这正是我想要的。但如果我使用下面其中一条路径之一,node 将为 null(因此会抛出 NullPointerException):

/configuration/param2/text()
/configuration/test/param3/text()

如何修改这段代码,以便创建节点(包括不存在的父节点)?
编辑:好吧,为了澄清:我有一组要保存到XML中的参数。在开发过程中,这个集合可能会改变(添加一些参数,移动一些参数,删除一些参数)。所以我想要一个函数将当前的参数集写入已经存在的文件中。它应该覆盖文件中已经存在的参数,添加新的参数并保留旧的参数。
读取也是同样的道理,我可以使用xPath或其他坐标从XML中获取值。如果它不存在,则返回空字符串。
对于实现方式,我没有任何限制,xPath、DOM、SAX、XSLT...一旦编写了功能,它应该很容易使用(就像BeniBela的解决方案)。
因此,如果我要设置以下参数:
/configuration/param1/text()         -> 4.0
/configuration/param2/text()         -> "asdf"
/configuration/test/param3/text()    -> true

结果应该是起始XML加上这些参数。如果它们已经存在于该xPath,它们将被替换,否则它们将插入到该点。
4个回答

7
如果你想要一个没有依赖的解决方案,你可以只使用DOM而不需要XPath/XSLT。
可以使用Node.getChildNodes|getNodeName / NodeList.*来查找节点,使用Document.createElement|createTextNode,Node.appendChild创建新节点。
接下来,你可以编写自己简单的“XPath”解释器,像这样创建路径中缺失的节点:
public static void update(Document doc, String path, String def){
  String p[] = path.split("/");
  //search nodes or create them if they do not exist
  Node n = doc;
  for (int i=0;i < p.length;i++){
    NodeList kids = n.getChildNodes();
    Node nfound = null;
    for (int j=0;j<kids.getLength();j++) 
      if (kids.item(j).getNodeName().equals(p[i])) {
    nfound = kids.item(j);
    break;
      }
    if (nfound == null) { 
      nfound = doc.createElement(p[i]);
      n.appendChild(nfound);
      n.appendChild(doc.createTextNode("\n")); //add whitespace, so the result looks nicer. Not really needed
    }
    n = nfound;
  }
  NodeList kids = n.getChildNodes();
  for (int i=0;i<kids.getLength();i++)
    if (kids.item(i).getNodeType() == Node.TEXT_NODE) {
      //text node exists
      kids.item(i).setNodeValue(def); //override
      return;
    }

  n.appendChild(doc.createTextNode(def));    
}

然后,如果你只想更新text()节点,可以使用以下方式:

DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();
Document doc = domFactory.newDocumentBuilder().parse(file.getAbsolutePath());

update(doc, "configuration/param1", "4.0");
update(doc, "configuration/param2", "asdf");
update(doc, "configuration/test/param3", "true");

JDK/JRE 包含 XPath (javax.xml.xpath) 和 XSLT (javax.xml.transform) API,因此这些方法不会引入任何依赖关系。 - bdoughan
@BlaiseDoughan:但从他的评论来看,brimborium似乎不太喜欢这些API。 - BeniBela
谢谢BeniBela。对我来说,这将是一个干净的解决方案,它正好做到了我想要的。你是对的,我并不完全相信XSLT,但这也可能是因为我还没有掌握它的概念。我也会研究一下它。 - brimborium
我决定采用你的解决方案,同时提升我的XSLT知识。因为Dimitre的答案也给我留下了深刻印象。;) 感谢你的帮助。 - brimborium

6

这里有一个简单的 XSLT 解决方案:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:strip-space elements="*"/>

 <xsl:template match="node()|@*">
     <xsl:copy>
       <xsl:apply-templates select="node()|@*"/>
     </xsl:copy>
 </xsl:template>

 <xsl:template match="param1/text()">4.0</xsl:template>

 <xsl:template match="/*">
  <xsl:copy>
   <xsl:apply-templates select="@*|node()"/>
     <param2>asdf</param2>
     <test><param3>true</param3></test>
  </xsl:copy>
 </xsl:template>
</xsl:stylesheet>

当对提供的XML文档应用此转换时:
<configuration>
    <param0>true</param0>
    <param1>1.0</param1>
</configuration>
期望得到的正确结果是:
<configuration>
   <param0>true</param0>
   <param1>4.0</param1>
   <param2>asdf</param2>
   <test><param3>true</param3></test>
</configuration>

注意:

XSLT转换永远不会“原地更新”。它总是创建一个新的结果树。因此,如果想要修改同一个文件,通常转换的结果将保存为另一个名称,然后删除原始文件,并将结果重命名为原始名称。


嗯,这只是转移了我的问题。我仍然需要创建一个文件(现在只是一个XSLT文件),并且必须检查每个节点是否已经在给定的XML文件中可用。也许我没有理解这使得它比使用DOM更简单的事情? - brimborium
1
@brimborium,XSLT代码只有几行(可能比您当前的代码短两倍)。对于更复杂的问题,这个比率将是十倍甚至百倍。XSLT代码也更简单(在大多数情况下不需要显式条件指令),可扩展(由于模板和模板匹配)和可维护(由于所有先前的事实)。因此,总结一下,当问题稍微复杂一些时,使用XSLT的优势是显而易见的。看看“xslt”标签中提出的问题,并尝试在没有XSLT的情况下解决它们 :) - Dimitre Novatchev
1
@brimborium,你没有一个明确定义的问题。请编辑问题并给出一个或多个XML文档以及每个文档应该转换成的精确期望结果。如果你没有漏掉任何重要情况,人们会给你提供覆盖所有重要情况的解决方案。如果你未能提供一些重要情况,那么不要抱怨人们无法猜测你当时脑海中的想法。 - Dimitre Novatchev
我编辑了问题(在末尾)以澄清。我希望现在已经清楚了,否则我会添加更多的例子。 - brimborium
1
@brimborium,XSLT 1.0或XSLT 2.0都没有动态评估字符串的能力,该字符串恰好包含语法上有效的XPath表达式。在XSLT 3.0中,将有一个新指令xsl:evaluate(http://www.w3.org/TR/xslt-30/#element-evaluate)来实现这一点,因此您的一般问题可以在纯XSLT 3.0中解决--并且早期实现(例如Saxon 9.4)已经支持了该功能。 - Dimitre Novatchev
显示剩余5条评论

4

我创建了一个小项目,用于使用XPATH创建/更新XML:https://github.com/shenghai/xmodifier更改XML的代码如下:

DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
builderFactory.setNamespaceAware(true);
DocumentBuilder documentBuilder = builderFactory.newDocumentBuilder();
Document document = documentBuilder.parse(xmlfile);

XModifier modifier = new XModifier(document);
modifier.addModify("/configuration/param1", "asdf");
modifier.addModify("/configuration/param2", "asdf");
modifier.addModify("/configuration/test/param3", "true");
modifier.modify();

2
这看起来非常有趣,谢谢你分享这个。我会去看一下的。 - brimborium

1
我想指出一种新的/新颖的方法来完成你所描述的内容,通过使用VTD-XML...有很多原因可以解释为什么VTD-XML比提供给这个问题的所有其他解决方案都要好...这里有一些链接...

dfs

   import com.ximpleware.*;
    import java.io.*;
    public class modifyXML {
            public static void main(String[] s) throws VTDException, IOException{
                VTDGen vg = new VTDGen();
                if (!vg.parseFile("input.xml", false))
                    return;
                VTDNav vn = vg.getNav();
                AutoPilot ap = new AutoPilot(vn);
                ap.selectXPath("/configuration/param1/text()");
                XMLModifier xm = new XMLModifier(vn);
                // using XPath
                int i=ap.evalXPath();
                if(i!=-1){
                    xm.updateToken(i, "4.0");
                }
                String s1 ="<param2>asdf</param2>/n<test>/n<param3>true</param3>/n</test>";
                xm.insertAfterElement(s1);
                xm.output("output.xml");
            }
        }

谢谢您提供这个替代方案。您能讲一下为什么这个方案更好吗(除了代码看起来很干净之外,我喜欢这一点)?链接很好,但通常不是SO最好的方式,因为它们往往会随着时间而失效。 - brimborium
如果我只列出原因而没有任何详细的解释,那听起来就像廉价的垃圾邮件...因此,我认为如果您至少能浏览上面第三个URL参考列表,这可能会对您有所帮助...DOM、SAX或StAX都有很多缺点,这一点将变得清晰明了。 - vtd-xml-author

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接