Как использовать XPath с пространством имен по умолчанию без префикса?

Что такое XPath (в C# API для XDocument.XPathSelectElements(xpath, nsman), если это имеет значение) для запроса всех MyNode из этого документа?

<?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, но Microsoft не удосужилась реализовать это... правда?

6 ответов

Решение

configuration элемент находится в безымянном пространстве имен, а MyNode привязан к lcmp Пространство имен без префикса пространства имен.

Это заявление XPATH позволит вам обратиться к MyNode элемент, не объявив lcmp namespace или используйте префикс пространства имен в вашем XPATH:

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

Это соответствует любому элементу, который является дочерним configuration а затем использует файл предиката с namespace-uri() а также local-name() функции, чтобы ограничить его MyNode элемент.

Если вы не знаете, какое пространство имен-uri будет использоваться для элементов, то вы можете сделать XPATH более универсальным и просто сопоставить его с local-name():

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

Однако вы рискуете сопоставить разные элементы в разных словарях (связанных с разными пространствами имен-uri), которые используют одно и то же имя.

Вам необходимо использовать 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);
   }

XPath (намеренно) не предназначен для случая, когда вы хотите использовать одно и то же выражение XPath для некоторых неизвестных пространств имен, которые существуют только в документе XML. Предполагается, что вы заранее знаете пространство имен, объявите пространство имен процессору XPath и будете использовать имя в своем выражении. Ответы Мартина и Дэна показывают, как это сделать в C#.

Причина этой трудности лучше всего выражена в спецификации пространств имен XML:

Мы предполагаем применения расширяемого языка разметки (XML), в котором один XML-документ может содержать элементы и атрибуты (здесь называемые "словарь разметки"), которые определены и используются несколькими программными модулями. Одной из причин этого является модульность: если существует такой словарь разметки, который понятен и для которого имеется полезное программное обеспечение, лучше повторно использовать эту разметку, а не заново ее изобретать.

Такие документы, содержащие несколько словарей разметки, создают проблемы распознавания и столкновения. Программные модули должны иметь возможность распознавать элементы и атрибуты, которые они предназначены для обработки, даже в условиях "коллизий", возникающих, когда разметка, предназначенная для какого-либо другого программного пакета, использует то же имя элемента или имя атрибута.

Эти соображения требуют, чтобы конструкции документа имели имена, сконструированные так, чтобы избежать столкновений между именами из разных словарей разметки. Эта спецификация описывает механизм пространств имен XML, который выполняет это, назначая расширенные имена элементам и атрибутам.

То есть пространства имен должны использоваться для того, чтобы вы знали, о чем говорит ваш документ: <head> элемент, говорящий о преамбуле к документу XHTML или заголовку somebody в документе AnatomyML? Вы никогда не "должны" быть агностиком в отношении пространства имен, и это первое, что вы должны определить в любом словаре XML.

Должна быть возможность делать то, что вы хотите, но я не думаю, что это можно сделать в одном выражении XPath. Прежде всего вам нужно покопаться в документе и извлечь все namespaceURI, затем добавить их в диспетчер пространства имен и затем запустить нужное выражение XPath (и вам нужно кое-что узнать о распределении пространств имен в документе указать, или у вас есть много выражений для запуска). Я думаю, что вам, вероятно, лучше всего использовать что-то другое, чем XPath (например, DOM или SAX-подобный API), чтобы найти namespaceURI, но вы также можете изучить ось пространства имен XPath (в XPath 1.0), использовать namespace-uri-from-QName функция (в XPath 2.0) или использовать выражения, как у Олега "configuration/*[local-name() = 'MyNode']", В любом случае, я думаю, что вам лучше всего избегать написания XPath, независимого от пространства имен! Почему вы не знаете свое пространство имен раньше времени? Как вы собираетесь избегать совпадений с вещами, которые вы не собираетесь сопоставлять?

Редактировать - вы знаете, namespaceURI?

Вот и получается, что ваш вопрос смутил нас всех. Очевидно, вы знаете URI пространства имен, но вы не знаете префикс пространства имен, который используется в документе XML. Действительно, в этом случае префикс пространства имен не используется, и URI становится пространством имен по умолчанию, где он определен. Главное, что нужно знать, это то, что выбранный префикс (или отсутствие префикса) не имеет отношения к вашему выражению XPath (и синтаксическому анализу XML в целом). Атрибут prefix / xmlns - это всего лишь один из способов связать узел с URI пространства имен, когда документ выражен в виде текста. Вы можете взглянуть на этот ответ, где я попытаюсь уточнить префиксы пространства имен.

Вы должны попытаться представить XML-документ так же, как анализатор - каждый узел имеет URI пространства имен и локальное имя. Правила префикса / наследования пространства имен просто экономят ввод URI много раз. Один из способов записать это в нотации Кларка: то есть вы пишете { 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 и localNames, существуют также способы доступа к префиксу из XML-документа. Это редко приходится использовать это.

Вот пример того, как сделать пространство имен доступным для выражения XPath в методе расширения XPathSelectElements:

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);
     }
   }
 }
}

Мне нравится @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);
        }
    }

Пример с Xpath 2.0 + библиотека:

using Wmhelp.XPath2;

doc.XPath2SelectElements("/*:configuration/*:MyNode");

Увидеть:

XPath и XSLT 2.0 для.NET?

Другие вопросы по тегам