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

Posted

技术标签:

【中文标题】按名称仅获取 XML 直接子元素【英文标题】:Get XML only immediate children elements by name 【发布时间】:2012-05-28 05:28:18 【问题描述】:

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

我正在使用 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>

如您所见,您可以在两个地方放置&lt;file&gt; 元素。无论是在组内还是在组外。我真的希望它采用这种结构,因为它对用户更友好。

现在,每当我调用notificationElement.getElementsByTagName("file"); 时,它都会为我提供所有&lt;file&gt; 元素,包括&lt;group&gt; 元素下的元素。我以不同的方式处理这些文件中的每一种,所以这个功能是不可取的。

我想到了两种解决方案:

    获取文件元素的父元素并进行相应处理(取决于是&lt;notification&gt;还是&lt;group&gt;。 重命名第二个&lt;file&gt; 元素以避免混淆。

这些解决方案都不像只是让事物保持原样并仅获得作为&lt;notification&gt; 元素的直接子元素的&lt;file&gt; 元素那样可取。

我愿意接受 IMPO cmets 并回答有关“最佳”方法的问题,但我真的对 DOM 解决方案很感兴趣,因为这就是其余的这个项目正在使用。谢谢。

【问题讨论】:

为什么不使用 XPath 来获取两个节点列表并区别对待? //groups/group/file//notification/file 足以拥有它们。还是您只需要一个 XPath 来获取它们? 为什么不通过你自己的循环来创建这个集合,比如点击:"NodeList nodes = element.getChildNodes(); for (int i = 0; i @Alex org.w3c.dom 不支持 XPath;他想为此使用不同的库,例如 org.jdom.xpath……尽管我完全同意这是更优雅的方法。 javax.xml.xpath 是Java Standard,所以我认为他几乎可以使用它,不需要为了这个简单的任务而获得JDom。 我应该提一下,这只是一个更大的 xml 文件的一小部分 :) 想让它可读。 【参考方案1】:

我最终在 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)


【讨论】:

【参考方案2】:

嗯,这个问题的 DOM 解决方案其实很简单,即使它不是太优雅。

当我遍历调用notificationElement.getElementsByTagName("file")时返回的filesNodeList时,我只是检查父节点的名称是否为“通知”。如果不是,那么我将忽略它,因为这将由 &lt;group&gt; 元素处理。这是我的代码解决方案:

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

【讨论】:

@JanusTroelsen,如果您在将项目转换为元素时谈论第二行,那么它取决于您正在解析的 DOM... 如果不是,您是什么意思? 为什么不直接遍历 element.getChildNodes()? 'getParentNode' 功能(和 'getNodeName')在 'Node' 接口上可用。因此,仅检查名称,不需要演员表。 (并且只是为了安全开关,等于是“通知”.equals(...))【参考方案3】:

如果你坚持使用 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"));


输出是:

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

【讨论】:

感谢您的解决方案。我的解决方案与此类似,但我不会遍历所有子元素,因为该元素中有更多子元素,我没有在我的问题中显示这些子元素,只是为了避免信息过载。无论如何,再次感谢。 +1 以获得好的答案。 @kentcdodds。我更新了我的答案。你看,在不使用“ID”的情况下使用 XML 基本上只剩下“getElementsByTagName”和“getChildNodes”可以使用。在我看来,直接使用 DOM 时,您没有其他答案。抱歉,您必须坚持使用 DOM。无论解决方案如何,它都可能归结为您如何访问给定节点的子节点(在本例中为“通知” ")。我的解决方案检查类型 Node 以便为您节省不必要的工作。但是您仍然必须迭代所有孩子。当没有“ID”时会发生这种情况:您最终会得到一个集合。 @arthur (off-topic) 出于对所有神圣事物的热爱,请在句号和下一句的第一个字母之间添加一些空格。这是纯粹的疯狂!【参考方案4】:

我在我的一个项目中遇到了同样的问题,并编写了一个小函数,它将返回一个仅包含直系子级的 List&lt;Element&gt;。 基本上,它检查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;
    

如果有一个名为“通知”的子节点 - 例如,kentcdodds 接受的答案将返回错误的结果(例如孙子)。当元素“组”将具有名称“通知”时返回孙子。我在我的项目中遇到了这种设置,这就是我想出我的功能的原因。

【讨论】:

【参考方案5】:

有一个不错的 LINQ 解决方案:

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

【讨论】:

【参考方案6】:

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

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】:

我写了这个函数来通过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 "";

【讨论】:

【参考方案8】:

我意识到您在 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>

正如您希望从这个 sn-p 中看到的那样,我想要的格式可以有 N 级嵌套 [relationship] 节点,所以很明显,我使用 Node.getChildNodes() 遇到的问题是我正在获取所有节点从层次结构的所有级别,并且没有任何关于节点深度的提示。

看了一会儿API,我注意到实际上还有另外两种可能有用的方法:-

Node.getFirstChild() Node.getNextSibling()

这两种方法似乎提供了获取节点的所有直接后代元素所需的一切。下面的 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 为该问题提供了另一个完全可以接受的答案。 :) 干杯 @kentcdodds 解决并找到另一个解决方案的非常有趣的问题。很高兴我可以继续使用 org.w3c.dom 而无需移植现有代码。感谢您的提问! +1 是一个非常简单、简单和干净的解决方案。您可以使用 for 循环和这种技术,以保持优雅并保持范围:for (Node n = docEl.getFirstChild(); n != null; n = n.getNextSibling()) 和getChildNodes有什么区别? @ceving - 我认为问题在于 getChildNodes 正在从层次结构的所有级别带回所有子节点。这是 8 年前的事了,所以 API 很可能从那时起就在继续发展,但我猜当时 getChildNodes 对我自己或 kentcdodds 都不起作用。【参考方案9】:

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

要获得&lt;notification&gt;&lt;file&gt; 直接子节点,请使用//notification/file,对于&lt;group&gt; 中的节点,请使用//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"));
        
    

它应该输出:

Files in //notification
location="C:\valid\file.txt"
location="C:\valid\file.xml"
location="C:\valid\file.doc"
Files in //groups/group
location="C:\valid\directory\"
location="C:\this\file\doesn't\exist.grr"
location="C:\valid\file\here.txt"

【讨论】:

看起来是个不错的答案,以后我可能会从DOM 转到XPath。但对于这个项目,这是我需要做的最后一件事,我想坚持使用DOM。但是,除非我得到DOM 的另一个答案,否则我会接受你的答案,因为这是一个很好的答案。无论哪种方式,您都会因为如此详尽的答案而获得 +1。 如果您需要坚持使用 DOM,那么您需要使用 ((Node)notificationElement).getChildNodes() 迭代 NodeList 并只保留名称为 file 的那个。理想情况下,您必须找到所有notification 标签才能做到这一点。 group 标签也需要这样做。 我找到了更好的解决方案。不起作用的原因是因为notification 元素中有很多childNodes。不过我回答了这个问题。谢谢你的好回答。将来我真的会研究 XPath。 我正在寻找一种方法来通过路径root/etc/foo 搜索元素并最终创建它,或者如果这些不存在,它是父节点。我可以在子节点中使用比 for 循环更好的东西吗?我只关心第一次出现。 XPath 非常慢。我有一个使用 XPath 进行每个节点选择的程序,它花了 5 个多小时才完成。在我使用 getChildNodes 将每个 XPath 使用替换为等效函数后,程序在不到 10 分钟内完成。

以上是关于按名称仅获取 XML 直接子元素的主要内容,如果未能解决你的问题,请参考以下文章

RF库XML测试通过xpath查找元素的说明

Java中的W3C DOM API,按名称获取子元素

如何通过jQuery函数仅获取直接子元素

按名称访问 XML DOM 子节点

如何仅在python selenium中获取第一层子元素

XPath 获取除具有特定名称的子元素之外的所有子元素?