删除没有子节点的父节点

6
我是一名有用的助手,我可以为您翻译文本。

我有一个与从xml文件中删除特定节点相关的问题。

这是我的XML示例:

<?xml version="1.0" encoding="UTF-8"?>
<root>
  <nodeA attribute="1">
    <nodeB attribute="table">
      <nodeC attribute="500"></nodeC>
      <nodeC attribute="5"></nodeC>
    </nodeB>
    <nodeB attribute="3">
      <nodeC attribute="4"></nodeC>
      <nodeC attribute="5"></nodeC>
      <nodeC attribute="5"></nodeC>
    </nodeB>
    <nodeB attribute="placeHolder">
    <nodeB attribute="toRemove">
      <nodeB attribute="glass"></nodeB>
        <nodeE attribute="7"></nodeE>
      <nodeB attribute="glass"></nodeB>
      <nodeB attribute="glass"></nodeB>
    </nodeB>
    </nodeB>
    <nodeB attribute="3">
      <nodeC attribute="4"></nodeC>
      <nodeC attribute="5"></nodeC>
      <nodeC attribtue="5"></nodeC>
     </nodeB>
    <nodeB attribute="placeHolder">
    <nodeB attribute="toRemove">
      <nodeB attribute="glass"></nodeB>
        <nodeE attribute="7"></nodeE>
      <nodeB attribute="glass"></nodeB>
      <nodeB attribute="glass"></nodeB>
    </nodeB>
    </nodeB>
  </nodeA>
</root>

我希望删除节点nodeB="toRemove"而不删除该节点的子项。之后,我需要对nodeB attribute="placeHolder"执行相同的操作。结果的一部分如下所示:
     <nodeB attribute="3">
      <nodeC attribute="4"></nodeC>
      <nodeC attribute="5"></nodeC>
      <nodeC attribtue="5"></nodeC>
     </nodeB>
     <nodeB attribute="glass"></nodeB>
        <nodeE attribute="7"></nodeE>
     <nodeB attribute="glass"></nodeB>
     <nodeB attribute="glass"></nodeB>

我一直在尝试编写类似这样的代码来实现这个目标:
        XmlNodeList nodeList = doc.SelectNodes("//nodeB[@attribute=\"toRemove\"]");

        foreach (XmlNode node in nodeList)
        {
            foreach (XmlNode child in node.ChildNodes)
            {
                node.ParentNode.AppendChild(child);
            }
            node.ParentNode.RemoveChild(node);
        }
        doc.Save(XmlFilePathSource);

我能够找到具有所需属性toRemove或placeHolder的节点,但我无法将这些节点的子元素上移一级。你能帮我解决这个问题吗?可以使用Linq、XDocument、XmlReader等方法来解决,但我更喜欢使用XmlDocument。
非常感谢您提前提供的任何帮助。
编辑:
在这种情况下,我使用了稍作修改的代码(为了保持顺序),Chuck Savage在下面写的代码。一旦删除。
  <nodeB attribute="toRemove"> </nodeB>

然后再用同样的方法处理

  <nodeB attribute="placeHolder"></nodeB>

这里是稍微修改过的代码

  XElement root = XElement.Load(XmlFilePathSource); 
  var removes = root.XPathSelectElements("//nodeB[@attribute=\"toRemove\"]");
  foreach (XElement node in removes.ToArray())
  {
    node.Parent.AddAfterSelf(node.Elements());
    node.Remove();
  }
  root.Save(XmlFilePathSource);

@MiMo 提供的 XSLT 方法在这种情况下非常有用。

你的许多 nodeC 元素缺少闭合标签。你能否更新你的问题,使用有效的、格式良好的 XML? - Ryan Gates
我已经更新了我的简化 XML 文件。感谢提示,现在它更容易让其他人阅读了。 - wariacik
3个回答

5
问题在于当你枚举节点的子项时,不能修改文档节点。相较于修改现有节点,你应该创建新节点,而使用 XmlDocument 实现这一点变得有点棘手。
完成这种转换最简单的方法是使用 XSLT,也就是应用以下 XSLT:
<xsl:stylesheet 
  version="1.0" 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

  <xsl:output method="xml" indent="yes"/>

  <xsl:template match="nodeB[@attribute='toRemove' or @attribute='placeHolder']">
    <xsl:apply-templates/>
  </xsl:template>

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

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

</xsl:stylesheet>

将输入文件传入后,输出结果为:
<root>
  <nodeA attribute="1">
    <nodeB attribute="table">
      <nodeC attribute="500" />
      <nodeC attribute="5" />
    </nodeB>
    <nodeB attribute="3">
      <nodeC attribute="4" />
      <nodeC attribute="5" />
      <nodeC attribute="5" />
    </nodeB>
    <nodeB attribute="glass" />
    <nodeE attribute="7" />
    <nodeB attribute="glass" />
    <nodeB attribute="glass" />
    <nodeB attribute="3">
      <nodeC attribute="4" />
      <nodeC attribute="5" />
      <nodeC attribtue="5" />
    </nodeB>
    <nodeB attribute="glass" />
    <nodeE attribute="7" />
    <nodeB attribute="glass" />
    <nodeB attribute="glass" />
  </nodeA>
</root>

应用XSLT的代码非常简单:

  XslCompiledTransform transform = new XslCompiledTransform();
  transform.Load(@"c:\temp\nodes.xslt");
  transform.Transform(@"c:\temp\nodes.xml", @"c:\temp\nodes-cleaned.xml");

如果不可能(或不希望)使用外部文件来进行XSLT转换,则可以从字符串中读取:

  string xsltString =
    @"<xsl:stylesheet 
      version='1.0' 
      xmlns:xsl='http://www.w3.org/1999/XSL/Transform'>

      <xsl:output method=""xml"" indent=""yes""/>

      <xsl:template match=""nodeB[@attribute='toRemove' or @attribute='placeHolder']"">
        <xsl:apply-templates/>
      </xsl:template>

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

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

    </xsl:stylesheet>";
  XslCompiledTransform transform = new XslCompiledTransform();
  using (StringReader stringReader = new StringReader(xsltString))
  using (XmlReader reader = XmlReader.Create(stringReader)) {
    transform.Load(reader);
  }
  transform.Transform(@"c:\temp\nodes.xml", @"c:\temp\nodes-cleaned.xml");    

谢谢你的回答。等我有能力加载附加文件时,我会再次使用这种方法。然而在这个特定的情况下,我不能使用外部文件。因此,在我的情况下,无法加载xslt文件是不可选的。 - wariacik
@wariacik:即使没有外部文件,你仍然可以使用XSLT——我扩展了我的回答。XSLT的问题在于,如果你对它们不熟悉,使用起来会很困难——但是,如果你经常进行XML处理,学习它们是一个不错的投资。 - MiMo
谢谢。我不知道我可以将xslt作为字符串加载。这对我的项目非常有用。 - wariacik

4
使用Linq-to-XML和您的XPath,
XElement root = XElement.Load(XmlFilePathSource); // or .Parse(string)
var removes = root.XPathSelectElements("//nodeB[@attribute=\"toRemove\"]");
foreach (XElement node in removes.ToArray())
{
    node.AddBeforeSelf(node.Elements());
    node.Remove();
}
root.Save(XmlFilePathSource);

注意:XPath 可在 System.Xml.XPath 中使用。
注意2:由于您更喜欢使用 XmlDocument,因此可以使用这些扩展来进行 XmlDocument 的转换。这些扩展

这里的一个缺点是,保留的子节点将被添加到包含节点的末尾,而不是留在它们所在的文档部分。提问者并没有说保留它们的位置是必需的,但很容易成为要求。 - JLRishe
@JLRishe 如果你看一下原帖的代码,他基本上是在做同样的事情,但我喜欢你的观点。 - Chuck Savage
我真的很喜欢这种方法,但在这种情况下,保留子节点的位置是必需的。有没有办法让子节点保持在它们原来所在的文档部分? - wariacik
编辑:看起来<code>node.Parent.AddAfterSelf(node.Elements());</code>而不是.Add方法可以完成任务。我稍后会测试它并确保我得到了正确的输出。 - wariacik
@wariacik,我调整了答案。你想要的是node.AddBeforeSelf(node.Elements()) - Chuck Savage

3

我知道这是一个老问题,但我直接使用XmlDocument编写了这个答案。

如果有人更喜欢用这种方式,可以添加以下内容:

XmlNode child_to_remove = parent.ChildNodes[i]; // get the child to remove

// move all the children of "child_to_remove" to be the child of their grandfather (== parent)
while(child_to_remove.HasChildNodes)
    parent.InsertBefore(child_to_remove.ChildNodes[0], child_to_remove);

parent.RemoveChild(child_to_remove);

这就是全部内容了,希望能对任何人有所帮助。

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