为什么XmlNamespaceManager是必需的?


70

我干了为什么-至少在.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"...

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


1
作为一个简短的附录,我承认在某些情况下,诸如XMLNamespaceManager之类的东西会使事情变得更容易,但是我相信在上述情况下,它会使事情变得比原来困难得多。
赛马会

1
我主要的困惑是,为什么需要在XML文档和实现XPath Query的代码中同时指定前缀与名称空间的关系。如果根节点已经包含映射,为什么在加载文档时为什么我必须对已解析的硬代码信息进行本质上的处理?另外,如果将来在文档中添加了第三个名称空间,我是否不必更改和重新编译代码来声明该第三个关系?
赛马会

1
从上面删除:仅将名称空间前缀放在XPath查询中doc.SelectSingleNode("//feg:nodeName")并完成该操作有什么问题?对于人类的大脑,该代码片段的含义是什么?[PARAGRAPH]换句话说,通过额外的代码行以及无法明确源自源XML文档和/或XPath查询的XmlNamespaceManager的实例化,真正增加了对情况的理解?
赛马会

1
从上面重写,继续当然,对于大多数使用XML和XPath的XML文档和情况,至少可以想象,即使不是很实际,也可以简单地从文档和查询中获取名称空间信息,而不需要事先了解名称空间,或手动解析文档以确定AddNamespace()?的参数 我忍不住想想我一定想念一些明显的东西,如果我愿意,请赐教!
赛马会

1
为此问题+1。我现在有同样的想法。我的根节点有很多xmlns:abc="..." xmlns:def="..."属性。为什么XPathNodeIterator到底为什么不能弄清楚什么样的命名空间与子节点相关联(如<abc:SomeNode/>没有XmlNamespaceManager?)?
耶兹(Jez)

Answers:


20

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

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

原因1

如果只允许将名称空间声明添加到documentElement,如您的示例中所示,那么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);

doc1:

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

doc2:

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

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


1
尽管“原因1”下的样本似乎是有效的AFAIK,但我想知道在现实世界中有多少个案例像这样疯狂地复杂。当然,使用单字母名称空间和节点名会在某种程度上限制了可能性的数量,尽管我已经看到了一些2、3和4字母缩写作为名称空间前缀的实际例子,但我还没有看到1-理论和示例之外的字母前缀。基本上,我发现自己真正在寻找如何使用名称空间管理器来表达任何这些内容。
骑师

要回答原因1的问题,这取决于我要查找或过滤掉的数据-使用如此复杂且同时无意义的节点名称和关系很难做到这一点。但是,到目前为止,原因1提供了最深入,最清晰的答案...至于原因2,由于源使用名称空间,因此我不确定所提供的代码是否可以执行,但您未提供命名空间管理器-am我错了吗?
赛马会

在我的两个示例中,我都要求您考虑没有命名空间管理器的生活。据我所知,如果没有求助于名称空间管理器,我在原因1中提出的问题是无法回答的。我不是在问如何提取任何特定的节点,而是在问您希望这些表达式返回哪个节点。
Paul Butcher,

没错-原因2中的代码将需要一个名称空间管理器。我特意省略了名称空间管理器,因为(根据我的理解)您的问题的重点是您认为没有它,我们可以生存–这表明我们无法做到这一点。
Paul Butcher,

1
对你的问题的最终答案原因1//z应匹配<z xmlns="mynamespace"><z xmlns="myOthernamespace">//a:z将返回一个空集,并//b:z会匹配<b:z xmlns:b="mynamespace"><b:z xmlns:b="myOthernamespace">-这背后的逻辑是,没有命名空间管理规定,并没有“试图从文档中获取信息本身”命令,因此将名称空间与其他任何属性一样对待,并且在我的脑海中:成为另一个有效的字符-,如果您知道自己的数据,或者不在乎,查询节点就不会那么痛苦
Code Jockey

14

原因很简单。在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中使用的文字前缀,仅关心为查询文字定义了前缀并且名称空间值与文档的实际值匹配。即使前缀在使用的文档之间有所不同,也必须具有恒定的查询表达式,这是一般情况下的正确实现。


12

据我所知,如果有这样的文档,那么没有充分的理由应该手动定义一个XmlNamespaceManager以到达abc带有-prefixed节点的节点:

<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:属性,然后将每个属性添加到名称空间管理器中。Dunno为什么Microsoft无法自动执行此操作。

这是我根据该博客帖子创建的一种方法,用于XmlNamespaceManager根据xmlns:来源的属性自动生成XmlDocument

/// <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));

困扰,直到所有时间都回到这个问题-不只是微软-我相信它在XML或XPATH规范中-而且在我使用过的其他非MS语言中也以类似的方式发生-不确定是否有但这提取物命名空间给你,但后来怎样一个指定范围(因为名字空间可以在任何范围内指定)... idunno -我喜欢文字模式,在那里:成为类似的文字字符的数字,字母或-和因此prfx:NodeName,就像prfxNodeName还是prfx-NodeName-一个简单的标识符...虽然不符合标准...叹气
Code Jockey

4

我回答第一点:

为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完成搜索,因此您必须通过使用传入的名称空间表来告知XPath有关名称空间的信息。 XmlNamespaceManager


尽管我没有概念上的理由要求仅在使用一个名称空间时就要求某人承认一个非“空”名称空间,但是为什么有必要使一个函数需要一个比标志更多的内容,例如doc.SelectSingleNode("//nodeName", NamespaceFlags.UseDocumentNamespace)
Code Jockey

-也就是说,为什么要求程序员实例化一个单独的对象,对文档中使用的名称空间有所了解(或解析和确定代码),然后指定一个完全随机且人为的名称空间前缀以插入XPath查询中?请原谅我的语气-我简直感到困惑。
赛马会

@code-这是因为在更复杂的文档(例如RSS feed)中,经常有多个名称空间在起作用。仅具有特殊标志来处理该特定条件(按照您的示例,文档仅位于默认名称空间中)是一种糟糕的设计选择,并给框架代码增加了额外的复杂性。那么,为什么不覆盖所有基础并要求代码的使用者传递一个XmlNamespaceManager替代代码呢?
凯夫

我相信,您提供的示例(RSS)与原始问题(多个名称空间)中的我的奥秘#2有关。XPath Query和RSS文档本身包含查询节点所需的所有信息。我可以想象的唯一情况XmlNamespaceManager是,存在多个使用相同前缀(使用xmlns:place或相似,但在文档范围不同)的名称空间(“ someplace.org”和“ otherplace.net” )。否则,文档和查询将提供产生所需结果所需的所有信息。
赛马会

1
感谢您的耐心配合,但这似乎仍无法回答我的问题。为什么除了//feg:nodeName查找特定节点外,还需要使用更多的东西?在内部进行转换应该比较简单feg...http://otherplace.net无需我明确声明该关系-它就在根节点中!(xmlns:feg="http://otherplace.net")。至少,我认为应该有一个辅助功能,例如XmlNamespaceManager.GetNSFromDocument(xdoc)...如果答案仅仅是他们还没有为您做这项工作,那就好!是这样吗
赛马会

3

您需要将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之间的连接。


3

该线程帮助我更清楚地了解了名称空间的问题。谢谢。当我看到杰兹的代码时,我尝试了一下,因为它看起来比我编写更好。我发现了它的一些缺点。如所写,它仅在根节点中查找(但可以在任何地方列出名称空间),并且不处理默认名称空间。我试图通过修改他的代码来解决这些问题,但无济于事。

这是该功能的我的版本。它使用正则表达式在整个文件中查找名称空间映射。与默认名称空间配合使用,为其赋予任意前缀“ 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;
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.