按名称仅获取XML直接子元素

时间:2012-05-21 17:36:43

标签: java xml parsing dom xml-parsing

我的问题是:当存在与父元素的“孙子”同名的其他元素时,如何直接在特定父元素下获取元素。

我正在使用Java DOM library来解析XML Elements而我遇到了麻烦。这是我正在使用的xml的一些(一小部分):

<notifications>
  <notification>
    <groups>
      <group name="zip-group.zip" zip="true">
        <file location="C:\valid\directory\" />
        <file location="C:\another\valid\file.doc" />
        <file location="C:\valid\file\here.txt" />
      </group>
    </groups>
    <file location="C:\valid\file.txt" />
    <file location="C:\valid\file.xml" />
    <file location="C:\valid\file.doc" />
  </notification>
</notifications>

如您所见,您可以在两个位置放置<file>元素。无论是团体还是外部团体。我真的希望它以这种方式构建,因为它更加用户友好。

现在,每当我致电notificationElement.getElementsByTagName("file");时,它都会向我提供所有<file>元素,包括<group>元素下的元素。我以不同的方式处理这些类型的文件,所以这个功能是不可取的。

我想到了两个解决方案:

  1. 获取文件元素的父元素并相应地处理它(取决于它是<notification>还是<group>
  2. 重命名第二个<file>元素以避免混淆。
  3. 这些解决方案都不是那么令人满意,只留下它们的方式,只获得<file>元素,这些元素是<notification>元素的直接子元素。

    我对 IMPO 有关“最佳”方式的评论和答案持开放态度,但我对 DOM 解决方案非常感兴趣,因为这就是其余的这个项目正在使用。感谢。

9 个答案:

答案 0 :(得分:21)

我意识到你在5月@kentcdodds找到了解决这个问题的方法,但我刚刚发现了一个相当类似的问题,我认为(也许在我的用例中,但不在你的用途中),解决方案。

我的XML格式的一个非常简单的例子如下所示: -

<?xml version="1.0" encoding="utf-8"?>
<rels>
    <relationship num="1">
        <relationship num="2">
            <relationship num="2.1"/>
            <relationship num="2.2"/>
        </relationship>
    </relationship>
    <relationship num="1.1"/>
    <relationship num="1.2"/>

</rels>

正如你可以从这个片段中看到的那样,我想要的格式可以为[关系]节点设置N级嵌套,所以很明显我在Node.getChildNodes()中遇到的问题是我从所有节点获取所有节点层次结构的级别,没有任何关于节点深度的提示。

查看 API 一段时间后,我注意到其他两种方法实际上可能会有用: -

这两种方法似乎提供了获取Node的所有直接后代元素所需的一切。以下jsp代码应该给出一个如何实现它的相当基本的概念。抱歉,JSP。我现在把它变成了一个bean,但没有时间从挑选的代码中创建一个完全正常的版本。

<%@page import="javax.xml.parsers.DocumentBuilderFactory,
                javax.xml.parsers.DocumentBuilder,
                org.w3c.dom.Document,
                org.w3c.dom.NodeList,
                org.w3c.dom.Node,
                org.w3c.dom.Element,
                java.io.File" %><% 
try {

    File fXmlFile = new File(application.getRealPath("/") + "/utils/forms-testbench/dom-test/test.xml");
    DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
    DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
    Document doc = dBuilder.parse(fXmlFile);
    doc.getDocumentElement().normalize();

    Element docEl = doc.getDocumentElement();       
    Node childNode = docEl.getFirstChild();     
    while( childNode.getNextSibling()!=null ){          
        childNode = childNode.getNextSibling();         
        if (childNode.getNodeType() == Node.ELEMENT_NODE) {         
            Element childElement = (Element) childNode;             
            out.println("NODE num:-" + childElement.getAttribute("num") + "<br/>\n" );          
        }       
    }

} catch (Exception e) {
    out.println("ERROR:- " + e.toString() + "<br/>\n");
}

%>

此代码将提供以下输出,仅显示初始根节点的直接子元素。

NODE num:-1
NODE num:-1.1
NODE num:-1.2

希望这对任何人都有帮助。欢呼为最初的职位。

答案 1 :(得分:13)

您可以使用XPath,使用两个路径获取它们并以不同方式处理它们。

要让<file>个节点指向使用<notification>的{​​{1}}的孩子,使用//notification/file的{​​{1}}使用<group>。{/ p>

这是一个简单的示例:

//groups/group/file

应输出:

public class SO10689900 {
    public static void main(String[] args) throws Exception {
        DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
        Document doc = db.parse(new InputSource(new StringReader("<notifications>\n" + 
                "  <notification>\n" + 
                "    <groups>\n" + 
                "      <group name=\"zip-group.zip\" zip=\"true\">\n" + 
                "        <file location=\"C:\\valid\\directory\\\" />\n" + 
                "        <file location=\"C:\\this\\file\\doesn't\\exist.grr\" />\n" + 
                "        <file location=\"C:\\valid\\file\\here.txt\" />\n" + 
                "      </group>\n" + 
                "    </groups>\n" + 
                "    <file location=\"C:\\valid\\file.txt\" />\n" + 
                "    <file location=\"C:\\valid\\file.xml\" />\n" + 
                "    <file location=\"C:\\valid\\file.doc\" />\n" + 
                "  </notification>\n" + 
                "</notifications>")));
        XPath xpath = XPathFactory.newInstance().newXPath();
        XPathExpression expr1 = xpath.compile("//notification/file");
        NodeList nodes = (NodeList)expr1.evaluate(doc, XPathConstants.NODESET);
        System.out.println("Files in //notification");
        printFiles(nodes);

        XPathExpression expr2 = xpath.compile("//groups/group/file");
        NodeList nodes2 = (NodeList)expr2.evaluate(doc, XPathConstants.NODESET);
        System.out.println("Files in //groups/group");
        printFiles(nodes2);
    }

    public static void printFiles(NodeList nodes) {
        for (int i = 0; i < nodes.getLength(); ++i) {
            Node file = nodes.item(i);
            System.out.println(file.getAttributes().getNamedItem("location"));
        }
    }
}

答案 2 :(得分:12)

嗯,这个问题的DOM解决方案实际上非常简单,即使它不太优雅当我遍历调用filesNodeList时返回的notificationElement.getElementsByTagName("file");我只是检查父节点是否是名字是“通知”。如果不是,那么我忽略它,因为它将由<group>元素处理。这是我的代码解决方案:

for (int j = 0; j < filesNodeList.getLength(); j++) {
  Element fileElement = (Element) filesNodeList.item(j);
  if (!fileElement.getParentNode().getNodeName().equals("notification")) {
    continue;
  }
  ...
}

答案 3 :(得分:4)

如果您坚持使用DOM API

NodeList nodeList = doc.getElementsByTagName("notification")
    .item(0).getChildNodes();

// get the immediate child (1st generation)
for (int i = 0; i < nodeList.getLength(); i++)
    switch (nodeList.item(i).getNodeType()) {
        case Node.ELEMENT_NODE:

            Element element = (Element) nodeList.item(i);
            System.out.println("element name: " + element.getNodeName());
            // check the element name
            if (element.getNodeName().equalsIgnoreCase("file"))
            {

                // do something with you "file" element (child first generation)

                System.out.println("element name: "
                    + element.getNodeName() + " attribute: "
                    + element.getAttribute("location"));

            }
    break;

}

我们的第一个任务是获得一个元素“Notification”(在本例中是第一个-item(0) - )及其所有子元素:

NodeList nodeList = doc.getElementsByTagName("notification")
    .item(0).getChildNodes();

(稍后您可以使用获取所有元素来处理所有元素。)

对于“通知”的每个孩子:

for (int i = 0; i < nodeList.getLength(); i++)

你首先得到它的类型,以查看它是否是一个元素:

switch (nodeList.item(i).getNodeType()) {
    case Node.ELEMENT_NODE:
        //.......
        break;  
}

如果是这样,那么你的孩子就是“文件”,那不是大孩子“通知”

你可以查看它们:

if (element.getNodeName().equalsIgnoreCase("file"))
{

    // do something with you "file" element (child first generation)

    System.out.println("element name:"
        + element.getNodeName() + " attribute: "
        + element.getAttribute("location"));

}

并且ouptut是:

element name: file
element name:file attribute: C:\valid\file.txt
element name: file
element name:file attribute: C:\valid\file.xml
element name: file
element name:file attribute: C:\valid\file.doc

答案 4 :(得分:3)

我在其中一个项目中遇到了同样的问题并编写了一个函数,它将返回仅包含直接子项的List<Element>。 基本上它检查getElementsByTagName返回的每个节点,如果它的parentNode实际上是我们正在搜索孩子的节点:

public static List<Element> getDirectChildsByTag(Element el, String sTagName) {
        NodeList allChilds = el.getElementsByTagName(sTagName);
        List<Element> res = new ArrayList<>();

        for (int i = 0; i < allChilds.getLength(); i++) {
            if (allChilds.item(i).getParentNode().equals(el))
                res.add((Element) allChilds.item(i));
        }

        return res;
    }

如果有一个叫做#34;通知&#34;的子节点,kentcdodds接受的答案将返回错误的结果(例如孙子)。 - 例如当元素&#34; group&#34;返回孙子时将有#34;通知&#34;的名称。我在项目中遇到了这个设置,这就是为什么我想出了我的功能。

答案 5 :(得分:0)

我写了这个函数来通过tagName获取节点值,限制到顶级

public static String getValue(Element item, String tagToGet, String parentTagName) {
    NodeList n = item.getElementsByTagName(tagToGet);
    Node nodeToGet = null;
    for (int i = 0; i<n.getLength(); i++) {
        if (n.item(i).getParentNode().getNodeName().equalsIgnoreCase(parentTagName)) {
            nodeToGet = n.item(i);
        }
    }
    return getElementValue(nodeToGet);
}

public final static String getElementValue(Node elem) {
    Node child;
    if (elem != null) {
        if (elem.hasChildNodes()) {
            for (child = elem.getFirstChild(); child != null; child = child
                    .getNextSibling()) {
                if (child.getNodeType() == Node.TEXT_NODE) {
                    return child.getNodeValue();
                }
            }
        }
    }
    return "";
}

答案 6 :(得分:0)

我遇到了一个相关问题,我需要处理直接子节点,即使所有“文件”节点的处理方式相似。对于我的解决方案,我将Element的父节点与正在处理的节点进行比较,以确定Element是否是直接子节点。

NodeList fileNodes = parentNode.getElementsByTagName("file");
for(int i = 0; i < fileNodes.getLength(); i++){
            if(parentNode.equals(fileNodes.item(i).getParentNode())){
                if (fileNodes.item(i).getNodeType() == Node.ELEMENT_NODE) {

                    //process the child node...
                }
            }
        }

答案 7 :(得分:0)

有一个很好的LINQ解决方案:

For Each child As XmlElement In From cn As XmlNode In xe.ChildNodes Where cn.Name = "file"
    ...
Next

答案 8 :(得分:0)

我最终在 Kotlin 中创建了一个扩展函数来做到这一点

fun Element.childrenWithTagName(name: String): List<Node> = childNodes
    .asList()
    .filter { it.nodeName == name }

呼叫者可能会像这样使用它:

val meta = target.newChildElement("meta-coverage")
source.childrenWithTagName("counter").forEach {
    meta.copyElementWithAttributes(it)
}

列表实现:


fun NodeList.asList(): List<Node> = InternalNodeList(this)

private class InternalNodeList(
    private val list: NodeList,
    override val size: Int = list.length
) : RandomAccess, AbstractList<Node>() {
    override fun get(index: Int): Node = list.item(index)
}