提问人:macleojw 提问时间:2/25/2009 更新时间:7/16/2021 访问量:84943
在 C 语言中使用具有默认命名空间的 Xpath#
Using Xpath With Default Namespace in C#
问:
我有一个带有默认命名空间的 XML 文档。我正在使用 XPathNavigator 使用 Xpath 选择一组节点,如下所示:
XmlElement myXML = ...;
XPathNavigator navigator = myXML.CreateNavigator();
XPathNodeIterator result = navigator.Select("/outerelement/innerelement");
我没有得到任何结果:我假设这是因为我没有指定命名空间。如何将命名空间包含在我的选择中?
答:
首先 - 你不需要导航员;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);
var nodes = el.SelectNodes(@"/outerelement/innerelement", nsmgr);
nsmgr.DefaultNamespace
xmlns=
xmlns:p
XElement does not contain a definition for OwnerDocument...
在这种情况下,可能是命名空间解析是问题的原因,但也可能是您的 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);
如果 outerelement 和 innerelement 的命名空间不同
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);
您可能需要尝试使用 XPath Visualizer 工具来帮助您完成任务。
XPathVisualizer 是免费的,易于使用。
重要提示:如果您使用的是 Windows 7/8 并且没有看到“文件”、“编辑”和“帮助”菜单项,请按 ALT 键。
评论
就我而言,添加前缀是不切实际的。在运行时确定了太多的 xml 或 xpath。最终,我在 XmlNode 上扩展了 methds。这还没有针对性能进行优化,它可能无法处理所有情况,但到目前为止它对我有用。
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");
希望这会有所帮助
评论
在 .NET 中使用 XPath(通过导航器或 SelectNodes/SelectSingleNode)在具有命名空间的 XML 上使用时,需要:
提供您自己的 XmlNamespaceManager
并显式地为 XPath 表达式中的所有元素添加前缀,这些元素位于命名空间中。
后者是(从下面链接的 MS 源代码转述):因为 XPath 1.0 忽略了默认的命名空间规范 (xmlns=“some_namespace”)。因此,当您使用不带前缀的元素名称时,它假定为 null 命名空间。
这就是为什么 XPath 的 .NET 实现会忽略 XmlNamespaceManager 中前缀为 String.Empty 的命名空间,并且 allways 使用 null 命名空间。
有关详细信息,请参阅 XmlNamespaceManager 和 UndefinedXsltContext 不处理默认命名空间。
我发现这个“功能”非常不方便,因为你不能通过简单地添加默认命名空间声明来使旧的 XPath 命名空间感知,但这就是它的工作原理。
评论
/root/child
<el xmlns="URI"/>
<pre:el xmlns:pre="URI"/>
<el xmlns:pre="URI"/>
我使用了上面 SpikeDog 描述的 hacky-but useful 方法。它运行良好,直到我向它抛出一个 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("{0}{1}:{2}", before, prefix_, after);
});
}
评论
我在空白默认命名空间中遇到了类似的问题。在此示例 XML 中,我混合了带有命名空间前缀的元素,以及一个不带以下内容的元素 (DataBlock):
<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” 返回了我的结果,就像我想要的那样。希望这有助于为其他人澄清这个问题。
您可以在不使用 XmlNamespaceManager 的情况下使用 XPath 语句,如下所示:
...
navigator.Select("//*[ local-name() = 'innerelement' and namespace-uri() = '' ]")
...
这是在定义了默认命名空间的 XML 中选择元素的简单方法。
关键是要使用:
namespace-uri() = ''
这将在不使用前缀的情况下找到具有默认命名空间的元素。
评论
或者,如果有人应该像我一样使用 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);
对于任何寻求快速黑客解决方案的人来说,尤其是在您了解 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");
当我处理单个文件时,我发现这比所有其他需要默认命名空间前缀的废话更容易。希望这会有所帮助。
评论
string filter = @"xmlns(:\w+)?=""([^""]+)""|xsi(:\w+)?=""([^""]+)"""; fileData = Regex.Replace(fileData, filter, "");
我的回答扩展了布兰登之前的回答。我使用他的示例创建了一个扩展方法,如下所示:
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 一起使用。
评论
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问题...
这个仍然一直困扰着我。我现在已经做了一些测试,所以希望我能帮到你。
重要的段落在这里:
XPath 将空前缀视为 null 命名空间。换句话说,只有映射到命名空间的前缀才能在 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>
本文档具有命名的默认前缀。 因此,with 不返回任何结果。MS 认为 的 XML 命名空间 URI 应为 NULL,而 的命名空间 URI 实际上是“https://standardns/”。从本质上讲,XPath 正在寻找 - 尽管这行不通,因为您不能将 NULL URI 称为“NULL”作为前缀。NULL 前缀是所有 XPath 中的默认值 - 因此存在问题。https://standardns/
XPathNavigator.Execute
/data/nsa:a
data
data
/NULL:data/nsa:a
我们如何解决这个问题?
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>
在此示例中,位于 NULL 命名空间中。 位于默认命名空间“https://standardns/”中。 根据 Microsoft 的说法,应该不起作用,因为位于 NS 中并且位于命名空间 NULL 中。 因此是隐藏的(除非通过执行奇怪的“忽略命名空间”黑客攻击),并且不能按原样选择。这本质上是根本原因 - 您不应该选择没有前缀的“a”和“data”,因为这会假设它们在同一个命名空间中,而事实并非如此!data
a
/data/a
a
https://standardns/
data
<a>
我们如何解决这个问题?
XmlNamespaceManager result = new XmlNamespaceManager(xDoc.NameTable);
result.AddNamespace("DEFAULT", "https://standardns/");
这样,我们现在可以引用 a,因为 data 是从 NULL 命名空间中选择的,并且 a 是从新前缀“DEFAULT”中选择的。在此示例中,重要的是命名空间前缀不需要保持不变。在代码中引用具有不同前缀的 URI 命名空间是完全可以接受的,因为您正在处理的文档中写入了什么。/data/DEFAULT:a
希望这对一些人有所帮助!
下一个:导入模块中全局变量的可见性
评论