对XML文档进行条件查询时,XPath表达式可以精准描述节点路径和约束条件。C#标准库提供两种主要方式执行XPath:基于XmlDocument的SelectSingleNode和SelectNodes方法,以及基于XPathDocument的XPathNavigator只读游标。前者会把整个文档加载到内存并构建DOM树,适合结构修改和中小型文件;后者按只读方式遍历,内存峰值更低。理解二者的执行模型,能帮助你在不同数据规模下选择合适的API。

C#中执行XPath的两种入口
XmlDocument属于System.Xml命名空间,它提供SelectSingleNode和SelectNodes两个核心方法。SelectSingleNode返回匹配表达式的第一个节点,没有匹配时返回null;SelectNodes返回XmlNodeList集合。两个方法都可以不带命名空间管理器直接使用,只要XML中没有默认命名空间或带前缀的节点。
另一种入口是XPathDocument与XPathNavigator。XPathDocument专为XPath查询设计,内部采用只读模型,创建速度比XmlDocument更快,占用内存更低。通过CreateNavigator方法拿到XPathNavigator后,可以使用Compile将表达式编译为XPathExpression,再执行Select方法。如果同一个表达式需要反复执行,编译后复用会减少解析开销。
下面的示例演示XmlDocument加载本地XML并读取book节点。代码中使用了绝对路径/catalog/book/title,这要求根元素必须是catalog。
<catalog>
<book id="bk101" category="programming">
<title>C# Guide</title>
<author>Zhang</author>
<price>59.00</price>
</book>
<book id="bk102" category="database">
<title>SQL Cookbook</title>
<author>Li</author>
<price>45.00</price>
</book>
</catalog>
using System;
using System.Xml;
class Program
{
static void Main()
{
XmlDocument doc = new XmlDocument();
doc.Load(@"C:\temp\books.xml");
XmlNode titleNode = doc.SelectSingleNode("/catalog/book/title");
Console.WriteLine(titleNode?.InnerText);
XmlNodeList bookList = doc.SelectNodes("//book");
foreach (XmlNode book in bookList)
{
Console.WriteLine(book.Attributes["id"].Value);
}
}
}
运行后会先输出第一个title文本,再遍历所有book节点并打印id属性。这里//book中的双斜杠表示从文档任意位置搜索,不依赖层级。
常用XPath筛选表达式与函数
基础路径能够定位节点,但真正的数据筛选依赖谓词和函数。谓词写在方括号中,用来对节点集合做条件过滤。按属性筛选的写法是//book[@category='database'],它返回category属性等于database的book节点。如果需要数值比较,可以使用XPath的比较运算符,例如//book[price > 40]。要注意在C#字符串中传入时,大于号在代码里需要转义为>,否则会破坏XPath字符串的XML语义。
多条件组合通过and和or连接,例如//book[@category='database' and price > 40]。字符串匹配可以使用contains函数,例如//book[contains(title,'C#')]表示标题包含C#的book。starts-with函数用于前缀匹配,last和position则用于序号筛选,例如//book[position()=1]取第一个book。
下面代码演示属性筛选、数值比较和字符串函数。XmlNodeList返回后可以直接foreach遍历,节点属性仍通过Attributes访问。
XmlDocument doc = new XmlDocument();
doc.Load(@"C:\temp\books.xml");
XmlNodeList databaseBooks = doc.SelectNodes("//book[@category='database']");
Console.WriteLine("database分类数量: " + databaseBooks.Count);
XmlNodeList priceBooks = doc.SelectNodes("//book[price > 40]");
foreach (XmlNode book in priceBooks)
{
Console.WriteLine(book.SelectSingleNode("title").InnerText);
}
XmlNodeList containsBooks = doc.SelectNodes("//book[contains(title,'C#')]");
foreach (XmlNode book in containsBooks)
{
Console.WriteLine("包含C#: " + book.Attributes["id"].Value);
}
还有一点需要区分:SelectSingleNode在匹配不到时返回null,如果直接调用InnerText会抛出NullReferenceException。因此生产代码中应当先判空,或者使用空值传播运算符。
命名空间查询为什么需要XmlNamespaceManager
如果XML文档带有默认命名空间,例如根元素声明xmlns="http://ipipp.com/books",那么所有没有前缀的元素都属于该命名空间。此时直接写//book往往返回空集合,因为XPath会认为book在空命名空间下,与实际节点所属命名空间不一致。
解决办法是创建XmlNamespaceManager,把前缀映射到命名空间URI,然后在XPath表达式中使用该前缀。注意这里的映射前缀不必与XML原文中的前缀相同,只要URI一致即可。比如XML里没有前缀,查询时仍然可以注册前缀bk指向同一个URI,然后使用//bk:book。
<catalog xmlns="http://ipipp.com/books">
<book id="bk101">
<title>C# Guide</title>
</book>
</catalog>
XmlDocument doc = new XmlDocument();
doc.Load(@"C:\temp\books_ns.xml");
XmlNamespaceManager nsmgr = new XmlNamespaceManager(doc.NameTable);
nsmgr.AddNamespace("bk", "http://ipipp.com/books");
XmlNode node = doc.SelectSingleNode("//bk:book/bk:title", nsmgr);
Console.WriteLine(node?.InnerText);
如果XML中带有多个命名空间,可以多次调用AddNamespace。XmlNamespaceManager依赖NameTable保证字符串比较性能,因此必须使用doc.NameTable或XPathDocument对应的NameTable初始化。忽略这一点也可能导致选择失败。
XPathNavigator与XmlDocument的区别及选择建议
XmlDocument是可变DOM,适合需要在筛选后修改节点内容、删除节点或新增属性的场景。缺点是加载完整DOM会占用较多内存,如果XML文件很大,构建和查询都比较慢。XPathDocument与XPathNavigator则采用只读游标模式,遍历过程不构建可变对象,内存占用明显更低。
如果需要多次执行不同XPath,可以选择XPathNavigator的Compile方法。编译后的XPathExpression对象可以重复使用,避免每次Select都重新解析表达式。XPathNodeIterator提供MoveNext遍历结果,用Current属性获取当前节点,再通过SelectSingleNode继续向下查询。
XPathDocument xpathDoc = new XPathDocument(@"C:\temp\books.xml");
XPathNavigator nav = xpathDoc.CreateNavigator();
XPathExpression expr = nav.Compile("//book[@category='programming']");
XPathNodeIterator iterator = nav.Select(expr);
while (iterator.MoveNext())
{
XPathNavigator current = iterator.Current;
Console.WriteLine(current.SelectSingleNode("title").Value);
}
综合来看,小规模XML并且需要修改数据时使用XmlDocument最直接;只读查询和大文件优先XPathDocument。XPath本身语法与C#版本无关,掌握谓词、函数和命名空间映射后,可以将同一套表达式迁移到其他支持XPath的语言中。