【问题标题】:Mysterious failure removing nodes from an XML document从 XML 文档中删除节点的神秘失败
【发布时间】:2013-07-08 13:24:14
【问题描述】:

如果有人能解释这一点,我会感到惊讶,但如果其他人能重现我正在经历的怪事,我会很有趣......

我们有一个基于处理大量表单的 InfoPath 的东西。表单数据应该符合 XSD,但 InfoPath 不断以所谓的“我的字段”的形式添加自己的元数据。我们想删除我的字段,我写了这个简单的方法:

string StripMyFields(string xml)
{
    var doc = new XmlDocument();
    doc.LoadXml(xml);

    var matches = doc.SelectNodes("//node()").Cast<XmlNode>().Where(n => n.NamespaceURI.StartsWith("http://schemas.microsoft.com/office/infopath/"));
    Dbug("Found {0} nodes to remove.", matches.Count());
    foreach (var m in matches)
        m.ParentNode.RemoveChild(m);

    return doc.OuterXml;
}

现在真正奇怪的东西来了!当我运行此代码时,它的行为与我预期的一样,删除了 InfoPath 命名空间中的所有节点。但是,如果我注释掉对 Dbug 的调用,代码就会完成,但 XML 中仍保留一个“我的字段”。

我什至注释掉了方便的 Dbug 方法的内容,它的行为仍然是一样的:

void Dbug(string s, params object[] args)
{
    //if (args.Length > 0)
    //    s = string.Format(s, args);
    //Debug.WriteLine(s);
}

输入 XML:

<?xml version="1.0" encoding="UTF-8"?>
<skjema xmlns:my="http://schemas.microsoft.com/office/infopath/2003/myXSD/2008-03-03T22:25:25" xml:lang="en-us">
    <Field-1643 orid="1643">data.</Field-1643>
    <my:myFields>
        <my:field1>Al</my:field1>
        <my:group1>
            <my:group2>
                <my:field2 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">2009-01-01</my:field2>
                <Field-1611 orid="1611">More data.</Field-1611>
                <my:field3>true</my:field3>
            </my:group2>
            <my:group2>
                <my:field2>2009-01-31</my:field2>
                <my:field3>false</my:field3>
            </my:group2>
        </my:group1>
    </my:myFields>
    <Field-1612 orid="1612">Even more data.</Field-1612>
    <my:field3>Blah blah</my:field3>
</skjema>

除非我调用 Dbug,否则不会删除“my:field3”元素(在底部,文本“Blah blah”)。

显然宇宙不应该是这样的,但我很想知道其他人是否能够复制。

我在 Win8 Enterprise 6.2.9200 上使用 VS2012 Premium (11.0.50727.1 RTMREL) 和 FW 4.5.50709。

【问题讨论】:

  • XmlDocument 不是LINQ
  • @MarcinJuraszek LINQ 在这里用于处理 XmlDocument 的节点(是的,很奇怪)
  • 正确,但我也使用 LINQ(在哪里)。由于我不知道错误的来源,因此我将其标记为两者。
  • @thedag:我格式化了你的 Xml 片段。与任何其他代码 sn-p 一样,只需将代码缩进 4 个空格即可。
  • 啊 - 我做了一个小改动,这似乎表明问题与 LINQ 有关。我更改了 Dbug 调用,因此它不会调用 Count() 扩展(只需在其中放置一个常量),然后永远不会删除最后一个字段!

标签: c# .net xml linq


【解决方案1】:

首先要做的事情。 LINQ 使用称为deferred execution 的概念。这意味着在您真正实现查询(例如通过枚举)之前,不会获取任何结果。

为什么您的节点删除问题很重要?让我们看看您的代码中发生了什么:

  1. SelectNodes 创建XPathNodeIterator,由XPathNavigator 使用,它将数据提供给XmlNodeList,由SelectNodes 返回
  2. XPathNodeIterator 基于提供的 XPath 表达式遍历 xml 文档树
  3. CastWhere 只是决定XPathNodeIterator 返回的节点是否应该参与最终结果

我们在DBug 方法调用之前到达。暂时假设它不存在。在这一点上,什么都没有实际上还没有发生。我们只得到了未实现的 LINQ 查询。

当我们开始迭代时,情况会发生变化。所有的迭代器(CastWhere 也有自己的迭代器)开始滚动。 WhereIteratorCastIterator 询问项目,然后向 XPathNodeIterator 询问最终返回第一个节点 (Field-1643)。不幸的是,这个没有通过Where 测试,所以我们要求下一个。 my:myFields 更幸运,这是一场比赛 - 我们将其删除。

我们快速转到my:field1(同样,WhereIteratorCastIteratorXPathNodeIterator),它也被删除了。停在这里片刻。删除 my:field1 会将其与其父级分离,从而将其 (my:field1) 同级设置为 null(在删除节点之前/之后没有其他节点)。

目前情况如何? XPathNodeIterator 知道它的当前元素是 my:field1 节点,它刚刚被删除。在 与父级分离 中删除,但迭代器仍保留引用。听起来不错,让我们请求下一个节点。 XPathNodeIterator 做什么?检查它的Current 项目,并要求NextSibling(因为它没有孩子先走)-这是null,因为我们刚刚执行了分离。这意味着迭代已经结束。任务完成。

因此,通过在迭代期间更改集合结构,您只从文档中删除了两个节点(而实际上只有一个,因为第二个删除的节点是已删除节点的子节点)。

使用更简单的 XML 可以观察到相同的行为:

<Root>
    <James>Bond</James>
    <Jason>Bourne</Jason>
    <Jimmy>Keen</Jimmy>
    <Tom />
    <Bob />
</Root>

假设我们想去掉以J 开头的节点,导致文档只包含诚实的人名:

var doc = new XmlDocument();
doc.LoadXml(xml);

var matches = doc
    .SelectNodes("//node()")
    .Cast<XmlNode>()
    .Where(n => n.Name.StartsWith("J"));

foreach (var node in matches)
{
    node.ParentNode.RemoveChild(node);
}

Console.WriteLine(doc.InnerXml);

不幸的是,JasonJimmy 仍然存在。 James 的下一个兄弟姐妹(由迭代器返回的那个)原本应该是 Jason,但是一旦我们将 James 从树中分离出来没有兄弟姐妹,迭代结束。

现在,为什么它适用于 DBugCount 调用实现查询。迭代器已经运行,当我们开始循环时,我们可以访问我们需要的所有节点。在 Where 之后调用 ToList 或者如果您在调试期间检查结果(VS 甚至通知您检查结果将枚举集合),也会发生同样的事情。

【讨论】:

  • 我对这个解释的唯一问题是 SelectNodes 不仅仅返回一个迭代器 - 它返回 XmlNodeList。现在我认为该列表必须仅使用迭代器进行初始化。我知道 LINQ 语句返回表达式树,但我认为节点列表已经完全由 SelectNodes 构建,然后我当然不会想到我在这里看到的行为。
  • @TheDag:这是正确的,这就是我写的。返回的XmlNodeList 在内部使用迭代器。当您考虑它时,您可以在枚举期间更改其结构时破坏任何可修改的集合。有些表现得更好(例如,List&lt;T&gt; 会抛出异常),有些则更糟。
【解决方案2】:

我认为这归结于薛定谔的猫问题,即在您查看或采取行动之前,Where 不会实际编译查询结果。意思是,在您调用 Count() (或任何其他用于获取结果的函数)或在调试器中查看它之前,结果不存在。作为测试,试着这样写:

if (matches.Any())
    foreach (var m in matches)
        m.ParentNode.RemoveChild(m);

【讨论】:

  • 我认为你在做某事。在我们开始修改文档之前导致集合被枚举的任何事情(例如使用 ToList() 或调用 Count())都会导致它按预期运行。如果谓词实际上是在选择节点,我认为这可能是有道理的,但这里的谓词只是说一个节点是否在特定的命名空间中,我不太明白发生了什么......
【解决方案3】:

很奇怪,只有当您在调试时实际查看结果时,它才会删除最后一个节点。顺便说一句,将结果转换为 List 然后循环遍历它也可以。

List<XmlNode> matches = doc.SelectNodes("//node()").Cast<XmlNode>().Where(n =>   n.NamespaceURI.StartsWith("http://schemas.microsoft.com/office/infopath/")).ToList();
        foreach (var m in matches)
        {
            m.ParentNode.RemoveChild(m);
        }

【讨论】:

  • ToList() 导致集合被枚举,这一定是发生了什么的关键。在原始代码中,我们在(隐式)调用 Enumerator.MoveNext() 之间修改了文档,但是使用 ToList() 我们强制枚举整个集合而不修改文档。但是,我仍然不明白发生了什么,因为我们从未修改正在枚举的集合。在我看来,我们修改文档的事实应该是无关紧要的。
  • 我怀疑这与两个 my:field3 元素有关。在枚举之前,它可能对两个元素都持有一个引用。但是当我们强制枚举时,它实际上会创建对这两个元素的单独引用。将尝试将最后一个 my:field3 元素更改为 my:field4 并针对您的原始代码重新运行,以查看结果。
【解决方案4】:

jimmy_keen 的解决方案对我有用。我只有一个简单的

//d is an XmlDocument
XmlNodeList t = d.SelectNodes(xpath);
foreach (XmlNode x in t)
{
    x.ParentNode.RemoveChild(x);
}
d.Save(outputpath);

这只会删除 3 个节点,而在调试模式下单步执行会删除 1000 多个节点。

在 foreach 解决问题之前添加一个 Count:

var count = t.Count;

【讨论】:

    猜你喜欢
    • 2016-11-05
    • 2020-01-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多