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

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

41 голосов
/ 16 января 2009

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

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

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

Ответы [ 9 ]

42 голосов
/ 18 января 2009

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

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]

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

11 голосов
/ 22 марта 2010

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

Позвольте мне поделиться своими последними изменениями в этом классе. По сути, он исключает индекс, если у элемента нет родственного элемента, и включает пространства имен с оператором 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.");
    }
}
4 голосов
/ 17 января 2009

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

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

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

2 голосов
/ 01 сентября 2013

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

0 голосов
/ 07 июня 2014

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>
0 голосов
/ 17 января 2009

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

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

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

0 голосов
/ 17 января 2009

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

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

0 голосов
/ 17 января 2009

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

...