Использование Xpath с пространством имен по умолчанию в C#
У меня есть документ XML с пространством имен по умолчанию. Я использую XPathNavigator для выбора набора узлов, используя Xpath следующим образом:
XmlElement myXML = ...;
XPathNavigator navigator = myXML.CreateNavigator();
XPathNodeIterator result = navigator.Select("/outerelement/innerelement");
Я не получаю никаких результатов: я предполагаю, что это потому, что я не указываю пространство имен. Как я могу включить пространство имен в мой выбор?
14 ответов
Первое - вам не нужен навигатор; 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);
Возможно, вы захотите попробовать инструмент XPath Visualizer, чтобы помочь вам в этом.
XPathVisualizer бесплатный, простой в использовании.
ВАЖНО: Если вы используете Windows 7/8 и не видите пунктов меню "Файл", "Редактировать" и "Справка", нажмите клавишу ALT.
Для тех, кто ищет быстрое решение для взлома, особенно в тех случаях, когда вы знаете 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");
Я нахожу это проще, чем все остальные бессмысленные требования требовать префикса для пространства имен по умолчанию, когда я имею дело с одним файлом. Надеюсь это поможет.
При использовании XPath в.NET (через навигатор или SelectNodes/SelectSingleNode) в XML с пространствами имен вам необходимо:
предоставить свой собственный XmlNamespaceManager
и явно префикс всех элементов в выражении XPath, которые находятся в пространстве имен.
Последнее (перефразировано из источника MS, связанного ниже): потому что XPath 1.0 игнорирует спецификации пространства имен по умолчанию (xmlns="some_namespace"). Поэтому, когда вы используете имя элемента без префикса, оно принимает пустое пространство имен.
Вот почему.NET-реализация XPath игнорирует пространство имен с префиксом String.Empty в XmlNamespaceManager и всегда использует пустое пространство имен.
См. XmlNamespaceManager и UndefinedXsltContext не обрабатывают пространство имен по умолчанию для получения дополнительной информации.
Я нахожу эту "особенность" очень неудобной, потому что вы не можете сделать старое пространство имен XPath осведомленным, просто добавив объявление пространства имен по умолчанию, но именно так оно и работает.
Вы можете использовать оператор XPath без использования XmlNamespaceManager, например так:
...
navigator.Select("//*[ local-name() = 'innerelement' and namespace-uri() = '' ]")
...
Это простой способ выбора элемента в XML с определенным пространством имен по умолчанию.
Дело в том, чтобы использовать:
namespace-uri() = ''
который найдет элемент с пространством имен по умолчанию без использования префиксов.
Мой ответ расширяет предыдущий ответ Брэндона. Я использовал его пример для создания метода расширения следующим образом:
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, для которого требуется несколько пространств имен для устранения конфликтов.
Я столкнулся с подобной проблемой с пустым пространством имен по умолчанию. В этом примере 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, который работал в XPath Visualizer, но не работал в моем коде:
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;
}
Я сузил его до элемента "DataBlock" в XPath, но не смог заставить его работать, кроме простого подстановочного знака элемента 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;
}
После долгих потрясений и поиска в Google (что привело меня сюда) я решил заняться пространством имен по умолчанию непосредственно в моем загрузчике 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 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);
В моем случае добавление префикса было непрактичным. Слишком большая часть 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");
Надеюсь это поможет
Или, если кто-то должен использовать 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);
Я использовал хакерский, но полезный подход, описанный SpikeDog выше. Это работало очень хорошо, пока я не бросил в него выражение 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);
});
}
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...
Этот все еще продолжает беспокоить меня. Сейчас я провел некоторые тесты, так что, надеюсь, я могу помочь вам с этим.
Это источник от Microsoft, который является ключом к проблеме
Важный абзац здесь:
XPath обрабатывает пустой префикс как нулевое пространство имен. Другими словами, в запросах XPath можно использовать только префиксы, сопоставленные с пространствами имен. Это означает, что если вы хотите запросить пространство имен в XML-документе, даже если это пространство имен по умолчанию, вам необходимо определить для него префикс.
По сути, вы должны помнить, что синтаксический анализатор XPath использует URI пространства имен с дизайном, в котором префикс является взаимозаменяемым. Это так, при программировании вы можете назначить любой префикс, который мы хотим - до тех пор, пока совпадает URI.
Для ясности с примерами:
Пример А:
<data xmlns:nsa="http://example.com/ns"><nsa:a>World</nsa:a></data>
Это имеет URI по умолчанию NULL (
xmlns=
не определено). Из-за этого возвращается «Мир».
Пример Б:
<data xmlns:nsa="http://example.com/ns" xmlns="https://standardns/"><nsa:a>World</nsa:a></data>
Этот документ имеет именованный префикс по умолчанию.
XPathNavigator.Execute
с
/data/nsa:a
поэтому не возвращает никаких результатов. MS считает, что uri пространства имен XML для должно быть NULL, а 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
Пример С:
<data><a xmlns="https://standardns/">World</a></data>
В этом примере находится в пространстве имен NULL. находится в пространстве имен по умолчанию «https://standardns/».
/data/a
не должен работать, по мнению Microsoft, потому что
a
находится в НС
https://standardns/
а также
data
находится в пространстве имен NULL.
<a>
поэтому скрыт (за исключением странных хаков «игнорировать пространство имен») и не может быть выбран как есть. По сути, это основная причина - вы не сможете выбрать «a» и «data» без префиксов для обоих, так как это предполагает, что они находятся в одном и том же пространстве имен, а это не так!
Как решить эту проблему?
XmlNamespaceManager result = new XmlNamespaceManager(xDoc.NameTable);
result.AddNamespace("DEFAULT", "https://standardns/");
Таким образом, теперь мы можем ссылаться на a как на
/data/DEFAULT:a
поскольку данные выбираются из пространства имен NULL, а a выбирается из нового префикса «DEFAULT». В этом примере важно то, что префикс пространства имен не должен оставаться прежним. Совершенно допустимо обращаться к пространству имен URI с другим префиксом в вашем коде, в зависимости от того, что написано в документе, который вы обрабатываете.
Надеюсь, это поможет некоторым людям!
В этом случае, вероятно, причиной проблемы является разрешение пространства имен, но также возможно, что ваше выражение 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);