Вопрос

У меня есть элемент XElement глубоко внутри документа.Учитывая XElement (и XDocument?), существует ли метод расширения для получения его полного (т.е.абсолютный, например /root/item/element/child) XPath?

Например.myXElement.GetXPath()?

Редактировать:Ладно, похоже, я упустил из виду что-то очень важное.Упс!Необходимо учитывать индекс элемента.Смотрите мой последний ответ для предлагаемого исправленного решения.

Это было полезно?

Решение

Методы расширений:

public static class XExtensions
{
    /// <summary>
    /// Get the absolute XPath to a given XElement
    /// (e.g. "/people/person[6]/name[1]/last[1]").
    /// </summary>
    public static string GetAbsoluteXPath(this XElement element)
    {
        if (element == null)
        {
            throw new ArgumentNullException("element");
        }

        Func<XElement, string> relativeXPath = e =>
        {
            int index = e.IndexPosition();
            string name = e.Name.LocalName;

            // If the element is the root, no index is required

            return (index == -1) ? "/" + name : string.Format
            (
                "/{0}[{1}]",
                name, 
                index.ToString()
            );
        };

        var ancestors = from e in element.Ancestors()
                        select relativeXPath(e);

        return string.Concat(ancestors.Reverse().ToArray()) + 
               relativeXPath(element);
    }

    /// <summary>
    /// Get the index of the given XElement relative to its
    /// siblings with identical names. If the given element is
    /// the root, -1 is returned.
    /// </summary>
    /// <param name="element">
    /// The element to get the index of.
    /// </param>
    public static int IndexPosition(this XElement element)
    {
        if (element == null)
        {
            throw new ArgumentNullException("element");
        }

        if (element.Parent == null)
        {
            return -1;
        }

        int i = 1; // Indexes for nodes start at 1, not 0

        foreach (var sibling in element.Parent.Elements(element.Name))
        {
            if (sibling == element)
            {
                return i;
            }

            i++;
        }

        throw new InvalidOperationException
            ("element has been removed from its parent.");
    }
}

И тест:

class Program
{
    static void Main(string[] args)
    {
        Program.Process(XDocument.Load(@"C:\test.xml").Root);
        Console.Read();
    }

    static void Process(XElement element)
    {
        if (!element.HasElements)
        {
            Console.WriteLine(element.GetAbsoluteXPath());
        }
        else
        {
            foreach (XElement child in element.Elements())
            {
                Process(child);
            }
        }
    }
}

И пример вывода:

/tests/test[1]/date[1]
/tests/test[1]/time[1]/start[1]
/tests/test[1]/time[1]/end[1]
/tests/test[1]/facility[1]/name[1]
/tests/test[1]/facility[1]/website[1]
/tests/test[1]/facility[1]/street[1]
/tests/test[1]/facility[1]/state[1]
/tests/test[1]/facility[1]/city[1]
/tests/test[1]/facility[1]/zip[1]
/tests/test[1]/facility[1]/phone[1]
/tests/test[1]/info[1]
/tests/test[2]/date[1]
/tests/test[2]/time[1]/start[1]
/tests/test[2]/time[1]/end[1]
/tests/test[2]/facility[1]/name[1]
/tests/test[2]/facility[1]/website[1]
/tests/test[2]/facility[1]/street[1]
/tests/test[2]/facility[1]/state[1]
/tests/test[2]/facility[1]/city[1]
/tests/test[2]/facility[1]/zip[1]
/tests/test[2]/facility[1]/phone[1]
/tests/test[2]/info[1]

Это должно все уладить.Нет?

Другие советы

Я обновил код Крисом, чтобы учесть префиксы пространства имен.Изменен только метод GetAbsoluteXPath.

public static class XExtensions
{
    /// <summary>
    /// Get the absolute XPath to a given XElement, including the namespace.
    /// (e.g. "/a:people/b:person[6]/c:name[1]/d:last[1]").
    /// </summary>
    public static string GetAbsoluteXPath(this XElement element)
    {
        if (element == null)
        {
            throw new ArgumentNullException("element");
        }

        Func<XElement, string> relativeXPath = e =>
        {
            int index = e.IndexPosition();

            var currentNamespace = e.Name.Namespace;

            string name;
            if (currentNamespace == null)
            {
                name = e.Name.LocalName;
            }
            else
            {
                string namespacePrefix = e.GetPrefixOfNamespace(currentNamespace);
                name = namespacePrefix + ":" + e.Name.LocalName;
            }

            // If the element is the root, no index is required
            return (index == -1) ? "/" + name : string.Format
            (
                "/{0}[{1}]",
                name,
                index.ToString()
            );
        };

        var ancestors = from e in element.Ancestors()
                        select relativeXPath(e);

        return string.Concat(ancestors.Reverse().ToArray()) +
               relativeXPath(element);
    }

    /// <summary>
    /// Get the index of the given XElement relative to its
    /// siblings with identical names. If the given element is
    /// the root, -1 is returned.
    /// </summary>
    /// <param name="element">
    /// The element to get the index of.
    /// </param>
    public static int IndexPosition(this XElement element)
    {
        if (element == null)
        {
            throw new ArgumentNullException("element");
        }

        if (element.Parent == null)
        {
            return -1;
        }

        int i = 1; // Indexes for nodes start at 1, not 0

        foreach (var sibling in element.Parent.Elements(element.Name))
        {
            if (sibling == element)
            {
                return i;
            }

            i++;
        }

        throw new InvalidOperationException
            ("element has been removed from its parent.");
    }
}

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

public static class XExtensions
{
    /// <summary>
    /// Get the absolute XPath to a given XElement, including the namespace.
    /// (e.g. "/a:people/b:person[6]/c:name[1]/d:last[1]").
    /// </summary>
    public static string GetAbsoluteXPath(this XElement element)
    {
        if (element == null)
        {
            throw new ArgumentNullException("element");
        }


        Func<XElement, string> relativeXPath = e =>
        {
            int index = e.IndexPosition();

            var currentNamespace = e.Name.Namespace;

            string name;
            if (String.IsNullOrEmpty(currentNamespace.ToString()))
            {
                name = e.Name.LocalName;
            }
            else
            {
                name = "*[local-name()='" + e.Name.LocalName + "']";
                //string namespacePrefix = e.GetPrefixOfNamespace(currentNamespace);
                //name = namespacePrefix + ":" + e.Name.LocalName;
            }

            // If the element is the root or has no sibling elements, no index is required
            return ((index == -1) || (index == -2)) ? "/" + name : string.Format
            (
                "/{0}[{1}]",
                name,
                index.ToString()
            );
        };

        var ancestors = from e in element.Ancestors()
                        select relativeXPath(e);

        return string.Concat(ancestors.Reverse().ToArray()) +
               relativeXPath(element);
    }

    /// <summary>
    /// Get the index of the given XElement relative to its
    /// siblings with identical names. If the given element is
    /// the root, -1 is returned or -2 if element has no sibling elements.
    /// </summary>
    /// <param name="element">
    /// The element to get the index of.
    /// </param>
    public static int IndexPosition(this XElement element)
    {
        if (element == null)
        {
            throw new ArgumentNullException("element");
        }

        if (element.Parent == null)
        {
            // Element is root
            return -1;
        }

        if (element.Parent.Elements(element.Name).Count() == 1)
        {
            // Element has no sibling elements
            return -2;
        }

        int i = 1; // Indexes for nodes start at 1, not 0

        foreach (var sibling in element.Parent.Elements(element.Name))
        {
            if (sibling == element)
            {
                return i;
            }

            i++;
        }

        throw new InvalidOperationException
            ("element has been removed from its parent.");
    }
}

На самом деле это дубликат это вопрос.Хотя это не помечено как ответ, метод в мой ответ ответ на этот вопрос - единственный способ однозначно сформулировать XPath для узла в XML-документе, который всегда будет работать при любых обстоятельствах.(Это также работает для всех типов узлов, а не только для элементов.)

Как вы можете видеть, XPath, который он создает, уродлив и абстрактен.но это решает проблемы, которые здесь высказали многие ответчики.Большинство сделанных здесь предложений создают XPath, который при использовании для поиска в исходном документе создаст набор из одного или нескольких узлов, включающий целевой узел.Именно это "или больше" и является проблемой.Например, если у меня есть XML-представление набора данных, наивный XPath к определенному элементу DataRow, /DataSet1/DataTable1, также возвращает элементы всех других потоков данных в DataTable.Вы не можете устранить неоднозначность, не зная кое-что о том, как форматируется XML (например, есть ли элемент первичного ключа?).

Но /node()[1]/node()[4]/node()[11], есть только один узел, который он когда-либо вернет, несмотря ни на что.

Как часть другой проект Я разработал метод расширения для генерации простого XPath к элементу.Он похож на выбранный ответ, но поддерживает XAttribute, XText, XCData и XComment в дополнение к XElement.Это доступно как код nuget, страница проекта здесь: xmlspecificationcompare.codeplex.com

Если вы ищете что-то изначально предоставляемое .NET, то ответ - нет.Для этого вам пришлось бы написать свой собственный метод расширения.

Может быть несколько xpaths, которые ведут к одному и тому же элементу, поэтому найти простейший xpath, который ведет к узлу, нетривиально.

Тем не менее, найти xpath к узлу довольно легко.Просто продвигайтесь вверх по дереву узлов, пока не прочтете корневой узел и не объедините имена узлов, и у вас не будет действительного xpath.

Под "полным xpath" я предполагаю, что вы имеете в виду простую цепочку тегов, поскольку количество xpaths, которые потенциально могут соответствовать любому элементу, может быть очень Большой.

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

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

Корпорация Майкрософт предоставила метод расширения для этого, начиная с .NET Framework 3.5:

http://msdn.microsoft.com/en-us/library/bb156083 (v= против100).aspx

Просто добавьте using к System.Xml.XPath и вызовите следующие методы:

  • XPathSelectElement:выберите один элемент
  • XPathSelectElements:выберите элементы и верните в виде IEnumerable<XElement>
  • XPathEvaluate:выберите узлы (не только элементы, но и текст, комментарии и т.д.) и верните в виде IEnumerable<object>
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top