Pergunta sobre xml, c#, xpath – SelectSingleNode retornando null para um caminho de nó xml bom e conhecido usando XPath

36

Considere este documento XML simples. O XML serializado mostrado aqui é o resultado de um XmlSerializer de um objeto POCO complexo cujo esquema não tenho controle.

<My_RootNode xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="">
  <id root="2.16.840.1.113883.3.51.1.1.1" extension="someIdentifier" xmlns="urn:hl7-org:v3" /> 
  <creationTime xsi:nil="true" xmlns="urn:hl7-org:v3" />      
</My_RootNode>

O objetivo é extrair o valor do atributo extension no nó id. Neste caso, estamos usando o método SelectSingleNode e recebemos uma expressão XPath:

XmlNode idNode = myXmlDoc.SelectSingleNode("/My_RootNode/id");
//idNode is evaluated to null at this point in the debugger!
string msgID = idNode.Attributes.GetNamedItem("extension").Value;

O problema é que oSelectSingleNode método retorna null para a expressão XPath especificada.

Questão: alguma idéia sobre a exatidão desta consulta XPath, ou por que essa chamada de método + expressão XPath retornaria um valor nulo? Talvez os namespaces sejam parte do problema?

@Oded: Correto, estamos olhando para um XmlDocument que carregou a saída de seqüência de caracteres de um XmlSerializer. p.campbell
A primeira coisa a verificar é se o documento XML foi carregado corretamente. Eu posso ver um atributo xmlns vazio no final do nó raiz - é isso mesmo? Oded
@pcampbell: este é um documento grande (HL7!)? Em caso afirmativo, talvez você queira tentar serializar diretamente no XmlDocument. Se você quiser uma amostra disso, me avise. John Saunders

Sua resposta

9   a resposta
44

Eu suspeito fortemente que o problema esteja relacionado a namespaces. Tente se livrar do namespace e você ficará bem - mas obviamente isso não ajudará em seu caso real, onde eu presumo que o documento seja fixo.

Não consigo me lembrar de como especificar um namespace em uma expressão XPath, mas tenho certeza de que esse é o problema.

EDIT: Ok, eu lembrei de como fazer isso agora. Não é muito agradável - você precisa criar umXmlNamespaceManager por isso. Veja um exemplo de código que funciona com o seu documento de amostra:

using System;
using System.Xml;

public class Test
{
    static void Main()
    {
        XmlDocument doc = new XmlDocument();
        XmlNamespaceManager namespaces = new XmlNamespaceManager(doc.NameTable);
        namespaces.AddNamespace("ns", "urn:hl7-org:v3");
        doc.Load("test.xml");
        XmlNode idNode = doc.SelectSingleNode("/My_RootNode/ns:id", namespaces);
        string msgID = idNode.Attributes["extension"].Value;
        Console.WriteLine(msgID);
    }
}
tente // id para ver se é realmente um problema de namespace. ScottE
Você pode adicionar namespaces ao criar o xmldoc. Oded
Observe que o nome do namespace não precisa corresponder ao xml. Kevin Dimey
Como alterar o código se a raiz é XmlNode, não XmlDocument? Evgeny Levin
0

Apenas para resolver os problemas de espaço de nomes, no meu caso, eu estava correndo em documentos com vários namespaces e precisava lidar com namespaces corretamente. Eu escrevi a função abaixo para obter um gerenciador de espaço para nome para lidar com qualquer namespace no documento:

private XmlNamespaceManager GetNameSpaceManager(XmlDocument xDoc)
    {
        XmlNamespaceManager nsm = new XmlNamespaceManager(xDoc.NameTable);
        XPathNavigator RootNode = xDoc.CreateNavigator();
        RootNode.MoveToFollowing(XPathNodeType.Element);
        IDictionary<string, string> NameSpaces = RootNode.GetNamespacesInScope(XmlNamespaceScope.All);

        foreach (KeyValuePair<string, string> kvp in NameSpaces)
        {
            nsm.AddNamespace(kvp.Key, kvp.Value);
        }

        return nsm;
    }
2

Bem ... eu tive o mesmo problema e foi uma dor de cabeça. Como não me importei muito com o namespace ou o esquema xml, acabei de excluir esses dados do meu xml e isso resolveu todos os meus problemas. Pode não ser a melhor resposta? Provavelmente, mas se você não quer lidar com tudo isso e só se preocupa com os dados (e não estará usando o xml para alguma outra tarefa), a exclusão do namespace pode resolver seus problemas.

XmlDocument vinDoc = new XmlDocument();
string vinInfo = "your xml string";
vinDoc.LoadXml(vinInfo);

vinDoc.InnerXml = vinDoc.InnerXml.Replace("xmlns=\"http://tempuri.org\/\", "");
Isso só funcionará para seus dados específicos. Não é uma resposta geral. John Saunders
Se você tem controle sobre o xsd, o xml e o código que o consome, é um excelente exemplo de uma maneira de lidar com o problema. Tomei esta resposta e generalizei um pouco usando um RegEx e enviei para esse segmento. David
8

Isso deve funcionar no seu caso sem remover namespaces:

XmlNode idNode = myXmlDoc.GetElementsByTagName("id")[0];
GetElementsByTagName retorna um XmlNodeList então apenas deixe o [0] se você quiser mais de 1 item correspondente Alex Stephens
0

A regra a ter em conta é: se o seu documento especificar umnamespace, você DEVE usar umXmlNamespaceManager na sua chamada paraSelectNodes() ouSelectSingleNode(). Isso é uma coisa boa.

Veja o artigoVantagens dos namespaces . Jon Skeet faz um ótimo trabalho em sua resposta mostrando como usarXmlNamespaceManager. (Esta resposta deve ser apenas um comentário sobre essa resposta, mas eu não tenho bastante Rep Points para comentar.)

12

Se você quiser ignorar namespaces completamente, você pode usar isto:

static void Main(string[] args)
{
    string xml =
        "<My_RootNode xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns=\"\">\n" +
        "    <id root=\"2.16.840.1.113883.3.51.1.1.1\" extension=\"someIdentifier\" xmlns=\"urn:hl7-org:v3\" />\n" +
        "    <creationTime xsi:nil=\"true\" xmlns=\"urn:hl7-org:v3\" />\n" +
        "</My_RootNode>";

    XmlDocument doc = new XmlDocument();
    doc.LoadXml(xml);

    XmlNode idNode = doc.SelectSingleNode("/*[local-name()='My_RootNode']/*[local-name()='id']");
}
Velho, mas ouro, corrigiu meu problema perfeitamente. AsheraH
0

apenas use // id em vez de / id. Funciona bem no meu código

7

Desculpe, você esqueceu o namespace. Você precisa:

XmlNamespaceManager ns = new XmlNamespaceManager(myXmlDoc.NameTable);
ns.AddNamespace("hl7","urn:hl7-org:v3");
XmlNode idNode = myXmlDoc.SelectSingleNode("/My_RootNode/hl7:id", ns);

De fato, seja aqui ou em serviços da Web, a obtenção de null de volta de uma operação XPath ou de qualquer coisa que dependa do XPath geralmente indica um problema com namespaces XML.

@Steven: boa pegada, e a maneira mais educada de dizer: "Ei, manequim, você esqueceu de usar o objeto que acabou de construir" que eu ouvi há algum tempo. "Quase completamente correto" - vou ter que lembrar disso. John Saunders
Obrigado John, na verdade, o namespace está faltando / em branco nos dados de teste! Você suspeita que isso é parte do problema? p.campbell
Eu acredito que John está quase completamente correto, porque o nome completo do elemento "id" é o par "urn: h17-org: v3" e "id". Você está procurando por "" e "id" com seu XPATH, para que não encontre nada. No entanto, para realmente funcionar, você precisa passar a instância ns como o segundo parâmetro de SelectSingleNode. Steven Sudit
Doh - passou todo esse tempo inventando um programa de teste, apenas para descobrir que você tinha me batido nisso :) Jon Skeet
-1

A resposta de Roisgoen funcionou para mim, mas para torná-lo mais genérico, você pode usar um RegEx:

//Substitute "My_RootNode" for whatever your root node is
string strRegex = @"<My_RootNode(?<xmlns>\s+xmlns([\s]|[^>])*)>";
var myMatch = new Regex(strRegex, RegexOptions.None).Match(myXmlDoc.InnerXml);
if (myMatch.Success)
{
    var grp = myMatch.Groups["xmlns"];
    if (grp.Success)
    {
        myXmlDoc.InnerXml = myXmlDoc.InnerXml.Replace(grp.Value, "");
    }
}

Eu admito totalmente que esta não é uma resposta das melhores práticas, mas é uma solução fácil e às vezes é tudo o que precisamos.

Perguntas relacionadas