【问题标题】:Where's the bug in this tree traversal code?这个树遍历代码中的错误在哪里?
【发布时间】:2011-05-07 12:45:03
【问题描述】:

Traverse() 中有一个错误导致它多次迭代节点。

错误代码

public IEnumerable<HtmlNode> Traverse()
{
    foreach (var node in _context)
    {
        yield return node;
        foreach (var child in Children().Traverse())
            yield return child;
    }
}

public SharpQuery Children()
{
    return new SharpQuery(_context.SelectMany(n => n.ChildNodes).Where(n => n.NodeType == HtmlNodeType.Element), this);
}

public SharpQuery(IEnumerable<HtmlNode> nodes, SharpQuery previous = null)
{
    if (nodes == null) throw new ArgumentNullException("nodes");
    _previous = previous;
    _context = new List<HtmlNode>(nodes);
}

测试代码

    static void Main(string[] args)
    {
        var sq = new SharpQuery(@"
<a>
    <b>
        <c/>
        <d/>
        <e/>
        <f>
            <g/>
            <h/>
            <i/>
        </f>
    </b>
</a>");
        var nodes = sq.Traverse();
        Console.WriteLine("{0} nodes: {1}", nodes.Count(), string.Join(",", nodes.Select(n => n.Name)));
        Console.ReadLine();

输出

19 个节点:#document,a,b,c,g,h,i,d,g,h,i,e,g,h,i,f,g,h,i

预期输出

每个字母 a-i 打印一次。

似乎无法弄清楚哪里出了问题...node.ChildNodes 是否只返回直接孩子,对吗? (来自 HtmlAgilityPack)


如果你想尝试自己运行的话,完整的课程(浓缩)。

public class SQLite
{
    private readonly List<HtmlNode> _context = new List<HtmlNode>();
    private readonly SQLite _previous = null;

    public SQLite(string html)
    {
        var doc = new HtmlDocument();
        doc.LoadHtml(html);
        _context.Add(doc.DocumentNode);
    }

    public SQLite(IEnumerable<HtmlNode> nodes, SQLite previous = null)
    {
        if (nodes == null) throw new ArgumentNullException("nodes");
        _previous = previous;
        _context = new List<HtmlNode>(nodes);
    }

    public IEnumerable<HtmlNode> Traverse()
    {
        foreach (var node in _context)
        {
            yield return node;
            foreach (var child in Children().Traverse())
                yield return child;
        }
    }

    public SQLite Children()
    {
        return new SQLite(_context.SelectMany(n => n.ChildNodes).Where(n => n.NodeType == HtmlNodeType.Element), this);
    }
}

【问题讨论】:

  • 调试器告诉你什么?
  • @Oli:关于什么?我应该在哪里设置断点?这是一个逻辑错误,而不是崩溃错误。
  • @Mark:我不确定我是否理解。调试器不仅仅用于诊断崩溃!
  • 你能发布整个 SharpQuery 类吗?
  • @BFree:675 行,不到四分之一完整>。

标签: c# html-agility-pack


【解决方案1】:

首先,请注意,只要我们迭代没有任何兄弟元素的元素,一切都会按计划进行。一旦我们点击元素&lt;c&gt;,事情就开始变得混乱。同样有趣的是&lt;c&gt;&lt;d&gt;&lt;e&gt; 都认为它们包含&lt;f&gt; 的孩子。

让我们仔细看看你对SelectMany()的调用:

_context.SelectMany(n => n.ChildNodes)

遍历_context 中的项目并累积每个项目的子节点。让我们看看_context。一切都应该没问题,因为它是一个长度为1List。还是这样?

我怀疑您的 SharpQuery(string) 构造函数将兄弟元素存储在同一个列表中。在这种情况下,_context 的长度可能不再是 1,请记住,SelectMany()累积列表中每个项目的子节点。

例如,如果_context 是一个包含&lt;c&gt;&lt;d&gt;&lt;e&gt;&lt;f&gt; 的列表,则只有&lt;f&gt; 有子元素,并且SelectMany() 为每个元素调用一次,它会累积并返回&lt;f&gt; 的孩子四次。

我认为这是你的错误。

编辑:既然你已经发布了完整的课程,我就不必再猜测了。查看迭代 &lt;b&gt; 时的操作顺序(迭代器替换为列表以便更好地理解):

  1. &lt;b&gt;上调用Children(),返回&lt;c&gt;&lt;d&gt;&lt;e&gt;&lt;f&gt;
  2. 在结果列表中调用Traverse()
    1. &lt;c&gt;上调用Children()但是 _context实际上包含&lt;c&gt;&lt;d&gt;&lt;e&gt;&lt;f&gt;,不仅是&lt;c&gt;,所以返回&lt;g&gt;&lt;h&gt;&lt;i&gt;
    2. &lt;d&gt; 上调用Children(),同样的事情,因为_context 相同对于两者 &lt;c&gt;&lt;d&gt;(和&lt;e&gt;,和&lt;f&gt;),
    3. 起泡、冲洗、重复。

【讨论】:

  • 我刚刚发布了字符串构造函数……它只是将文档节点添加到上下文中,没有别的。
  • 您的问题是兄弟节点共享相同的_context,而这不适用于SelectMany()。请参阅我的更新答案。
  • 嗯,这就解释了这个错误。谢谢:)
  • 你发布了两次相同的链接;)
  • 哎呀,我的错 :) 由于您必须重写它,您可能希望从循环中删除 yield return 并使用例如an item stack 避免创建太多迭代器。请参阅this question 接受的答案。
【解决方案2】:
public IEnumerable<HtmlNode> All()
{
    var queue = new Queue<HtmlNode>(_context);

    while (queue.Count > 0)
    {
        var node = queue.Dequeue();
        yield return node;
        foreach (var next in node.ChildNodes.Where(n => n.NodeType == HtmlNodeType.Element))
            queue.Enqueue(next);
    }
}

courtesy link

【讨论】:

    【解决方案3】:

    Children() 被破坏,它也选择兄弟姐妹的孩子。我会改写成这样的:

    public IEnumerable<HtmlNode> Traverse(IEnumerable<HtmlNode> nodes)
    {
        foreach (var node in nodes)
        {
            yield return node;
            foreach (var child in Traverse(ChildNodes(node)))
                yield return child;
        }
    }
    
    private IEnumerable<HtmlNode> ChildNodes(HtmlNode node)
    {
        return node.ChildNodes.Where(n => n.NodeType == HtmlNodeType.Element);
    }
    
    
    public SharpQuery(IEnumerable<HtmlNode> nodes, SharpQuery previous = null)
    {
        if (nodes == null) throw new ArgumentNullException("nodes");
        _previous = previous; // Is this necessary?
        _context = new List<HtmlNode>(nodes);
    }
    

    【讨论】:

    • 太棒了!我不喜欢让函数接受参数,但你给了我正确的想法……谢谢!是的,_previous 是必需的,它被其他函数使用。
    【解决方案4】:

    我会做这样的事情,看看它是否能解决问题:

    public IEnumerable<HtmlNode> Traverse()
    {
    foreach (var node in _context)
    {
        Console.WriteLine(node.Name);
        if(!node.HasChildren) {
         yield return node;
        }
        else {
        foreach (var child in Children().Traverse())
            yield return child;
        }
        }
    }
    }
    

    【讨论】:

    • 返回 c,d,e,g,h,i... 这应该是预期的。这些节点没有子节点。
    【解决方案5】:

    我不能确切地说,但你可以看到模式是,一旦你的东西遇到“c”的“/”,它就会开始认为“g,h,i”是每个后续的一部分字母直到遇到“f”的“/”。

    你很可能有一个额外的循环,你应该骑。

    或者由于某种原因,它没有正确计算“/>”部分。

    【讨论】:

    • 不是自闭节点。我也尝试扩展它们。额外的循环在哪里?这是完整的代码...
    【解决方案6】:

    这还不够吗?

    public IEnumerable<HtmlNode> Traverse()
    {
        foreach (var node in _context)
        {
            Console.WriteLine(node.Name);
            yield return node;
            foreach (var child in Children().Traverse())
                {} //yield return child;
        }
    }
    

    【讨论】:

    • 不...您正在遍历整个树,但没有返回第一级以外的任何结果。在比第一个更深的任何级别,您都在产生正确的节点,但随后不对它们做任何事情(它们没有传递给调用函数,只是传递给 depth-1)。
    • 公平地说,您曾经在 IEnumerable 中拥有 Console.WriteLine(node.Name)。现在它在外面,你是对的。
    • @Scott:嗯……那个 Console.WriteLine 只是为了调试……我把它移到外面以免混淆问题。但你是对的.. 我想在这种情况下它会产生正确的输出,但重点不是打印它,而是实际返回它;)
    猜你喜欢
    • 2014-10-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多