【问题标题】:Is there a way to find innermost nodes recursively from xml using c# or vb有没有办法使用 c# 或 vb 从 xml 递归查找最内层节点
【发布时间】:2009-12-30 06:20:27
【问题描述】:

我有一个 XML 文件说

  <items>
      <item1>
        <piece>1300</piece>
        <itemc>665583</itemc> 
      </item1>
      <item2>
        <piece>100</piece>
        <itemc>665584</itemc>
      </item2>
    </items>

我正在尝试编写一个 c# 应用程序来获取到最内部节点的所有 x 路径,例如:

items/item1/piece
items/item1/itemc
items/item2/piece
items/item2/itemc

有没有使用 C# 或 VB 的方法?提前感谢您提供可能的解决方案。

【问题讨论】:

  • 请贴出你已经写好的代码:人们一般不喜欢为你写代码。
  • @Mitch Wheat 我还没有开始,只是在概念阶段。
  • 可以通过使用 XmlDocument 类的递归方法轻松完成。试试看!!
  • 不确定您所说的“所有 xpath”到底是什么意思。您的示例 xpath 表达式只是两个表达式,但您可以生成无限数量的 xpath 表达式或者您的意思是您想要一种遍历所有元素或节点的方法?
  • @ Roland 我的意思是从给定的 xml 派生以下内容: items/item1/piece items/item1/itemc items/item2/piece items/item2/itemc 我认为如果可以的话,迭代也可以访问父节点。

标签: .net xml xpath


【解决方案1】:
//*[not(*)]

是查找所有没有子元素的子元素的 XPath,因此您可以执行类似的操作

doc.SelectNodes("//*[not(*)]")

但我不太确定 .Net API,所以请检查一下。

参考

// --> descendant (not only children)
*  --> any name
[] --> predicate to evaluate
not(*) --> not having children

【讨论】:

  • 非常感谢。我确信 XPath 可以解决这个问题,但我在 w3schools 中找不到答案,所以 Google 来了。
  • 迅雷:获取节点后,可以询问每个节点的路径。
【解决方案2】:

你去吧:

static void Main()
{
   XmlDocument doc = new XmlDocument();
   doc.Load(@"C:\Test.xml");

   foreach (XmlNode node in doc.DocumentElement.ChildNodes)
   {
        ProcesNode(node, doc.DocumentElement.Name);
   }
}


    private void ProcesNode(XmlNode node, string parentPath)
    {
        if (!node.HasChildNodes
            || ((node.ChildNodes.Count == 1) && (node.FirstChild is System.Xml.XmlText)))
        {
            System.Diagnostics.Debug.WriteLine(parentPath + "/" + node.Name);
        }
        else
        {
            foreach (XmlNode child in node.ChildNodes)
            {
                ProcesNode(child, parentPath + "/" + node.Name);
            }
        }
    }

以上代码将为任何类型的文件生成所需的输出。请在需要的地方添加支票。 主要是我们忽略了输出中的Text节点(节点内的文本)。

【讨论】:

    【解决方案3】:

    只是为了稍微扩展 helios 的答案,您可以使用 [text()] 对 xpath 进行质量处理,以仅指定那些具有 text() 节点的节点:

    // XDocument
    foreach(XElement textNode in xdoc.XPathSelectElements("//*[not(*)][text()]"))
    {
        Console.WriteLine(textNode.Value);
    }
    
    // XmlDocument
    foreach(XmlText textNode in doc.SelectNodes("//*[not(*)]/text()"))
    {
        Console.WriteLine(textNode.Value);
    }
    

    【讨论】:

      【解决方案4】:

      这是一个 XSLT 解决方案,它为每个最里面的元素生成 XPATH 表达式。

      <?xml version="1.0" encoding="UTF-8"?>
      <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
          <xsl:template match="/">
              <xsl:apply-templates />
          </xsl:template>
      
          <!--Match on all elements that do not contain child elements -->
          <xsl:template match="//*[not(*)]">
              <!--look up the node tree and write out:
                 - a slash
                 - the name of the element
                 - and a predicate filter for the position of the element at each step -->
              <xsl:for-each select="ancestor-or-self::*">
                  <xsl:text>/</xsl:text>
                  <xsl:value-of select="local-name()"/>
                  <!--add a predicate filter to specify the position, in case there are more than one element with that name at that step -->
                  <xsl:text>[</xsl:text>
                  <xsl:value-of select="count(preceding-sibling::*[name()=name(current())])+1" />
                  <xsl:text>]</xsl:text>
              </xsl:for-each>  
              <!--Create a new line after ever element -->
              <xsl:text>&#xA;</xsl:text>
          </xsl:template>
      
      <!--override default template to prevent extra whitespace and carriage return from being copied into the output-->
      <xsl:template match="text()" />
      
      </xsl:stylesheet>
      

      我添加了谓词过滤器来指定元素的位置。这样,如果您在同一级别有多个 pieceitemc 元素,XPATH 将指定正确的元素。

      所以,而不是:

      items/item1/piece
      items/item1/itemc
      items/item2/piece
      items/item2/itemc
      

      它产生:

      /items[1]/item1[1]/piece[1]
      /items[1]/item1[1]/itemc[1]
      /items[1]/item2[1]/piece[1]
      /items[1]/item2[1]/itemc[1]
      

      【讨论】:

        【解决方案5】:

        下面的代码查找文档中的所有叶元素,并为每个元素输出一个 XPath 表达式,该表达式将明确地从文档根导航到该元素,包括在每个节点步骤的谓词以消除同名元素之间的歧义:

        static void Main(string[] arguments)
        {
            XDocument d = XDocument.Load("xmlfile1.xml");
        
            foreach (XElement e in d.XPathSelectElements("//*[not(*)]"))
            {
                Console.WriteLine("/" + string.Join("/",
                    e.XPathSelectElements("ancestor-or-self::*")
                        .Select(x => x.Name.LocalName 
                            + "[" 
                            + (x.ElementsBeforeSelf()
                                .Where(y => y.Name.LocalName == x.Name.LocalName)
                                .Count() + 1)
                            + "]")
                        .ToArray()));            
            }
        
            Console.ReadKey();
        }
        

        例如,这个输入:

        <foo>
          <bar>
            <fizz/>
            <baz>
              <bat/>
            </baz>
            <fizz/>
          </bar>
          <buzz></buzz>
        </foo>
        

        产生这个输出:

        /foo[1]/bar[1]/fizz[1]
        /foo[1]/bar[1]/baz[1]/bat[1]
        /foo[1]/bar[1]/fizz[2]
        /foo[1]/buzz[1]
        

        【讨论】:

        • 应该有办法找到节点的索引,而不用查询每个节点有多少之前的项目......
        • 这很好,但我不认为你可以在 lambda 表达式中到达那里。
        【解决方案6】:

        它未经测试,可能需要对其进行一些工作才能获得编译,但你想要这样的东西吗?

        class Program
        {
            static void Main()
            {
                XmlDocument xml = new XmlDocument();
                xml.Load("test.xml");
        
                var toReturn = new List<string>();
                GetPaths(string.Empty, xml.ChildNodes[0], toReturn);
            }
        
            public static void GetPaths(string pathSoFar, XmlNode node, List<string> results)
            {
                string scopedPath = pathSoFar + node.Name + "/";
        
                if (node.HasChildNodes)
                {
                    foreach (XmlNode itemNode in node.ChildNodes)
                    {
                        GetPaths(scopedPath, itemNode, results);
                    }
                }
                else
                {
                    results.Add(scopedPath);
                }
            }
        }
        

        对于大块的 xml,虽然它可能不是很节省内存。

        【讨论】:

        • 在回答 7 年后,是否有任何评论伴随反对票?
        【解决方案7】:

        也许不是最快的解决方案,但它表明允许将任意 XPath 表达式用作选择器,在我看来,这似乎也最清楚地表达了代码的意图。

        class Program
        {
            static void Main(string[] args)
            {
                XmlDocument xml = new XmlDocument();
                xml.Load("test.xml");
        
                IEnumerable innerItems = (IEnumerable)e.XPathEvaluate("//*[not(*)]");
                foreach (XElement innerItem in innerItems)
                {
                    Console.WriteLine(GetPath(innerItem));
                }
            }
        
            public static string GetPath(XElement e)
            {
                if (e.Parent == null)
                {
                    return "/" + e.Name;
                }
                else
                {
                    return GetPath(e.Parent) + "/" + e.Name;
                }
            }
        }
        

        【讨论】:

        • 此代码是特定于 xml 的。我认为 OP 需要一个跨给定 Xmls 通用的解决方案
        • 你是对的。我添加了 Helios 建议的 xpath,这使得它变得通用。尽管我最初的评论认为这清楚地表达了意图可能不再适用。您必须了解 XPath 才能注意这会找到所有最内层的节点。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-05-28
        • 2010-09-06
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-02-17
        相关资源
        最近更新 更多