Использование 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);
Другие вопросы по тегам