使用XmlNode.SelectNodes查询文本节点时仅返回首个匹配项的问题
XPath text() 查询移除节点后的异常原因分析
问题描述
原始XML结构:<label>Matrise <foo/><foo/>GM:1<foo/></label>
- 初始状态:通过
labelNode.SelectNodes("text()")可正常获取两个文本节点:Matrise和GM:1 - 修改后状态:移除所有
<foo>节点后,labelNode.ChildNodes仍显示存在两个独立的文本节点,但text()查询仅返回第一个节点,出现结果矛盾。
复现代码
using System.Xml; using System.Linq; var xml = "<label>Matrise <foo/><foo/>GM:1<foo/></label>"; var doc = new XmlDocument(); doc.LoadXml(xml); var labelNode = doc.SelectSingleNode("label"); Console.WriteLine("Before modifications:"); PrintTextNodesInOriginal(labelNode); Console.WriteLine("\r\nAfter modifications:"); PrintTextNodesInModified(labelNode); void PrintTextNodesInOriginal(XmlNode labelNode) { PrintTextNodes(labelNode); } void PrintTextNodesInModified(XmlNode labelNode) { // Remove foo nodes var foos = labelNode .SelectNodes("foo") .Cast<XmlNode>() .ToList(); foreach (var foo in foos) { foo.ParentNode.RemoveChild(foo); } PrintTextNodes(labelNode); } List<XmlNode> GetTextNodes(XmlNode labelNode) { return labelNode.SelectNodes("text()") .Cast<XmlNode>() .ToList(); } void PrintTextNodes(XmlNode labelNode) { var textNodes = GetTextNodes(labelNode); var textNodesActual = labelNode.ChildNodes .Cast<XmlNode>() .Where(node => node.NodeType == XmlNodeType.Text) .ToList(); Console.WriteLine($"Nodes count using query: {textNodes.Count}"); Console.WriteLine($"Nodes count actual: {textNodesActual.Count}"); Console.WriteLine("Nodes' contents:"); foreach (var textNode in textNodes) { Console.WriteLine(textNode.InnerText); } }
预期结果
Before modifications: Nodes count using query: 2 Nodes count actual: 2 Nodes' contents: Matrise GM:1 After modifications: Nodes count using query: 2 Nodes count actual: 2 Nodes' contents: Matrise GM:1
实际结果
Before modifications: Nodes count using query: 2 Nodes count actual: 2 Nodes' contents: Matrise GM:1 After modifications: Nodes count using query: 1 Nodes count actual: 2 Nodes' contents: Matrise
原因解析
这个矛盾源于XmlDocument的DOM节点模型与XPath查询的规范差异:
- DOM节点模型的特性:XmlDocument在移除节点后,不会自动合并相邻的文本节点。因此移除
<foo>后,原来被分隔的两个文本节点变成相邻的兄弟节点,但物理上仍然是两个独立的节点,这就是ChildNodes能检测到两个节点的原因。 - XPath查询的规范遵循:XPath 1.0的
text()函数查询时,严格遵循XML的「字符数据归一化」规则——相邻的文本节点在逻辑上会被视为一个合并后的节点。因此即使DOM中存在两个物理文本节点,XPath查询只会返回第一个,其余相邻的会被忽略。
解决方法
如果需要让XPath查询与DOM节点的实际状态一致,可在移除节点后手动调用XmlNode.Normalize()方法,该方法会将所有相邻的文本节点合并为单个节点:
void PrintTextNodesInModified(XmlNode labelNode) { // Remove foo nodes var foos = labelNode .SelectNodes("foo") .Cast<XmlNode>() .ToList(); foreach (var foo in foos) { foo.ParentNode.RemoveChild(foo); } // 手动合并相邻文本节点 labelNode.Normalize(); PrintTextNodes(labelNode); }
内容的提问来源于stack exchange,提问作者Jānis
相关产品推荐
相关产品推荐

