Pergunta

Eu tenho um Xelement no fundo de um documento. Dado o Xelement (e Xdocument?), Existe um método de extensão para obter seu completo (ou seja, absoluto, por exemplo /root/item/element/child) Xpath?

Por exemplo, myxElement.getxPath ()?

EDIT: Ok, parece que eu esqueci algo muito importante. Opa! O índice do elemento precisa ser levado em consideração. Veja minha última resposta para a solução corrigida proposta.

Foi útil?

Solução

Os métodos de extensão:

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

E o teste:

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

E saída de amostra:

/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]

Isso deve resolver isso. Não?

Outras dicas

Atualizei o código da Chris para levar em consideração os prefixos do espaço para nome. Somente o método getABSolutexPath é modificado.

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

Deixe -me compartilhar minha mais recente modificação para esta classe. Basicamente, exclui o índice se o elemento não tiver irmãos e inclui espaços para nome com o operador local-name () que eu estava tendo problemas com o prefixo de espaço para nome.

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

Isso é realmente uma duplicata de isto pergunta. Embora não esteja marcado como a resposta, o método em minha resposta Para essa questão, é a única maneira de formular inequivocamente o XPath para um nó em um documento XML que sempre funcionará em todas as circunstâncias. (Também funciona para todos os tipos de nós, não apenas elementos.)

Como você pode ver, o XPath que produz é feio e abstrato. Mas aborda as preocupações que muitos respondentes levantaram aqui. A maioria das sugestões feitas aqui produz um XPath que, quando usado para pesquisar no documento original, produzirá um conjunto de um ou mais nós que incluem o nó de destino. É isso "ou mais", esse é o problema. Por exemplo, se eu tiver uma representação XML de um conjunto de dados, o XPath ingênuo para um elemento específico de Datarow, /DataSet1/DataTable1, também retorna os elementos de todos os outros dados do DataTable. Você não pode se desambiguar que, sem saber algo sobre como o XML é forumlado (como, existe um elemento de chave primária?).

Mas /node()[1]/node()[4]/node()[11], há apenas um nó que ele voltará, não importa o quê.

Como parte de um projeto diferente Desenvolvi um método de extensão para gerar um XPath simples para um elemento. É semelhante à resposta selecionada, mas suporta Xattribute, XText, Xcdata e XComment, além do Xelement. Está disponível como Código Nuget, página do projeto aqui: xmlspecificationCompare.codeplex.com

Se você está procurando algo fornecido nativamente pelo .NET, a resposta é não. Você teria que escrever seu próprio método de extensão para fazer isso.

Pode haver vários XPaths que levam ao mesmo elemento; portanto, encontrar o XPath mais simples que leva ao nó não é trivial.

Dito isto, é muito fácil encontrar um XPath no nó. Basta intensificar a árvore do nó até ler o nó raiz e combinar os nomes dos nó e você tem um XPath válido.

Por "Full XPath", suponho que você quer dizer uma simples cadeia de tags, pois o número de xpaths que poderia corresponder a qualquer elemento pode ser muito ampla.

O problema aqui é que é muito difícil, se não especificamente impossível, construir um determinado XPath que reversivelmente traçará ao mesmo elemento - essa é uma condição?

Se "não", talvez você possa construir uma consulta, voltando recursivamente com referência aos elementos atuais. Se "sim", então você estará tentando estendê-lo por referência cruzada para a posição do índice nos conjuntos de irmãos, referenciando atributos do tipo ID se houver, e isso será muito dependente do seu XSD se uma solução geral é possível.

A Microsoft forneceu um método de extensão para fazer isso desde o .NET Framework 3.5:

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

Basta adicionar um uso a System.Xml.XPath e invocar os seguintes métodos:

  • XPathSelectElement: selecione um único elemento
  • XPathSelectElements: Selecione elementos e retorne como um IEnumerable<XElement>
  • XPathEvaluate: Selecione nós (não apenas elementos, mas também texto, comentários etc.) e retorne como um IEnumerable<object>
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top