为什么需要 XmlNamespaceManager?

我对 为什么没什么研究,至少在。Net Framework ——在执行 XPath 查询时,需要使用 XmlNamespaceManager来处理名称空间(或者相当笨重和冗长的 [local-name()=... XPath 谓词/函数/其他)。我理解为什么名称空间是必要的或者至少是有益的,但是 为什么是否如此复杂?

为了查询一个简单的 XML 文档(没有名称空间) ..。

<?xml version="1.0" encoding="ISO-8859-1"?>
<rootNode>
<nodeName>Some Text Here</nodeName>
</rootNode>

人们可以使用类似 doc.SelectSingleNode("//nodeName")的东西(这将匹配 <nodeName>Some Text Here</nodeName>)

神秘 # 1 : 我的第一个烦恼——如果我理解正确的话——仅仅是添加对父/根标记的名称空间引用(不管是否用作子节点标记的一部分) ,就像这样:

<?xml version="1.0" encoding="ISO-8859-1"?>
<rootNode xmlns="http://example.com/xmlns/foo">
<nodeName>Some Text Here</nodeName>
</rootNode>

... 需要额外的几行代码才能得到相同的结果:

Dim nsmgr As New XmlNamespaceManager(doc.NameTable)
nsmgr.AddNamespace("ab", "http://example.com/xmlns/foo")
Dim desiredNode As XmlNode = doc.SelectSingleNode("//ab:nodeName", nsmgr)

... 实际上是想出一个不存在的前缀(“ ab”)来查找一个甚至不使用前缀的节点。doc.SelectSingleNode("//nodeName")(概念上)有什么问题?

神秘 # 2 : 假设您有一个使用前缀的 XML 文档:

<?xml version="1.0" encoding="ISO-8859-1"?>
<rootNode xmlns:cde="http://example.com/xmlns/foo" xmlns:feg="http://example.com/xmlns/bar">
<cde:nodeName>Some Text Here</cde:nodeName>
<feg:nodeName>Some Other Value</feg:nodeName>
<feg:otherName>Yet Another Value</feg:otherName>
</rootNode>

... 如果我理解正确的话,您必须将这两个名称空间都添加到 XmlNamespaceManager中,以便对单个节点执行查询..。

Dim nsmgr As New XmlNamespaceManager(doc.NameTable)
nsmgr.AddNamespace("cde", "http://example.com/xmlns/foo")
nsmgr.AddNamespace("feg", "http://example.com/xmlns/bar")
Dim desiredNode As XmlNode = doc.SelectSingleNode("//feg:nodeName", nsmgr)

... ... 在这种情况下,为什么我需要(概念上)名称空间管理器?

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

编辑补充: 我修改和完善的问题是基于 XmlNamespaceManager 在大多数情况下的明显冗余,以及使用名称空间管理器指定前缀到 URI 的映射:

当命名空间前缀(“ cde”)到命名空间 URI (“ http://example.com/xmlns/foo”)的直接映射在源文档中明确说明时:

...<rootNode xmlns:cde="http://example.com/xmlns/foo"...

程序员在进行查询之前重新创建映射的概念需求是什么?

33327 次浏览

我回答第一点:

为 XML 文档设置默认名称空间仍然意味着节点,即使没有名称空间前缀,比如:

<rootNode xmlns="http://someplace.org">
<nodeName>Some Text Here</nodeName>
</rootNode>

不再在“空”名称空间中。您仍然需要一些方法来使用 XPath 引用这些节点,所以您需要创建一个前缀来引用它们,即使它是“编造的”。

回答第二点:

<rootNode xmlns:cde="http://someplace.org" xmlns:feg="http://otherplace.net">
<cde:nodeName>Some Text Here</cde:nodeName>
<feg:nodeName>Some Other Value</feg:nodeName>
<feg:otherName>Yet Another Value</feg:otherName>
</rootNode>

在实例文档内部,驻留在一个名称空间中的节点存储为它们的节点名和它们的长名称空间名,它被称为(用 W3C 的说法) 扩展名称

例如,<cde:nodeName>实际上存储为 <http://someplace.org:nodeName>。名称空间前缀对于人类来说是一种任意的便利,因此当我们键入 XML 或者必须读取它时,我们不必这样做:

<rootNode>
<http://someplace.org:nodeName>Some Text Here</http://someplace.org:nodeName>
<http://otherplace.net:nodeName>Some Other Value</http://otherplace.net:nodeName>
<http://otherplace.net:otherName>Yet Another Value</http://otherplace.net:otherName>
</rootNode>

当搜索 XML 文档时,它不是通过友好的前缀搜索,而是通过名称空间 URI 进行搜索,因此必须通过使用 XmlNamespaceManager传递的名称空间表告诉 XPath 有关名称空间的信息。

您需要将 URI/前缀对注册到 XmlNamespaceManager 实例,以便让 SelectSingleNode ()知道您正在引用的 哪个特定“ nodeName”节点——来自“ http://someplace.org”的节点或来自“ http://otherplace.net”的节点。

请注意,在执行 XPath 查询时,具体的前缀名称并不重要:

Dim nsmgr As New XmlNamespaceManager(doc.NameTable)
nsmgr.AddNamespace("any", "http://someplace.org")
nsmgr.AddNamespace("thing", "http://otherplace.net")
Dim desiredNode As XmlNode = doc.SelectSingleNode("//thing:nodeName", nsmgr)

SelectSingleNode ()只需要 XPath 表达式的前缀与名称空间 URI 之间的连接。

据我所知,如果您有这样的文档,那么没有充分的理由需要手动定义一个 XmlNamespaceManager来获取 abc前缀的节点:

<itemContainer xmlns:abc="http://abc.com" xmlns:def="http://def.com">
<abc:nodeA>...</abc:nodeA>
<def:nodeB>...</def:nodeB>
<abc:nodeC>...</abc:nodeC>
</itemContainer>

微软根本懒得编写一些东西来检测 xmlns:abc已经在父节点中指定了。我可能是错的,如果是这样,我欢迎对这个答案的评论,以便我可以更新它。

然而,这篇博文似乎证实了我的怀疑。它基本上说,您需要手动定义一个 XmlNamespaceManager并手动遍历 xmlns:属性,将每个属性添加到名称空间管理器。不知道为什么微软不能自动做到这一点。

下面是我根据那篇博客文章创建的一个方法,它可以根据源 XmlDocumentxmlns:属性自动生成 XmlNamespaceManager:

/// <summary>
/// Creates an XmlNamespaceManager based on a source XmlDocument's name table, and prepopulates its namespaces with any 'xmlns:' attributes of the root node.
/// </summary>
/// <param name="sourceDocument">The source XML document to create the XmlNamespaceManager for.</param>
/// <returns>The created XmlNamespaceManager.</returns>
private XmlNamespaceManager createNsMgrForDocument(XmlDocument sourceDocument)
{
XmlNamespaceManager nsMgr = new XmlNamespaceManager(sourceDocument.NameTable);


foreach (XmlAttribute attr in sourceDocument.SelectSingleNode("/*").Attributes)
{
if (attr.Prefix == "xmlns")
{
nsMgr.AddNamespace(attr.LocalName, attr.Value);
}
}


return nsMgr;
}

我是这样使用它的:

XPathNavigator xNav = xmlDoc.CreateNavigator();
XPathNodeIterator xIter = xNav.Select("//abc:NodeC", createNsMgrForDocument(xmlDoc));

原因很简单。XPath 查询中使用的前缀与 xml 文档中声明的前缀之间没有必要的连接。举个例子,下面的 xml 在语义上是等价的:

<aaa:root xmlns:aaa="http://someplace.org">
<aaa:element>text</aaa:element>
</aaa:root>

  <bbb:root xmlns:bbb="http://someplace.org">
<bbb:element>text</bbb:element>
</bbb:root>

ccc:root/ccc:element”查询将匹配这两个实例,前提是名称空间管理器中存在这两个实例的映射。

nsmgr.AddNamespace("ccc", "http://someplace.org")

那个。NET 实现不关心 xml 中使用的文字前缀,只关心为查询文字定义的前缀,以及名称空间值与 doc 的实际值匹配。这需要具有常量查询表达式,即使前缀在使用的文档之间有所不同,并且这是一般情况下的正确实现。

基本要点(如 Kev 上面所指出的)是,名称空间 URI 是名称空间的重要组成部分,而不是名称空间前缀,前缀是一种“任意的便利”

至于为什么需要名称空间管理器,而不是使用文档来解决这个问题,我可以想到两个原因。

理由1

如果像您的示例那样,只允许向 document entElement 添加名称空间声明,那么 selectSingleNode 只使用已定义的任何内容都是微不足道的。

但是,可以在文档中的任何元素上定义命名空间前缀,并且命名空间前缀不唯一绑定到文档中的任何给定命名空间。考虑下面的例子

<w xmlns:a="mynamespace">
<a:x>
<y xmlns:a="myOthernamespace">
<z xmlns="mynamespace">
<b:z xmlns:b="mynamespace">
<z xmlns="myOthernamespace">
<b:z xmlns:b="myOthernamespace">
</y>
</a:x>
</w>

在这个例子中,您希望 //z//a:z//b:z返回什么?如果没有某种外部名称空间管理器,您将如何表示它?

理由2

它允许您对任何等价的文档重用相同的 XPath 表达式,而无需了解正在使用的名称空间前缀。

myXPathExpression = "//z:y"
doc1.selectSingleNode(myXPathExpression);
doc2.selectSingleNode(myXPathExpression);

文件1:

<x>
<z:y xmlns:z="mynamespace" />
</x>

文件2:

<x xmlns"mynamespace">
<y>
</x>

为了在没有名称空间管理器的情况下实现后一个目标,您必须检查每个文档,为每个文档构建定制的 XPath 表达式。

这个线程帮助我更清楚地理解了名称空间的问题。谢谢。当我看到 Jez 的密码的时候,我尝试了它,因为它看起来比我编写的解决方案更好。不过我发现了它的一些缺点。如前所述,它只查看根节点(但是名称空间可以在任何地方列出),并且它不处理默认名称空间。我试图通过修改他的代码来解决这些问题,但没有用。

下面是这个函数的版本。它使用正则表达式查找整个文件中的名称空间映射; 使用默认名称空间,给它们任意前缀“ ns”; 并处理同一名称空间的多个匹配项。

private XmlNamespaceManager CreateNamespaceManagerForDocument(XmlDocument document)
{
var nsMgr = new XmlNamespaceManager(document.NameTable);


// Find and remember each xmlns attribute, assigning the 'ns' prefix to default namespaces.
var nameSpaces = new Dictionary<string, string>();
foreach (Match match in new Regex(@"xmlns:?(.*?)=([\x22\x27])(.+?)\2").Matches(document.OuterXml))
nameSpaces[match.Groups[1].Value + ":" + match.Groups[3].Value] = match.Groups[1].Value == "" ? "ns" : match.Groups[1].Value;


// Go through the dictionary, and number non-unique prefixes before adding them to the namespace manager.
var prefixCounts = new Dictionary<string, int>();
foreach (var namespaceItem in nameSpaces)
{
var prefix = namespaceItem.Value;
var namespaceURI = namespaceItem.Key.Split(':')[1];
if (prefixCounts.ContainsKey(prefix))
prefixCounts[prefix]++;
else
prefixCounts[prefix] = 0;
nsMgr.AddNamespace(prefix + prefixCounts[prefix].ToString("#;;"), namespaceURI);
}
return nsMgr;
}