我如何在没有前缀的默认命名空间中使用XPath?

36

在此文档中,使用XPath查询所有MyNodes的方法是什么(如果有必要,在C# API到XDocument.XPathSelectElements(xpath, nsman))?

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

编辑:我不能像一些回答者建议的那样使用 mgr.AddNamespace("df", "lcmp");。这要求XML解析程序预先知道我计划使用的所有命名空间。由于这是应用于任何源文件的,我不知道哪些命名空间需要手动添加前缀。看起来{my uri}是XPath语法,但是微软没有实现它...这是真的吗?


不清楚您想要实现什么。确定您要查找哪些节点的标准是什么?您是否要根据命名空间查找元素?在这种情况下,您的代码将了解命名空间。 至于 {my uri} 是“XPath 语法”,那么您认为 XPath 1.0 规范的哪个部分定义了该语法?无论您是将命名空间 URI 放在花括号中还是将命名空间 URI 传递给 AddNamespace 方法,在 C# 代码中都不应该有差别,因为在两种情况下,命名空间 URI 都需要作为字符串可用。 - Martin Honnen
@Martin:我确实想在XPath中指定命名空间,但我只有命名空间URI而没有命名空间前缀。我仔细研究了我从哪里“发明”{}的地方,可能是我的粗略阅读......我从这个参考文献中得到的:http://www.jclark.com/xml/xmlns.htm。谢谢你指出这一点。当然,即使它不合法,似乎也是一件很容易做到的有用的事情.. ;) - Scott Stafford
Scott,你需要选择任何你喜欢的允许前缀,使用AddNamespace(prefix,namespaceURI)将其与命名空间URI关联起来,并在XPath表达式中使用所选前缀。这就是XPath的工作方式,至少是XPath 1.0。前缀不必存在于输入XML中,也可以与输入XML中使用的前缀不同,元素选择将基于命名空间匹配而非前缀进行。 - Martin Honnen
如果您想使用Clark符号,则请考虑不要使用XPath,而是使用LINQ to XML轴方法,例如Descendants,该方法接受一个支持{}符号的XName。示例:foreach (XElement myNode in doc.Descendants("{lcmp}MyNode"))。当然,您也可以使用变量,例如 XNamespace df = "lcmp"; foreach (XElement myNode in doc.Descendants(df + "MyNode")) - Martin Honnen
6个回答

38

configuration元素位于未命名空间中,而MyNode绑定到lcmp命名空间,没有使用命名空间前缀。

这个XPATH语句将允许您在XPATH中不声明lcmp命名空间或使用命名空间前缀的情况下解析MyNode元素:

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

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

如果您不知道哪些namespace-uri将用于元素,则可以使XPATH更通用并仅匹配local-name()

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

但是,你运行的风险是匹配不同词汇表(绑定到不同的命名空间URI)中使用相同名称的不同元素。


@Mads:啊,有趣,我不知道"[namespace-uri()='lcmp'"语法...应该可以工作,如果是这样的话(我会在周一尝试),我会将其标记为答案。你知道"/configuration/{lcmp}MyNode"是否正确,只是C#不支持吗? - Scott Stafford
@Scott 不,你尝试使用的语法不是有效的XPATH语句,并且在我所知道的任何实现中都不支持。虽然它可能会扩展到该QName,但你不能以这种方式在你的XPATH语句中引用它。 - Mads Hansen
1
但是,如果命名空间URI已知(Scott现在说它已知),值得注意的是,这种方法对于Mads所述的原因(“您有可能匹配不同词汇表中的不同元素”)是不必要的脆弱。事实上,这种方法可行并不意味着它是一个好主意(除非您真的不知道URI)。 - Andrew Walker
2
@Andrew:我从未改变我的调子。如原问题所示,命名空间URI是已知的。xmlns="lcmp"命令提供的是命名空间URI而非前缀。而 @Mads 的建议是同时使用local-name()和namespace-uri(),这就是他为什么正确的答案。他确实提到了你可以选择不使用namespace-uri(),但那只是一个事后想法。 - Scott Stafford

12

你需要按照以下方式使用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);
   }

2
是的,我认为那样做可以,但我不能这样做。由于XML解析代码对实际XML文件及其使用的任何命名空间都是不可知的,mgr.AddNamespace("df", "lcmp");是一行无法编写的代码... - Scott Stafford
2
但是你的解析代码不能忽略元素名称,对吧?命名空间被认为是名称的一部分,因此忽略它是有点糟糕的设计,但如果你确定不会出现命名空间冲突,你可以这样做:"configuration / *[local-name() ='MyNode']" - Oleg Tkachenko
Scott,请解释一下你的代码如何在不知道命名空间URI的情况下识别元素?你的代码到底在寻找什么,是任何命名空间中具有本地名称“MyNode”的元素吗?那就使用Oleg的建议。否则,请更详细地解释你要查找哪些元素。 - Martin Honnen
Oleg:XPath 当然应该指定命名空间,就像你说的那样。但是我从中读取的 XML 没有别名/前缀命名空间。/configuration/lcmp:MyNode 是不正确的,因为 XPath 中的“lcmp”是一个命名空间前缀,而不是命名空间 URI。/configuration/{lcmp}MyNode 似乎是正确的语法,但 C# 似乎不支持 {} 符号。 - Scott Stafford

8
XPath故意不设计用于那些想要在XML文档中使用同一XPath表达式但未知命名空间的情况。你需要提前了解命名空间,向XPath处理器声明命名空间并在表达式中使用名称。Martin和Dan的答案展示了如何在C#中实现这一点。
这种困难的原因最好在XML namespaces规范中表述。
我们设想可扩展标记语言(XML)的应用,其中单个XML文档可以包含为多个软件模块定义和使用的元素和属性(这里称为“标记词汇”)。其中一个动机是模块化:如果存在这样一个众所周知且有有用软件可用的标记词汇,最好重用此标记,而不是重新发明它。
这些包含多个标记词汇的文档会带来识别和冲突问题。即使面对意图供其他软件包使用的标记使用相同的元素名称或属性名称时,软件模块也需要能够识别它们设计的处理元素和属性。
这些考虑要求文档结构应具有构造名称,以避免来自不同标记词汇的名称之间的冲突。本规范描述了一种机制,即XML命名空间,通过为元素和属性分配扩展名称来实现这一点。
换句话说,命名空间应该用于确保您知道文档正在讨论什么:是 <head> 元素在谈论 XHTML 文档的前言还是 AnatomyML 文档中的某个人的头部?您永远不应该对命名空间持不可知论态度,并且这基本上是任何 XML 词汇表中您应该定义的第一件事情。
你想要的是可能可以实现,但我认为它不能在单个XPath表达式中完成。首先,您需要在文档中搜寻并提取所有的namespaceURIs,然后将这些添加到命名空间管理器中,然后运行您想要的实际XPath表达式(此时您需要了解文档中命名空间的分布,否则您将需要运行很多表达式)。我认为最好使用其他东西而不是XPath(例如DOM或SAX-like API)来查找namespaceURIs,但您也可以探索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/example}LocalNodeName,但此注释通常仅用于文档- XPath对此注释一无所知。
相反,XPath使用自己的命名空间前缀。例如/ns1:root/ns2:node。但是这些与原始XML文档中可能使用的任何前缀完全独立无关。任何XPath实现都将有一种方法将其自己的前缀与命名空间URI映射起来。对于您使用的C#实现,您需要一个XmlNamespaceManager,在Perl中,您提供哈希表,xmllint采用命令行参数...因此,您只需要为您知道的命名空间URI创建一些任意前缀,并在XPath表达式中使用此前缀。您使用的前缀并不重要,在XML中,您只关心URI和localName的组合。

另一个需要记住的事情(通常会让人惊讶)是XPath不执行命名空间继承。您需要为每个具有命名空间的内容添加前缀,而不管该命名空间是从继承,xmlns属性还是命名空间前缀中获取的。此外,虽然您应该始终以URI和localName为基础思考,但也有从XML文档中访问前缀的方法。很少需要使用这些方法。


@Andrew:我确实提前知道命名空间,可以将其放入XPath中。但我不知道的是命名空间前缀,这是在你说类似于“/configuration/lcmp:MyNode”时使用的内容。似乎“/configuration/{lcmp}MyNode”是使用命名空间URI而不是前缀的正确语法,但C#似乎不支持{}符号。而且我没有前缀。 - Scott Stafford
啊,我明白了。我会写一个新的答案 - 基本上你只需要知道XML文档中的命名空间前缀与XPath表达式中的命名空间前缀除了都必须映射到相同的nsURI之外没有任何共同点。 - Andrew Walker
非常详细和冗长的编辑写作,但我认为它实际上并没有回答我的问题,那就是:什么XPath可以找到那个节点?此外,您是否在说,如果XML确实指定了前缀(它没有),那么查找该前缀的XPath查询将无法使用它? - Scott Stafford
答案是任何XPath命名空间前缀都可以。XML文档中声明的前缀或者缺乏前缀对你的问题没有影响,只有声明的命名空间URI才重要。你可以选择在XPath表达式中使用的命名空间URI和XPath前缀之间的映射关系。 - Andrew Walker
1
如何在XPath表达式中指定前缀,而不编写C#代码并硬编码XmlNamespaceManager以知道每个可能的URI? - Scott Stafford

4
以下是如何在XPathSelectElements扩展方法中使命名空间可用于XPath表达式的示例:
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答案下的评论。 - Scott Stafford

1

我很喜欢@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);
        }
    }

1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接