XML作为一种数据格式标准,深受某些“科技大厂”的推崇,特别是微软这个老毕登公司,几乎所有的文件格式都用的XML格式。
然而XML虽属于行业标准,但是其缺点跟微软一样——缓慢、笨重、繁琐,目前在大部分情况下,都已经被 JavaScript 的子集 JSON 给打败了。
虽然有着各种缺点,不过你还是得和XML打交道,同时也得踩中XML语法中的各种陷阱,其中之一就是命名空间(namespace)。

命名空间是XML的一种特性,当给TAG挂上一个xmlns属性后,即可为该标签及后续子标签添加指定的命名空间。
可是当你设定过命名空间后,再使用 XPath 语法查询XML文档时,若是没有指定正确的命名空间配置,则永远只会返回空的查询结果。

那么如何才能正确查询带有命名空间的XML文档呢?

以下面的一个 VSProject 文档为示例,我想获取该文档内所有带Include属性的Import标签:
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <ItemGroup>
    <Reference Include="System" />
    <Reference Include="System.Data" />
    <Reference Include="System.Deployment" />
    <Reference Include="System.Drawing" />
    <Reference Include="System.Windows.Forms" />
    <Reference Include="System.Xml" />
    <Reference Include="System.Core" />
    <Reference Include="System.Xml.Linq" />
    <Reference Include="System.Data.DataSetExtensions" />
    <Reference Include="System.Net.Http" />
    <Reference Include="netstandard" />
  </ItemGroup>
  <ItemGroup>
    <Import Include="Microsoft.VisualBasic" />
    <Import Include="System" />
    <Import Include="System.Collections" />
    <Import Include="System.Collections.Generic" />
    <Import Include="System.Data" />
    <Import Include="System.Drawing" />
    <Import Include="System.Diagnostics" />
    <Import Include="System.Windows.Forms" />
    <Import Include="System.Linq" />
    <Import Include="System.Xml.Linq" />
    <Import Include="System.Threading.Tasks" />
  </ItemGroup>
</Project>
若是一个没有命名空间的XML文档,我可以直接使用 XPath 来查询:
Dim doc As New XmlDocument()
doc.Load(xmlPath)

' 查询所有包含 Include 属性的 Import 标签
Dim ret = root.SelectNodes("/Project/ItemGroup/Import[@Include]")
MsgBox(ret.Count)
可若是文档带有命名空间(如示例的XML文档),则 SelectNodes 查询出的结果永远是空的。
此时,就需要使用 XmlNamespaceManager 对象来生成一个命名空间的配置对象,并且还需要修改查询的语法,为查询的元素设定命名空间的别名:
Dim doc As New XmlDocument()
doc.Load(xmlPath)

' 将特殊的命名空间描述文本添加到一个别名当中,这个别名可以自定义,同时你还可以添加更多其他的别名
Dim xnm As New XmlNamespaceManager(New NameTable)
xnm.AddNamespace("x", "http://schemas.microsoft.com/developer/msbuild/2003")
'xnm.AddNamespace("ns", "其他的命名空间描述文本")

' 查询所有包含 Include 属性的 Import 标签
' 查询时需要指定命名空间的别名
Dim ret = root.SelectNodes("/x:Project/x:ItemGroup/x:Import[@Include]", xnm)
MsgBox(ret.Count)
是的,你需要为每一个查询的元素添加别名,繁琐至极!但又必须添加~


那有什么办法可以让 XPath 查询忽略XML文档的命名空间吗?
其实也很简单,我们直接在读取XML文档之前,将文档的xmlns属性给重命名,让XmlDocument在读取XML文档时不读取命名空间不就行了?!
' 我们自己先读取完整的XML文档,并替换 xmlns 属性为别的名称
Dim xmlText = IO.File.ReadAllText(xmlPath, System.Text.Encoding.UTF8)
xmlText = xmlText.Replace(" xmlns=", " fuckxmlns=")

' 直接通过文本来加载XML文档
Dim doc As New XmlDocument()
doc.LoadXml(xmlText)

' 这样就可以避免命名空间的限制,直接查询文档内容了
Dim ret = doc.SelectNodes("/Project/ItemGroup/Import[@Include]")
MsgBox(ret.Count)

' 若是你需要保存修改过的XML文档内容,则需要将之前的属性改回原来的名称
' 此方法导出的XML文档无缩进
Dim xmlOut = doc.OuterXml.Replace(" fuckxmlns=", " xmlns=")

' 导出带缩进的XML文档
Dim sb As New System.Text.StringBuilder()
Dim sw As New IO.StringWriter(sb)
Dim xw As New XmlTextWriter(sw)
' 缩进方法为2个空格
xw.Formatting = Formatting.Indented
xw.Indentation = 2 '缩进文本的数量
xw.IndentChar = " " '缩进的文本,你也可以设置为Tab
doc.WriteTo(xw)
xw.Close()
sw.Close()
xmlOut = sb.ToString().Replace(" fuckxmlns=", " xmlns=")
需要注意的是:命名空间是为了让XML内的同名标签具有区别,如果你解析的XML标签非常复杂,同时标签也应用了命名空间的特性用来区隔,那么此方法可能会让你查询到意料之外的元素,所以不要用此方法来解析结构较为复杂和庞大的XML文档!
另外,此方法也没有考虑自带别名的命名空间,如 xmlns:name="xxx" 这种情况,如果XML文档带多个命名空间,只替换一个主命名空间,对于 XPath 的查询是没有优化的,你查询时还是需要带上别名才能正确搜索到元素。
最后,来聊一下 .NET 框架内的 XML 解析类,目前常用的 XML 解析类型有 XmlDocumentXPathDocumentXDocument
其中 XPathDocument 专门用作 XPath 查询类型,只能读取XML文档,无法修改内容,所以不多做介绍。
XmlDocument 则是最基础的XML读写类型,XPath 查询也只需要使用 SelectNodesSelectSingleNode 方法即可,上面的代码也有所演示。
另一个 XDocument 类型则是专门针对 Linq 语法进行优化的类型,大部分情况下不需要使用 XPath 来进行查询。
当然,若是你非常叛逆,硬是要用 XPath 来查询 XDocument 对象,也不是不行:
' 需要先引用 System.Xml.XPath,包含针对 XDocument 的 XPath 查询的扩展方法
Imports System.Xml, System.Xml.Linq, System.Xml.XPath

Dim doc = XDocument.Load(xmlPath)
Dim xnm As New XmlNamespaceManager(New NameTable)
xnm.AddNamespace("ns", "http://schemas.microsoft.com/developer/msbuild/2003")

' XPathSelectElements 为 XPath 的扩展方法,不过还是得用命名空间!!!
Dim ret = doc.XPathSelectElements("//ns:Import[@Include]", xnm)
MsgBox(ret.Count)

' XDocument 可以使用 ToString 直接输出带缩进的 XML 文档
Dim xmlOut = doc.ToString()