按名称仅获取 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>
如您所见,您可以在两个地方放置<file>
元素。无论是在组内还是在组外。我真的希望它采用这种结构,因为它对用户更友好。
现在,每当我调用notificationElement.getElementsByTagName("file");
时,它都会为我提供所有<file>
元素,包括<group>
元素下的元素。我以不同的方式处理这些文件中的每一种,所以这个功能是不可取的。
我想到了两种解决方案:
-
获取文件元素的父元素并进行相应处理(取决于是
<notification>
还是<group>
。
重命名第二个<file>
元素以避免混淆。
这些解决方案都不像只是让事物保持原样并仅获得作为<notification>
元素的直接子元素的<file>
元素那样可取。
我愿意接受 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
时,我只是检查父节点的名称是否为“通知”。如果不是,那么我将忽略它,因为这将由 <group>
元素处理。这是我的代码解决方案:
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<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;
如果有一个名为“通知”的子节点 - 例如,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,使用两条路径来获取它们并以不同方式处理它们。
要获得<notification>
的<file>
直接子节点,请使用//notification/file
,对于<group>
中的节点,请使用//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 直接子元素的主要内容,如果未能解决你的问题,请参考以下文章