如何找到具有给定名称的最高级别后代

时间:2017-09-01 20:43:02

标签: c# xml linq

我正在寻找一种使用linq在XML树中查找第一个结果级别的方法。我拥有的XML如下所示:

<column>
    <row>
        <object/>
        <column column-id="1" column-name="abc">
            <row>
                <column>
                    <row>
                        <column column-id="2" column-name="abc"/>
                    </row>
                </column>
            </row>
        </column>
    </row>
    <row>
        <column column-id="3" column-name="abc">
            <row>
                <column/>
            </row>
        </column>
    </row>
</column>

现在我希望得到列名为columns的所有第一级abc。所以结果应该是:

<column column-id="1" column-name="abc">...</column>
<column column-id="3" column-name="abc">...</column>

我已经尝试过以下代码:

layout.Descendants("column")
      .Where(x => x.Attribute("column-name").Value.Equals("abc") && !x.Ancestors("column").Any());

当搜索的XElement layout未命名为"column"且未嵌套在名为"column"的任何容器元素内时,此方法正常。但我的XElement实际上属于一个文档,其根元素名为"column",因此x.Ancestors("column").Any()表达式错误地过滤掉所有匹配项。即通过初始化layout,可以使用上面的XML字符串重现该问题,如下所示:

var layout = XElement.Parse(xmlString);

我想保留变量中的关系,因为我必须稍后进行更改。

是否有办法限制祖先选择器?

2 个答案:

答案 0 :(得分:1)

假设您事先并不知道要查询的元素的精确深度,您要做的是下降指定元素下面的元素层次结构,并返回最顶层匹配给定条件的元素,在这种情况下具有名称"column"

作为一种快速而肮脏的方法,您只能使用TakeWhile()检查候选匹配列的祖先是layout的后代还是

var matches = layout
    .Descendants("column")
    .Where(x => (string)x.Attribute("column-name") == "abc" && !x.Ancestors().TakeWhile(a => a != layout).Any(a => a.Name == "column"));

更高性能的通用解决方案是在XElement上引入一个扩展方法,该方法枚举给定元素的所有后代,返回与给定谓词匹配的最顶层元素。这通常是有用的,例如如果想要查询将要接近深层XML层次结构顶层的后代,因为它可以避免不必要地下降到匹配的节点中:

public static partial class XElementExtensions
{
    /// <summary>
    /// Enumerates through all descendants of the given element, returning the topmost elements that match the given predicate
    /// </summary>
    /// <param name="root"></param>
    /// <param name="filter"></param>
    /// <returns></returns>
    public static IEnumerable<XElement> DescendantsUntil(this XElement root, Func<XElement, bool> predicate, bool includeSelf = false)
    {
        if (predicate == null)
            throw new ArgumentNullException();
        return GetDescendantsUntil(root, predicate, includeSelf);
    }

    static IEnumerable<XElement> GetDescendantsUntil(XElement root, Func<XElement, bool> predicate, bool includeSelf)
    {
        if (root == null)
            yield break;
        if (includeSelf && predicate(root))
        {
            yield return root;
            yield break;
        }
        var current = root.FirstChild<XElement>();
        while (current != null)
        {
            var isMatch = predicate(current);
            if (isMatch)
                yield return current;

            // If not a match, get the first child of the current element.
            XElement next = (isMatch ? null : current.FirstChild<XElement>());

            if (next == null)
                // If no first child, get the next sibling of the current element.
                next = current.NextSibling<XElement>();

            // If no more siblings, crawl up the list of parents until hitting the root, getting the next sibling of the lowest parent that has more siblings.
            if (next == null)
            {
                for (var parent = current.Parent as XElement; parent != null && parent != root && next == null; parent = parent.Parent as XElement)
                {
                    next = parent.NextSibling<XElement>();
                }
            }

            current = next;
        }
    }

    public static TNode FirstChild<TNode>(this XNode node) where TNode : XNode
    {
        var container = node as XContainer;
        if (container == null)
            return null;
        return container.FirstNode.NextSibling<TNode>(true);
    }

    public static TNode NextSibling<TNode>(this XNode node) where TNode : XNode
    {
        return node.NextSibling<TNode>(false);
    }

    public static TNode NextSibling<TNode>(this XNode node, bool includeSelf) where TNode : XNode
    {
        if (node == null)
            return null;
        for (node = (includeSelf ? node : node.NextNode); node != null; node = node.NextNode)
        {
            var nextTNode = node as TNode;
            if (nextTNode != null)
                return nextTNode;
        }
        return null;
    }
}

然后使用它:

var matches = layout
    .DescendantsUntil(x => x.Name == "column")
    .Where(x => (string)x.Attribute("column-name") == "abc");

扩展方法应该具有合理的性能,因为它避免了递归和复杂的嵌套linq查询。

示例.Net fiddle显示两个选项。

答案 1 :(得分:0)

说明问题的另一种方法是,您希望谓词包含与文档根目录的距离。

这是一个执行此操作的函数:

static int DistanceToRoot(XElement elem, XElement root)
{
    var dist = 0;

    var curr = elem;

    while(curr != root)
    {
        dist++;
        curr = curr.Parent;
    }

    return dist;
}

你就这样使用它(基于你的例子,我们想要的距离 2 ):

var columns = from column in xml.Descendants("column")
              where 
                  DistanceToRoot(column, xml.Root) == 2  &&
                  column.Attribute("column-name").Value == "abc"
              select column;


 foreach(var abc in xyzs)
 {
     Console.WriteLine(abc);
     Console.Write("Distance is: ");
     Console.WriteLine(DistanceToRoot(abc, xml.Root));
     Console.ReadLine();
 }

结果是:

<column column-id="1" column-name="abc">
  <row>
    <column>
      <row>
        <column column-id="2" column-name="abc" />
      </row>
    </column>
  </row>
</column>
Distance is: 2

<column column-id="3" column-name="abc">
  <row>
    <column />
  </row>
</column>
Distance is: 2

<强> Rextester Demo