在 C# 中使用具有默认命名空间的 Xpath

Posted

技术标签:

【中文标题】在 C# 中使用具有默认命名空间的 Xpath【英文标题】:Using Xpath With Default Namespace in C# 【发布时间】:2010-10-09 19:20:49 【问题描述】:

我有一个带有默认命名空间的 XML 文档。我正在使用 XPathNavigator 使用 Xpath 选择一组节点,如下所示:

XmlElement myXML = ...;  
XPathNavigator navigator = myXML.CreateNavigator();
XPathNodeIterator result = navigator.Select("/outerelement/innerelement");

我没有得到任何结果:我假设这是因为我没有指定命名空间。如何在我的选择中包含命名空间?

【问题讨论】:

查看类似的问题***.com/questions/4271689/…,其中有一些紧凑的示例作为单元测试 【参考方案1】:

首先 - 您不需要导航器; SelectNodes / SelectSingleNode 应该足够了。

但是,您可能需要一个命名空间管理器 - 例如:

XmlElement el = ...; //TODO
XmlNamespaceManager nsmgr = new XmlNamespaceManager(
    el.OwnerDocument.NameTable);
nsmgr.AddNamespace("x", el.OwnerDocument.DocumentElement.NamespaceURI);
var nodes = el.SelectNodes(@"/x:outerelement/x:innerelement", nsmgr);

【讨论】:

注意:将别名设置为空白字符串 (nsmgr.AddNamespace("", el.OwnerDocument.DocumentElement.NamespaceURI);) 使其成为默认命名空间。然而,遗憾的是,这并不意味着您可以在不使用前缀的情况下使用 XPath(例如 var nodes = el.SelectNodes(@"/outerelement/innerelement", nsmgr);)。只有您可以使用nsmgr.DefaultNamespace 看到这一点。更多信息在这里:***.com/a/4271875/361842。如果希望避免使用前缀,则添加注释以节省其他时间;即你不能。 还有一个技巧,如果您只是将 xmlns= 替换为 xmlns:p 其中 p 可以是任何有效的前缀,您的代码应该可以正常工作。 我试过你的代码,但是 Visual Studio 说XElement does not contain a definition for OwnerDocument...,你能看看吗? imgur.com/a/TPHVeoM【参考方案2】:

您可能想尝试使用 XPath Visualizer 工具来帮助您完成任务。

XPathVisualizer 是免费的,易于使用。

重要提示:如果您使用的是 Windows 7/8 并且没有看到文件、编辑和帮助菜单项,请按 ALT 键。

【讨论】:

不知道这是否适用于 XP。如果您只是获取二进制文件,它可能会。我没有XP,所以无法测试。我不知道其他工具。 版本 1.2 在 Windows XP 下工作 - xpathvisualizer.codeplex.com/releases/view/42941【参考方案3】:

对于任何寻求快速破解解决方案的人,尤其是在您了解 XML 并且无需担心命名空间等所有问题的情况下,您可以绕过这个烦人的“小”功能" 只需将文件读取为字符串并替换攻击属性即可:

XmlDocument doc = new XmlDocument();
string fileData = File.ReadAllText(fileName);
fileData = fileData.Replace(" xmlns=\"", " whocares=\"");
using (StringReader sr = new StringReader(fileData))

   doc.Load(sr);


XmlNodeList nodeList = doc.SelectNodes("project/property");

当我处理单个文件时,我发现这比所有其他需要默认命名空间前缀的废话更容易。希望这会有所帮助。

【讨论】:

这太棒了。关于处理 XmlNamespaceManager 的所有其他 BS 都是无用的。 10,000 次中有 9999 次您知道 XML。 唯一的缺点是,正如预期的那样,选定的 XML 项目位于 null 命名空间中。虽然我真的很喜欢这种 hack,但如果命名空间是您工作的必要条件,那么这不符合要求。 “废话”与单个文件无关——它与命名空间标签有关。如果您可以控制 XML,那么您不必使用命名空间(标签将存在于空命名空间中)。如果您无法控制,那么您正在为需要 1/2 代码的解决方案创建一个 hack。 Timothy 是否指出,现在您将有两种不同的解决方案,具体取决于您是否可以抓住不重复标签的机会。因为你想保存两行,用了 4 行来做。 @Gerard - 我并没有试图深入任何人的皮肤。我的帖子更多的是与吻有关,而不是嘲笑。无论如何:(1)我将我的解决方案称为 hack,暗示这不是“正确”的方法; (2) 无论我的听众是否能够控制 XML,我都明确指出,如果您了解 XML 并且不需要担心名称空间,这才是一个很好的解决方案。 (3) 虽然它可能确实只需要几行额外的行来包含一个管理器并指定命名空间,但 XPath 字符串本身最终看起来非常混乱,所有额外的命名空间噪音把它们弄得一团糟。 这个不错的正则表达式 string filter = @"xmlns(:\w+)?=""([^""]+)""|xsi(:\w+)?=""([^""]+)"""; fileData = Regex.Replace(fileData, filter, ""); 我在这里找到了 techoctave.com/c7/posts/113-c-reading-xml-with-namespace【参考方案4】:

在带有命名空间的 XML 上使用 .NET 中的 XPath(通过导航器或 SelectNodes/SelectSingleNode)时,您需要:

提供您自己的 XmlNamespaceManager

显式地为 XPath 表达式中的所有元素添加前缀,这些元素位于命名空间中。

后者是(从下面链接的 MS 源代码解释):因为 XPath 1.0 忽略了默认命名空间规范 (xmlns="some_namespace")。因此,当您使用不带前缀的元素名称时,它假定为 null 命名空间。

这就是为什么 XPath 的 .NET 实现会忽略 XmlNamespaceManager 中带有前缀 String.Empty 的命名空间,并且总是使用空命名空间。

更多信息请参见XmlNamespaceManager and UndefinedXsltContext don't handle default namespace。

我发现这个“功能”非常不方便,因为您不能通过简单地添加默认命名空间声明来使旧的 XPath 命名空间感知,但这就是它的工作原理。

【讨论】:

您写的 XPath 1.0 忽略默认命名空间。那是错误的。如果您使用 /root/child,您将忽略它,因为无前缀 QName 测试选择空或空命名空间下的元素按定义 正确地说,一个 QName 是一个(命名空间 URI、本地名称、前缀)的元组。所以,这个元素 <el xmlns="URI"/> 有一个 QName ('URI','el','') 等价于这个另一个元素 <pre:el xmlns:pre="URI"/> ('URI','el','pre') 但不同于最后一个元素 @ 987654325@('','el','') @Alejandro:经过考虑,我决定删除我的 cmets,因为我觉得这个讨论毫无意义。如果我的回答不够准确,请写一个更好的。如果我的回答不正确,请提供显示它的工作示例。 它不会忽略默认命名空间。您只是不能指定默认命名空间。巨大的差异。并且差异是有道理的——任何给定标签的默认命名空间可能不同; xpath 应该去一个明确的标签。除非您使用标签名称,否则您可以这样做。但它会找到所有使用默认命名空间定义的标签;您只需在 xpath 表达式中使用标记指定该命名空间。 @GerardONeill 我之前对 user357812(又名 Alejandro)的评论仍然适用。另外,我认为您将 XML 文档中包含的名称空间规范与适用于 XPath 表达式本身的名称空间规范混淆了。我的回答是关于后者。【参考方案5】:

您可以像这样使用 XPath 语句而不使用 XmlNamespaceManager:

...
navigator.Select("//*[ local-name() = 'innerelement' and namespace-uri() = '' ]")
...

这是在定义了默认命名空间的 XML 中选择元素的简单方法。 重点是使用:

namespace-uri() = ''

它将找到具有默认命名空间而不使用前缀的元素。

【讨论】:

namespace-uri='' 对我不起作用,但它给了我动态创建 xpath 表达式的想法,如下所示: doc.SelectNodes(String.Format("//*[local -name()='innerelement' and namespace-uri()='0']", doc.DocumentElement.NamespaceURI));并且有效【参考方案6】:

我的回答扩展了 Brandon 之前的回答。我用他的例子创建了一个扩展方法如下:

static public class XmlDocumentExt

    static public XmlNamespaceManager GetPopulatedNamespaceMgr(this System.Xml.XmlDocument xd)
    
        XmlNamespaceManager nmsp = new XmlNamespaceManager(xd.NameTable);
        XPathNavigator nav = xd.DocumentElement.CreateNavigator();
        foreach (KeyValuePair<string,string> kvp in nav.GetNamespacesInScope(XmlNamespaceScope.All))
        
            string sKey = kvp.Key;
            if (sKey == "")
            
                sKey = "default";
            
            nmsp.AddNamespace(sKey, kvp.Value);
        

        return nmsp;
    

然后在我的 XML 解析代码中,我只添加了一行:

XmlDocument xdCandidate = new XmlDocument();
xdCandidate.Load(sCandidateFile);
XmlNamespaceManager nmsp = xdCandidate.GetPopulatedNamespaceMgr();  // 1-line addition
XmlElement xeScoreData = (XmlElement)xdCandidate.SelectSingleNode("default:ScoreData", nmsp);

我真的很喜欢这种方法,因为它在从源 XML 文件加载命名空间方面是完全动态的,并且它并没有完全忽视 XML 命名空间的概念,因此这可以用于需要多个命名空间来消除冲突的 XML .

【讨论】:

我注意到,与@Brandon 的解决方案相比,您将空白 ("") 键替换为“默认”。 Brandon 添加了“”键和带有“默认”键的第二个版本。【参考方案7】:

我在使用空白默认命名空间时遇到了类似的问题。在这个 XML 示例中,我混合了带有命名空间前缀的元素,以及一个没有:

<src:SRCExample xmlns="urn:some:stuff:here" xmlns:src="www.test.com/src" xmlns:a="www.test.com/a" xmlns:b="www.test.com/b">
 <DataBlock>
  <a:DocID>
   <a:IdID>7</a:IdID>
  </a:DocID>
  <b:Supplimental>
   <b:Data1>Value</b:Data1>
   <b:Data2/>
   <b:Extra1>
    <b:More1>Value</b:More1>
   </b:Extra1>
  </b:Supplimental>
 </DataBlock>
</src:SRCExample>

我尝试使用在 XPath Visualizer 中有效的 XPath,但在我的代码中无效:

  XmlDocument doc = new XmlDocument();
  doc.Load( textBox1.Text );
  XPathNavigator nav = doc.DocumentElement.CreateNavigator();
  XmlNamespaceManager nsman = new XmlNamespaceManager( nav.NameTable );
  foreach ( KeyValuePair<string, string> nskvp in nav.GetNamespacesInScope( XmlNamespaceScope.All ) ) 
    nsman.AddNamespace( nskvp.Key, nskvp.Value );
  

  XPathNodeIterator nodes;

  XPathExpression failingexpr = XPathExpression.Compile( "/src:SRCExample/DataBlock/a:DocID/a:IdID" );
  failingexpr.SetContext( nsman );
  nodes = nav.Select( failingexpr );
  while ( nodes.MoveNext() ) 
    string testvalue = nodes.Current.Value;
  

我将其缩小到 XPath 的“DataBlock”元素,但只能通过简单地通配 DataBlock 元素使其工作:

  XPathExpression workingexpr = XPathExpression.Compile( "/src:SRCExample/*/a:DocID/a:IdID" );
  failingexpr.SetContext( nsman );
  nodes = nav.Select( failingexpr );
  while ( nodes.MoveNext() ) 
    string testvalue = nodes.Current.Value;
  

经过多次头疼和谷歌搜索(这让我来到这里),我决定直接在我的 XmlNamespaceManager 加载器中处理默认命名空间,方法是将其更改为:

  foreach ( KeyValuePair<string, string> nskvp in nav.GetNamespacesInScope( XmlNamespaceScope.All ) ) 
    nsman.AddNamespace( nskvp.Key, nskvp.Value );
    if ( nskvp.Key == "" ) 
      nsman.AddNamespace( "default", nskvp.Value );
    
  

所以现在“default”和“”指向同一个命名空间。一旦我这样做了,XPath "/src:SRCExample/default:DataBlock/a:DocID/a:IdID" 就会像我想要的那样返回我的结果。希望这有助于为其他人澄清问题。

【讨论】:

【参考方案8】:

如果外部元素和内部元素的命名空间不同

XmlNamespaceManager manager = new XmlNamespaceManager(myXmlDocument.NameTable);
                            manager.AddNamespace("o", "namespaceforOuterElement");
                            manager.AddNamespace("i", "namespaceforInnerElement");
string xpath = @"/o:outerelement/i:innerelement"
// For single node value selection
XPathExpression xPathExpression = navigator.Compile(xpath );
string reportID = myXmlDocument.SelectSingleNode(xPathExpression.Expression, manager).InnerText;

// For multiple node selection
XmlNodeList myNodeList= myXmlDocument.SelectNodes(xpath, manager);

【讨论】:

【参考方案9】:

在我的情况下,添加前缀是不切实际的。太多的 xml 或 xpath 是在运行时确定的。最终我在 XmlNode 上扩展了方法。这尚未针对性能进行优化,它可能无法处理所有情况,但到目前为止它对我有用。

    public static class XmlExtenders


    public static XmlNode SelectFirstNode(this XmlNode node, string xPath)
    
        const string prefix = "pfx";
        XmlNamespaceManager nsmgr = GetNsmgr(node, prefix);
        string prefixedPath = GetPrefixedPath(xPath, prefix);
        return node.SelectSingleNode(prefixedPath, nsmgr);
    

    public static XmlNodeList SelectAllNodes(this XmlNode node, string xPath)
    
        const string prefix = "pfx";
        XmlNamespaceManager nsmgr = GetNsmgr(node, prefix);
        string prefixedPath = GetPrefixedPath(xPath, prefix);
        return node.SelectNodes(prefixedPath, nsmgr);
    

    public static XmlNamespaceManager GetNsmgr(XmlNode node, string prefix)
    
        string namespaceUri;
        XmlNameTable nameTable;
        if (node is XmlDocument)
        
            nameTable = ((XmlDocument) node).NameTable;
            namespaceUri = ((XmlDocument) node).DocumentElement.NamespaceURI;
        
        else
        
            nameTable = node.OwnerDocument.NameTable;
            namespaceUri = node.NamespaceURI;
        
        XmlNamespaceManager nsmgr = new XmlNamespaceManager(nameTable);
        nsmgr.AddNamespace(prefix, namespaceUri);
        return nsmgr;
    

    public static string GetPrefixedPath(string xPath, string prefix)
    
        char[] validLeadCharacters = "@/".ToCharArray();
        char[] quoteChars = "\'\"".ToCharArray();

        List<string> pathParts = xPath.Split("/".ToCharArray()).ToList();
        string result = string.Join("/",
                                    pathParts.Select(
                                        x =>
                                        (string.IsNullOrEmpty(x) ||
                                         x.IndexOfAny(validLeadCharacters) == 0 ||
                                         (x.IndexOf(':') > 0 &&
                                          (x.IndexOfAny(quoteChars) < 0 || x.IndexOfAny(quoteChars) > x.IndexOf(':'))))
                                            ? x
                                            : prefix + ":" + x).ToArray());
        return result;
    

然后在你的代码中使用类似的东西

        XmlDocument document = new XmlDocument();
        document.Load(pathToFile);
        XmlNode node = document.SelectFirstNode("/rootTag/subTag");

希望对你有帮助

【讨论】:

我使用了这段代码,它就像一个魅力,直到我今天遇到了问题。它不处理使用管道的 xpath 表达式。由于我发现原始代码难以阅读,因此我使用正则表达式重写了它,我觉得这更容易(请参阅下面的答案)【参考方案10】:

我使用了上面 SpikeDog 描述的 hacky 但有用的方法。它工作得非常好,直到我向它抛出了一个使用管道组合多个路径的 xpath 表达式。

所以我用正则表达式重写了它,并认为我会分享:

public string HackXPath(string xpath_, string prefix_)

    return System.Text.RegularExpressions.Regex.Replace(xpath_, @"(^(?![A-Za-z0-9\-\.]+::)|[A-Za-z0-9\-\.]+::|[@|/|\[])(?'Expression'[A-Za-z][A-Za-z0-9\-\.]*)", x =>
                
                    int expressionIndex = x.Groups["Expression"].Index - x.Index;
                    string before = x.Value.Substring(0, expressionIndex);
                    string after = x.Value.Substring(expressionIndex, x.Value.Length - expressionIndex);
                    return String.Format("01:2", before, prefix_, after);
                );

【讨论】:

这个版本在路径表达式有属性时有问题。例如,“element/@id”应该是“p:element/@id”时被转换为“p:element/p:@id”。【参考方案11】:

或者,如果有人像我一样应该使用 XPathDocument:

XPathDocument xdoc = new XPathDocument(file);
XPathNavigator nav = xdoc.CreateNavigator();
XmlNamespaceManager nsmgr = new XmlNamespaceManager(nav.NameTable);
nsmgr.AddNamespace("y", "http://schemas.microsoft.com/developer/msbuild/2003");
XPathNodeIterator nodeIter = nav.Select("//y:PropertyGroup", nsmgr);

【讨论】:

【参考方案12】:

1] 如果您有一个在命名空间中没有任何前缀的 XML 文件:

<bookstore xmlns="http://www.contoso.com/books">
…
</bookstore>

你有这个解决方法:

XmlTextReader reader = new XmlTextReader(@"C:\Temp\books.xml");
// ignore the namespace as there is a single default namespace:
reader.Namespaces = false;
XPathDocument document = new XPathDocument(reader);
XPathNavigator navigator = document.CreateNavigator();
XPathNodeIterator nodes = navigator.Select("//book");

2] 如果您在命名空间中有一个带前缀的 XML 文件:

<bookstore xmlns:ns="http://www.contoso.com/books">
…
</bookstore>

使用这个:

XmlTextReader reader = new XmlTextReader(@"C:\Temp\books.xml");
XPathDocument document = new XPathDocument(reader);
XPathNavigator navigator = document.CreateNavigator();
XPathNodeIterator nodes = navigator.Select("//book");

当然,如果需要,您可以使用命名空间管理:

XmlTextReader reader = new XmlTextReader(@"C:\Temp\books.xml");
XPathDocument document = new XPathDocument(reader);
XPathNavigator navigator = document.CreateNavigator();
XmlNamespaceManager nsmgr = new XmlNamespaceManager(reader.NameTable);
nsmgr.AddNamespace("ns", "http://www.contoso.com/book");
XPathNodeIterator nodes = navigator.Select("//book", nsmgr);

我认为这是让代码在大多数情况下工作的最简单方法。

我希望这有助于解决这个 Microsoft 问题……

【讨论】:

【参考方案13】:

这个问题一直困扰着我。我现在做了一些测试,希望能帮到你。

This is the source from Microsoft, which is the key to the problem

重要的段落在这里:

XPath 将空前缀视为空名称空间。换句话说,只有映射到命名空间的前缀才能在 XPath 查询中使用。这意味着如果要查询 XML 文档中的命名空间,即使它是默认命名空间,也需要为其定义前缀。

本质上,您必须记住 XPath 解析器使用命名空间 URI - 前缀可互换的设计。就是这样,在编程时,您可以分配我们想要的任何前缀 - 只要 URI 匹配。

为了清晰起见:

示例 A:

<data xmlns:nsa="http://example.com/ns"><nsa:a>World</nsa:a></data>

这有一个 NULL 默认 URI(xmlns= 未定义)。因此/data/nsa:a 返回“世界”。

示例 B:

<data xmlns:nsa="http://example.com/ns" xmlns="https://standardns/"><nsa:a>World</nsa:a></data>

此文档有一个命名的默认前缀https://standardns/XPathNavigator.Execute/data/nsa:a 因此不返回任何结果。 MS 认为data 的 XML namespace uri 应该为 NULL,data 的命名空间 URI 实际上是“https://standardns/”。本质上,XPath 正在寻找/NULL:data/nsa:a——尽管这不起作用,因为您不能将 NULL URI 称为“NULL”作为前缀。 NULL 前缀是所有 XPath 中的默认值 - 因此存在问题。

我们如何解决这个问题?

XmlNamespaceManager result = new XmlNamespaceManager(xDoc.NameTable);
result.AddNamespace("DEFAULT", "https://standardns/");
result.AddNamespace("nsa", "http://example.com/ns");

这样,我们现在可以将a称为/DEFAULT:data/nsa:a

示例 C:

<data><a xmlns="https://standardns/">World</a></data>

在此示例中,data 位于 NULL 命名空间中。 a 位于默认命名空间“https://standardns/”中。根据微软的说法,/data/a 不应该工作,因为a 在 NS https://standardns/ 中,而data 在命名空间 NULL 中。 &lt;a&gt; 因此被隐藏(除非通过奇怪的“忽略命名空间”黑客)并且不能按原样选择。这本质上是根本原因 - 您不应该选择没有前缀的“a”和“data”,因为这会假设它们在同一个命名空间中,而它们不是!

我们如何解决这个问题?

XmlNamespaceManager result = new XmlNamespaceManager(xDoc.NameTable);
result.AddNamespace("DEFAULT", "https://standardns/");

这样,我们现在可以将 a 称为 /data/DEFAULT:a,因为数据是从 NULL 命名空间中选择的,而 a 是从新前缀“DEFAULT”中选择的。在这个例子中重要的是命名空间 prefix 不需要保持不变。在您的代码中引用具有不同前缀的 URI 命名空间是完全可以接受的,这与您正在处理的文档中所写的内容有关。

希望这对某些人有所帮助!

【讨论】:

【参考方案14】:

在这种情况下,问题的原因可能是名称空间解析,但也可能是您的 XPath 表达式本身不正确。您可能需要先对其进行评估。

这是使用 XPathNavigator 的代码。

//xNav is the created XPathNavigator.
XmlNamespaceManager mgr = New XmlNamespaceManager(xNav.NameTable);
mgr.AddNamespace("prefix", "http://tempuri.org/");

XPathNodeIterator result = xNav.Select("/prefix:outerelement/prefix:innerelement", mgr);

【讨论】:

以上是关于在 C# 中使用具有默认命名空间的 Xpath的主要内容,如果未能解决你的问题,请参考以下文章

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

如何使用 XPath 忽略命名空间

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

使用 xpath 访问具有命名空间的子节点

具有命名空间的 XML 文档上的 XPath

XPATHS 和默认命名空间