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

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

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

РЕДАКТИРОВАТЬ: Хорошо, похоже, я упустил что-то очень важное. Упс! Индекс элемента должен быть принят во внимание. Смотрите мой последний ответ для предложенного исправленного решения.

10 ответов

Решение

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

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-представление DataSet, наивный XPath для конкретного элемента DataRow, /DataSet1/DataTable1, также возвращает элементы всех других DataRows в DataTable. Вы не можете устранить это неоднозначно, не зная кое-что о том, как XML представлен на форуме (например, есть ли элемент первичного ключа?).

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

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

В тот или иной момент я использовал это более компактное выражение с C#а также .Net Framework 4.8как цель:

      public static string GetAbsoluteXPath(XElement element,int xpversion)
{
    IEnumerable<XElement> ancestors = element.AncestorsAndSelf();
    string xpath = ancestors.Aggregate(new StringBuilder(),
                        (str, elem) => str.Insert(0, (xpversion > 1 ? ("/*:" + elem.Name.LocalName) : ("/*[local-name(.) = '" + elem.Name.LocalName + "']")) + "[" + (int)(elem.ElementsBeforeSelf().Where(el => el.Name.LocalName == elem.Name.LocalName).Count() + 1) + "]"),
                        str => str.ToString());
    return xpath;
}

В качестве общего решения работает отлично, но иногда немного медленно (на мой вкус :-)). С xpversionвы можете выбрать между XPath 1.0а также XPath >1.0версия подстановочного знака пространства имен:

Пример: с xpversion =< 1результат выглядит так: /*[local-name(.) = 'AUTOSAR'][1]/*[local-name(.) = 'AR-PACKAGES'][1]/*[local-name(.) = 'AR-PACKAGE'][1]и с xpversion > 1результат такой: /*:AUTOSAR[1]/*:AR-PACKAGES[1]/*:AR-PACKAGE[1]/*:AR-PACKAGES[1]

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

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

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

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

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

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

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

http://msdn.microsoft.com/en-us/library/bb156083%28v=vs.100%29.aspx

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

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