Подтвердить что ты не робот

Получить XPath для XElement?

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

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

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

4b9b3361

Ответ 1

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

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]

Это должно решить. Нет?

Ответ 2

Я обновил код Крисом, чтобы учесть префиксы пространства имен. Изменен только метод 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.");
    }
}

Ответ 3

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

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

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

Ответ 4

Позвольте мне поделиться своей последней модификацией с этим классом. В основном это исключает индекс, если элемент не имеет родного брата и включает пространства имен с оператором 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.");
    }
}

Ответ 5

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

Ответ 6

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

Ответ 7

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

Тем не менее, довольно легко найти xpath для node. Просто поднимите дерево node, пока не увидите корень node, и соедините имена node, и у вас есть допустимый xpath.

Ответ 8

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

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

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

Ответ 9

Microsoft предоставила метод расширения для этого, поскольку .NET Framework 3.5:

http://msdn.microsoft.com/en-us/library/bb156083 (v = vs .100).aspx

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

  • XPathSelectElement: выберите один элемент
  • XPathSelectElements: выберите элементы и верните их как IEnumerable<XElement>
  • XPathEvaluate: выберите узлы (не только элементы, но также текст, комментарии и т.д.) и вернитесь как IEnumerable<object>