如何将 XPath 与没有前缀的默认命名空间一起使用?

Posted

技术标签:

【中文标题】如何将 XPath 与没有前缀的默认命名空间一起使用?【英文标题】:How do I use XPath with a default namespace with no prefix? 【发布时间】:2011-02-01 06:15:08 【问题描述】:

什么是 XPath(在 C# API 到 XDocument.XPathSelectElements(xpath, nsman) 如果重要的话)从这个文档中查询所有 MyNodes?

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <MyNode xmlns="lcmp" attr="true">
    <subnode />
  </MyNode>
</configuration>
我尝试了/configuration/MyNode,这是错误的,因为它忽略了命名空间。 我试过/configuration/lcmp:MyNode 是错误的,因为lcmp 是URI,而不是前缀。 我尝试了/configuration/lcmpMyNode,但失败了,因为Additional information: '/configuration/lcmpMyNode' has an invalid token.

编辑:我不能像一些回答者建议的那样使用mgr.AddNamespace("df", "lcmp");。这要求 XML 解析程序提前知道我计划使用的所有名称空间。由于这适用于任何源文件,因此我不知道要为哪些命名空间手动添加前缀。 my uri 似乎是 XPath 语法,但微软没有费心去实现它……真的吗?

【问题讨论】:

目前尚不清楚您想要实现的确切目标。确定您正在寻找哪些节点的标准是什么?您是否正在根据名称空间寻找元素?在这种情况下,您的代码将知道命名空间。至于 my uri 是“XPath 语法”,您认为在 XPath 1.0 规范中的什么地方定义了该语法?无论您是把命名空间 URI 放在大括号中还是将命名空间 URI 传递给 AddNamespace 方法,这对您的 C# 代码都无关紧要,在这两种情况下,命名空间 URI 都需要作为字符串提供。 @Martin:我确实想在 XPath 中指定命名空间,但我只有命名空间 URI,没有命名空间前缀。我仔细研究了我“发明” 的地方,我可能略读不正确......我从这个参考中得到它:jclark.com/xml/xmlns.htm。感谢您指出了这一点。当然,即使它无效,它似乎也是一件有用的事情,能够轻松完成.. ;) Scott,您需要选择任何您喜欢的允许前缀,使用 AddNamespace(prefix, namespaceURI) 将其与您拥有的名称空间 URI 相关联,并在您的 XPath 表达式中使用选择的前缀。这就是 XPath 的工作方式,至少是 XPath 1.0。前缀根本不必存在于输入 XML 中,也可以与输入 XML 中使用的不同,元素选择将基于命名空间匹配发生,而不是前缀。 如果您想使用 Clark 表示法,请考虑不使用 XPath,而是使用 LINQ to XML 轴方法,例如采用 XName 的 Descendants,它支持 表示法。示例:foreach (XElement myNode in doc.Descendants("lcmpMyNode"))。您当然也可以使用变量,例如 XNamespace df = "lcmp"; foreach (XElement myNode in doc.Descendants(df + "MyNode")) 【参考方案1】:

您需要按如下方式使用 XmlNamespaceManager:

   XDocument doc = XDocument.Load(@"..\..\XMLFile1.xml");
   XmlNamespaceManager mgr = new XmlNamespaceManager(new NameTable());
   mgr.AddNamespace("df", "lcmp");
   foreach (XElement myNode in doc.XPathSelectElements("configuration/df:MyNode", mgr))
   
       Console.WriteLine(myNode.Attribute("attr").Value);
   

【讨论】:

是的,我认为这可行,但我做不到。由于 XML 解析代码与实际的 XML 文件及其使用的任何命名空间无关,因此 mgr.AddNamespace("df", "lcmp");是不可能写出来的…… 但是你解析代码不能与元素名称无关,对吧?命名空间被认为是名称的一部分,因此忽略它是一种糟糕的设计,但如果您确定不会有命名空间冲突,您可以执行类似“configuration/*[local-name() = 'MyNode']”的操作 Scott,请解释一下如果命名空间 URI 未知,您的代码应该如何识别元素?您的代码到底在寻找什么,在任何命名空间中具有本地名称“MyNode”的元素?然后使用奥列格的建议。否则,请更详细地解释您正在寻找的元素。 /Oleg:XPath 应该指定命名空间,当然,就像你说的那样。但是我正在读取的 XML 没有为命名空间添加别名/前缀。 /configuration/lcmp:MyNode 不正确,因为该 XPath 中的 'lcmp' 是名称空间前缀,而不是名称空间 URI。 /configuration/lcmpMyNode 似乎是正确的语法,但 C# 似乎不支持 表示法。【参考方案2】:

这是一个示例,说明如何使命名空间可用于 XPathSelectElements 扩展方法:

using System;
using System.Xml.Linq;
using System.Xml.XPath;
using System.Xml;
namespace XPathExpt

 class Program
 
   static void Main(string[] args)
   
     XElement cfg = XElement.Parse(
       @"<configuration>
          <MyNode xmlns=""lcmp"" attr=""true"">
            <subnode />
          </MyNode>
         </configuration>");
     XmlNameTable nameTable = new NameTable();
     var nsMgr = new XmlNamespaceManager(nameTable);
     // Tell the namespace manager about the namespace
     // of interest (lcmp), and give it a prefix (pfx) that we'll
     // use to refer to it in XPath expressions. 
     // Note that the prefix choice is pretty arbitrary at 
     // this point.
     nsMgr.AddNamespace("pfx", "lcmp");
     foreach (var el in cfg.XPathSelectElements("//pfx:MyNode", nsMgr))
     
         Console.WriteLine("Found element named 0", el.Name);
     
   
 

【讨论】:

@Dan:是的,我认为这可行,但需要对任何使用过的命名空间进行硬编码。而我只能控制 XPath——请参阅我在@Martin Honnen 的回答下的评论。【参考方案3】:

XPath (故意)不是为您希望对仅存在于 XML 文档中的某些未知名称空间使用相同 XPath 表达式的情况而设计的。您应该提前知道命名空间,向 XPath 处理器声明命名空间,并在表达式中使用该名称。 Martin 和 Dan 的回答展示了如何在 C# 中执行此操作。

XML namespaces 规范中最好地表达了这种困难的原因:

我们设想可扩展标记语言 (XML) 的应用,其中单个 XML 文档可能包含为多个软件模块定义和使用的元素和属性(这里称为“标记词汇表”)。这样做的一个动机是模块化:如果存在这样一个易于理解的标记词汇表并且有可用的有用软件,那么最好重用这个标记而不是重新发明它。

此类包含多个标记词汇的文档会造成识别和冲突问题。软件模块需要能够识别它们设计用于处理的元素和属性,即使在针对某些其他软件包的标记使用相同的元素名称或属性名称时会发生“冲突”。

这些考虑要求文档结构应该有名称,以避免来自不同标记词汇的名称之间的冲突。本规范描述了一种机制,即 XML 名称空间,它通过为元素和属性分配扩展名称来实现这一点。

也就是说,命名空间应该用来确保您知道您的文档在说什么:&lt;head&gt; 元素是在谈论 Xhtml 文档的前导还是 AnatomyML 文档中的某些主体?您永远不会“假定”对名称空间不可知,这几乎是您应该在任何 XML 词汇表中定义的第一件事。

应该可以做你想做的事,但我不认为它可以在单个 XPath 表达式中完成。首先,您需要在文档中翻找并提取所有命名空间URI,然后将它们添加到命名空间管理器中,然后运行您想要的实际 XPath 表达式(并且您需要了解有关文档中命名空间分布的信息)点,或者你有很多表达式要运行)。我认为您可能最好使用 XPath 以外的其他东西(例如 DOM 或类似 SAX 的 API)来查找命名空间URI,但您也可以探索 XPath 命名空间轴(在 XPath 1.0 中),使用 namespace-uri-from-QName 函数(在XPath 2.0) 或使用像 Oleg 的 "configuration/*[local-name() = 'MyNode']" 这样的表达式。无论如何,我认为最好的办法是尽量避免编写与命名空间无关的 XPath!为什么你不提前知道你的命名空间?你将如何避免匹配你不打算匹配的东西?

编辑 - 你知道 namespaceURI 吗?

事实证明,您的问题让我们所有人都感到困惑。显然您知道名称空间 URI,但您不知道 XML 文档中使用的名称空间前缀。实际上,在这种情况下,没有使用命名空间前缀,并且 URI 成为定义它的默认命名空间。要知道的关键是所选前缀(或缺少前缀)与您的 XPath 表达式(以及一般的 XML 解析)无关。当文档表示为文本时,前缀/xmlns 属性只是将节点与命名空间 URI 相关联的一种方式。你可能想看看this answer,我在这里尝试澄清命名空间前缀。

您应该尝试以解析器的方式来考虑 XML 文档 - 每个节点都有一个命名空间 URI 和一个本地名称。命名空间前缀/继承规则只是节省了多次输入 URI。写下来的一种方法是使用 Clark 表示法:也就是说,您编写 http://www.example.com/namespace/exampleLocalNodeName,但这种表示法通常只用于文档 - XPath 对这种表示法一无所知。

相反,XPath 使用它自己的命名空间前缀。类似于/ns1:root/ns2:node。但这些与可能在原始 XML 文档中使用的任何前缀完全分开,并且没有任何关系。任何 XPath 实现都可以通过命名空间 URI 映射它自己的前缀。对于 C# 实现,您使用 XmlNamespaceManager,在 Perl 中您提供哈希,xmllint 接受命令行参数...所以您需要做的就是为您知道的命名空间 URI 创建一些任意前缀,并在XPath 表达式。使用什么前缀并不重要,在 XML 中,您只关心 URI 和 localName 的组合。

要记住的另一件事(通常令人惊讶)是 XPath 不进行命名空间继承。无论命名空间是来自继承、xmlns 属性还是命名空间前缀,您都需要为每个具有命名空间的对象添加前缀。此外,尽管您应该始终考虑 URI 和 localNames,但也有一些方法可以从 XML 文档中访问前缀。很少需要使用这些。

【讨论】:

@Andrew:我确实提前知道了命名空间,并且可以将它放在 XPath 中。我不知道命名空间前缀,当您说“/configuration/lcmp:MyNode”之类的内容时会使用它。 "/configuration/lcmpMyNode" 似乎是使用命名空间 URI 而不是前缀的正确语法,但 C# 似乎不支持 表示法。而且我没有前缀。 啊,我明白了。我会写一个新的答案 - 基本上你只需要知道你的 XML 文档中的命名空间前缀与 XPath 表达式中的命名空间前缀没有任何共同之处,除了它们都必须映射到相同的 nsURI。 非常翔实和冗长的编辑写作,但我认为它实际上并没有解决我的问题,即:什么 XPath 找到了那个节点?另外,你是说如果 XML DID 指定了一个前缀(它没有),那么 XPath 查询就不能使用它? 嗯,答案是您选择的任何 XPath 命名空间前缀。 XML 文档中声明的缺少前缀的前缀与您的问题完全无关。只有声明的命名空间 URI。您可以选择在 XPath 表达式中使用的命名空间 URI 和 XPath 前缀之间的映射。 如何在不编写 C# 代码和硬编码 XmlNamespaceManager 以了解每个可能的 URI 的情况下指定要在 XPath 表达式中使用的前缀?【参考方案4】:

configuration 元素在未命名的命名空间中,MyNode 绑定到 lcmp 命名空间,没有命名空间前缀。

XPATH 语句将允许您在没有声明 lcmp 命名空间或在 XPATH 中使用命名空间前缀的情况下寻址 MyNode 元素:

/configuration/*[namespace-uri()='lcmp' and local-name()='MyNode']

它匹配任何属于configuration 子元素的元素,然后使用具有namespace-uri()local-name() 函数的谓词过滤器将其限制为MyNode 元素。

如果您不知道元素将使用哪个命名空间 uri,那么您可以使 XPATH 更通用,只匹配 local-name()

/configuration/*[local-name()='MyNode']

但是,您冒着匹配碰巧使用相同名称的不同词汇表(绑定到不同的命名空间 uri)中的不同元素的风险。

【讨论】:

@Mads:啊,有趣,我不知道“[namespace-uri()='lcmp'” 语法...应该可以工作,如果可以(我会尝试星期一)我会将此标记为答案。你知道“/configuration/lcmpMyNode”是否真的正确并且C#根本不支持? @Scott 不,您尝试使用的语法不是有效的 XPATH 语句,并且在我知道的任何实现中都不支持。虽然它可能会扩展到该 QName,但您不能在 XPATH 语句中以这种方式处理它。 但是如果命名空间 URI 是已知的(Scott 现在是这样说的),那么值得注意的是,这种方法是不必要的脆弱,因为 Mads 指出(“你冒着在不同词汇表中匹配不同元素的风险”)。这个工作的事实并不能使它成为一个好主意(除非你真的不知道 URI)。 @Andrew:我从未改变过我的曲调。正如您在原始问题中所见,命名空间 URI 是已知的。 xmlns="lcmp" 命令给出的是命名空间 URI,而不是前缀。 @Mads 的建议是使用 local-name() AND namespace-uri(),这就是他的回答正确的原因。他继续说你可以选择不使用 namespace-uri(),但这只是事后的想法。【参考方案5】:

Xpath 2.0 + 库示例:

using Wmhelp.XPath2;

doc.XPath2SelectElements("/*:configuration/*:MyNode");

见:

XPath and XSLT 2.0 for .NET?

【讨论】:

【参考方案6】:

我喜欢@mads-hansen,他的回答非常好,以至于我编写了这些通用实用程序类成员:

    /// <summary>
    /// Gets the <see cref="XNode" /> into a <c>local-name()</c>, XPath-predicate query.
    /// </summary>
    /// <param name="childElementName">Name of the child element.</param>
    /// <returns></returns>
    public static string GetLocalNameXPathQuery(string childElementName)
    
        return GetLocalNameXPathQuery(namespacePrefixOrUri: null, childElementName: childElementName, childAttributeName: null);
    

    /// <summary>
    /// Gets the <see cref="XNode" /> into a <c>local-name()</c>, XPath-predicate query.
    /// </summary>
    /// <param name="namespacePrefixOrUri">The namespace prefix or URI.</param>
    /// <param name="childElementName">Name of the child element.</param>
    /// <returns></returns>
    public static string GetLocalNameXPathQuery(string namespacePrefixOrUri, string childElementName)
    
        return GetLocalNameXPathQuery(namespacePrefixOrUri, childElementName, childAttributeName: null);
    

    /// <summary>
    /// Gets the <see cref="XNode" /> into a <c>local-name()</c>, XPath-predicate query.
    /// </summary>
    /// <param name="namespacePrefixOrUri">The namespace prefix or URI.</param>
    /// <param name="childElementName">Name of the child element.</param>
    /// <param name="childAttributeName">Name of the child attribute.</param>
    /// <returns></returns>
    /// <remarks>
    /// This routine is useful when namespace-resolving is not desirable or available.
    /// </remarks>
    public static string GetLocalNameXPathQuery(string namespacePrefixOrUri, string childElementName, string childAttributeName)
    
        if (string.IsNullOrEmpty(childElementName)) return null;

        if (string.IsNullOrEmpty(childAttributeName))
        
            return string.IsNullOrEmpty(namespacePrefixOrUri) ?
                string.Format("./*[local-name()='0']", childElementName)
                :
                string.Format("./*[namespace-uri()='0' and local-name()='1']", namespacePrefixOrUri, childElementName);
        
        else
        
            return string.IsNullOrEmpty(namespacePrefixOrUri) ?
                string.Format("./*[local-name()='0']/@1", childElementName, childAttributeName)
                :
                string.Format("./*[namespace-uri()='0' and local-name()='1']/@2", namespacePrefixOrUri, childElementName, childAttributeName);
        
    

【讨论】:

以上是关于如何将 XPath 与没有前缀的默认命名空间一起使用?的主要内容,如果未能解决你的问题,请参考以下文章

SimpleXML 中的 XPath 用于默认命名空间,无需前缀

来自带有命名空间前缀的 xml 的 xpath 表达式

XPATHS和默认命名空间

XPATHS 和默认命名空间

为啥命名空间限定节点没有 XPath 语法?

具有显式默认命名空间的 XML 文档的 XPath 和命名空间规范