Lendo Xml com XmlReader em C #

97

Estou tentando ler o seguinte documento Xml o mais rápido que posso e permitir que classes adicionais gerenciem a leitura de cada sub-bloco.

<ApplicationPool>
    <Accounts>
        <Account>
            <NameOfKin></NameOfKin>
            <StatementsAvailable>
                <Statement></Statement>
            </StatementsAvailable>
        </Account>
    </Accounts>
</ApplicationPool>

No entanto, estou tentando usar o objeto XmlReader para ler cada conta e, posteriormente, o "StatementsAvailable". Você sugere usar XmlReader.Read e verificar cada elemento e manipulá-lo?

Eu pensei em separar minhas classes para lidar com cada nó corretamente. Portanto, há uma classe AccountBase que aceita uma instância XmlReader que lê o NameOfKin e várias outras propriedades sobre a conta. Então eu queria interagir com as declarações e deixar outra classe se informar sobre a declaração (e subsequentemente adicioná-la a um IList).

Até agora, eu tenho a parte "por classe" feita executando XmlReader.ReadElementString (), mas não consigo treinar como dizer ao ponteiro para mover para o elemento StatementsAvailable e deixar-me iterar por eles e deixar outra classe ler cada uma dessas propriedades .

Parece fácil!

Gloria Huang
fonte
1
Clique no ponto de interrogação laranja no canto superior direito da caixa de edição para obter ajuda de edição. Provavelmente você deseja criar um bloco de código, o que é feito primeiro por uma linha em branco e, em seguida, cada linha recuada com quatro espaços.
Anders Abel
ou apenas selecione suas linhas de código / XML e clique no botão "código" (101 010) na barra de ferramentas do editor - tão simples quanto isso!
marc_s

Respostas:

163

Minha experiência XmlReaderé que é muito fácil ler acidentalmente demais. Eu sei que você disse que queria ler o mais rápido possível, mas você tentou usar um modelo DOM? Descobri que o LINQ to XML torna o trabalho com XML muito mais fácil.

Se o seu documento for particularmente grande, você pode combinar um XmlReaderLINQ to XML criando um XElementde um XmlReaderpara cada um dos seus elementos "externos" em um fluxo contínuo: isso permite que você faça a maior parte do trabalho de conversão em LINQ to XML, mas ainda só precisa uma pequena parte do documento na memória a qualquer momento. Aqui está um exemplo de código (ligeiramente adaptado desta postagem do blog ):

static IEnumerable<XElement> SimpleStreamAxis(string inputUrl,
                                              string elementName)
{
  using (XmlReader reader = XmlReader.Create(inputUrl))
  {
    reader.MoveToContent();
    while (reader.Read())
    {
      if (reader.NodeType == XmlNodeType.Element)
      {
        if (reader.Name == elementName)
        {
          XElement el = XNode.ReadFrom(reader) as XElement;
          if (el != null)
          {
            yield return el;
          }
        }
      }
    }
  }
}

Já usei isso para converter os dados do usuário StackOverflow (que são enormes) em outro formato antes - funciona muito bem.

EDIT do radarbob, reformatado por Jon - embora não esteja muito claro a qual problema "ler muito longe" está sendo referido ...

Isso deve simplificar o aninhamento e resolver o problema de "uma leitura longe demais".

using (XmlReader reader = XmlReader.Create(inputUrl))
{
    reader.ReadStartElement("theRootElement");

    while (reader.Name == "TheNodeIWant")
    {
        XElement el = (XElement) XNode.ReadFrom(reader);
    }

    reader.ReadEndElement();
}

Isso resolve o problema de "leitura muito longe" porque implementa o padrão clássico de loop while:

initial read;
(while "we're not at the end") {
    do stuff;
    read;
}
Jon Skeet
fonte
17
Chamar XNode.ReadFrom lê o elemento e vai para o próximo, então o seguinte reader.Read () lê o próximo novamente. Você essencialmente perderia um elemento se eles tivessem o mesmo nome e fossem consecutivos.
pbz
3
@pbz: Obrigado. Não tenho certeza se confio em mim mesmo para editá-lo corretamente (isso é o quanto não gosto de XmlReader :) Você consegue editá-lo corretamente?
Jon Skeet
1
@JonSkeet - Posso estar faltando alguma coisa, mas não vou simplesmente mudar if(reader.Name == elementName)para while(reader.Name == elementName)corrigir o problema apontado por pbz?
David McLean,
1
@pbz: Eu mudei a linha: XElement el = XNode.ReadFrom (leitor) como XElement; para ser: XElement el = XElement.Load (reader.ReadSubtree ()); já que isso corrige o bug de salto de elementos consecutivos.
Dylan Hogg
1
Conforme mencionado em outros comentários, a versão atual do SimpleStreamAxis()ignorará elementos quando o XML não estiver indentado, porque Node.ReadFrom()posiciona o leitor no próximo nó após o elemento carregado - que será ignorado pelo próximo incondicional Read(). Se o próximo nó for um espaço em branco, está tudo bem. Caso contrário, não. Para versões sem esse problema, veja aqui , aqui ou aqui .
dbc
29

Três anos depois, talvez com a ênfase renovada nos dados WebApi e xml, me deparei com essa pergunta. Como estou inclinado a seguir Skeet para fora de um avião sem pára-quedas, e vendo seu código inicial duplamente corraborado pelo artigo da equipe MS Xml, bem como um exemplo na Transformação de streaming BOL de Large Xml Docs , rapidamente esqueci os outros comentários , mais especificamente de 'pbz', que apontou que, se você tiver os mesmos elementos por nome em sucessão, todos os outros serão ignorados devido à leitura dupla. E, de fato, os artigos do blog BOL e MS estavam analisando documentos de origem com elementos de destino aninhados mais profundamente do que o segundo nível, mascarando esse efeito colateral.

As outras respostas tratam desse problema. Eu só queria oferecer uma revisão um pouco mais simples que parece funcionar bem até agora e leva em consideração que o xml pode vir de fontes diferentes, não apenas um uri, e assim a extensão funciona no XmlReader gerenciado pelo usuário. A única suposição é que o leitor está em seu estado inicial, caso contrário, o primeiro 'Read ()' pode avançar além de um nó desejado:

public static IEnumerable<XElement> ElementsNamed(this XmlReader reader, string elementName)
{
    reader.MoveToContent(); // will not advance reader if already on a content node; if successful, ReadState is Interactive
    reader.Read();          // this is needed, even with MoveToContent and ReadState.Interactive
    while(!reader.EOF && reader.ReadState == ReadState.Interactive)
    {
        // corrected for bug noted by Wes below...
        if(reader.NodeType == XmlNodeType.Element && reader.Name.Equals(elementName))
        {
             // this advances the reader...so it's either XNode.ReadFrom() or reader.Read(), but not both
             var matchedElement = XNode.ReadFrom(reader) as XElement;
             if(matchedElement != null)
                 yield return matchedElement;
        }
        else
            reader.Read();
    }
}
mdisibio
fonte
1
Sua instrução "if (reader.Name.Equals (elementName))" está faltando um "else reader.Read ();" declaração. Se o elemento não for o que você deseja, continue lendo. Isso é o que eu tive que adicionar para fazer funcionar para mim.
Wes
1
@Wes Corrigido o problema recolhendo as duas condicionais (NodeType e Name) para que else Read()se aplique a ambos. Obrigado por pegar isso.
mdisibio
1
Eu votei contra você, mas não estou muito feliz em ver a chamada do método Read escrita duas vezes. Pode ser que você possa usar um loop do while aqui? :)
nawfal
Outra resposta que percebeu e resolveu o mesmo problema com os documentos do MSDN: stackoverflow.com/a/18282052/3744182
dbc
17

Fazemos esse tipo de análise XML o tempo todo. A chave é definir onde o método de análise deixará o leitor na saída. Se você sempre deixar o leitor no próximo elemento após o elemento que foi lido primeiro, poderá ler de forma segura e previsível no fluxo XML. Portanto, se o leitor estiver indexando o <Account>elemento no momento, após a análise, o leitor indexará a </Accounts>tag de fechamento.

O código de análise é semelhante a este:

public class Account
{
    string _accountId;
    string _nameOfKin;
    Statements _statmentsAvailable;

    public void ReadFromXml( XmlReader reader )
    {
        reader.MoveToContent();

        // Read node attributes
        _accountId = reader.GetAttribute( "accountId" );
        ...

        if( reader.IsEmptyElement ) { reader.Read(); return; }

        reader.Read();
        while( ! reader.EOF )
        {
            if( reader.IsStartElement() )
            {
                switch( reader.Name )
                {
                    // Read element for a property of this class
                    case "NameOfKin":
                        _nameOfKin = reader.ReadElementContentAsString();
                        break;

                    // Starting sub-list
                case "StatementsAvailable":
                    _statementsAvailable = new Statements();
                    _statementsAvailable.Read( reader );
                    break;

                    default:
                        reader.Skip();
                }
            }
            else
            {
                reader.Read();
                break;
            }
        }       
    }
}

A Statementsclasse apenas lê no <StatementsAvailable>

public class Statements
{
    List<Statement> _statements = new List<Statement>();

    public void ReadFromXml( XmlReader reader )
    {
        reader.MoveToContent();
        if( reader.IsEmptyElement ) { reader.Read(); return; }

        reader.Read();
        while( ! reader.EOF )
        {
            if( reader.IsStartElement() )
            {
                if( reader.Name == "Statement" )
                {
                    var statement = new Statement();
                    statement.ReadFromXml( reader );
                    _statements.Add( statement );               
                }
                else
                {
                    reader.Skip();
                }
            }
            else
            {
                reader.Read();
                break;
            }
        }
    }
}

A Statementclasse seria muito parecida

public class Statement
{
    string _satementId;

    public void ReadFromXml( XmlReader reader )
    {
        reader.MoveToContent();

        // Read noe attributes
        _statementId = reader.GetAttribute( "statementId" );
        ...

        if( reader.IsEmptyElement ) { reader.Read(); return; }

        reader.Read();
        while( ! reader.EOF )
        {           
            ....same basic loop
        }       
    }
}
Paul Alexander
fonte
6

Para subobjetos, ReadSubtree()dá a você um leitor de xml limitado aos subobjetos, mas eu realmente acho que você está fazendo isso da maneira mais difícil. A menos que você tenha requisitos muito específicos para lidar com xml incomum / imprevisível, use XmlSerializer(talvez junto com, sgen.exese realmente desejar).

XmlReaderé ... complicado. Contrasta com:

using System;
using System.Collections.Generic;
using System.Xml.Serialization;
public class ApplicationPool {
    private readonly List<Account> accounts = new List<Account>();
    public List<Account> Accounts {get{return accounts;}}
}
public class Account {
    public string NameOfKin {get;set;}
    private readonly List<Statement> statements = new List<Statement>();
    public List<Statement> StatementsAvailable {get{return statements;}}
}
public class Statement {}
static class Program {
    static void Main() {
        XmlSerializer ser = new XmlSerializer(typeof(ApplicationPool));
        ser.Serialize(Console.Out, new ApplicationPool {
            Accounts = { new Account { NameOfKin = "Fred",
                StatementsAvailable = { new Statement {}, new Statement {}}}}
        });
    }
}
Marc Gravell
fonte
3

O exemplo a seguir navega pelo fluxo para determinar o tipo de nó atual e, em seguida, usa XmlWriter para gerar o conteúdo XmlReader.

    StringBuilder output = new StringBuilder();

    String xmlString =
            @"<?xml version='1.0'?>
            <!-- This is a sample XML document -->
            <Items>
              <Item>test with a child element <more/> stuff</Item>
            </Items>";
    // Create an XmlReader
    using (XmlReader reader = XmlReader.Create(new StringReader(xmlString)))
    {
        XmlWriterSettings ws = new XmlWriterSettings();
        ws.Indent = true;
        using (XmlWriter writer = XmlWriter.Create(output, ws))
        {

            // Parse the file and display each of the nodes.
            while (reader.Read())
            {
                switch (reader.NodeType)
                {
                    case XmlNodeType.Element:
                        writer.WriteStartElement(reader.Name);
                        break;
                    case XmlNodeType.Text:
                        writer.WriteString(reader.Value);
                        break;
                    case XmlNodeType.XmlDeclaration:
                    case XmlNodeType.ProcessingInstruction:
                        writer.WriteProcessingInstruction(reader.Name, reader.Value);
                        break;
                    case XmlNodeType.Comment:
                        writer.WriteComment(reader.Value);
                        break;
                    case XmlNodeType.EndElement:
                        writer.WriteFullEndElement();
                        break;
                }
            }

        }
    }
    OutputTextBlock.Text = output.ToString();

O exemplo a seguir usa os métodos XmlReader para ler o conteúdo de elementos e atributos.

StringBuilder output = new StringBuilder();

String xmlString =
    @"<bookstore>
        <book genre='autobiography' publicationdate='1981-03-22' ISBN='1-861003-11-0'>
            <title>The Autobiography of Benjamin Franklin</title>
            <author>
                <first-name>Benjamin</first-name>
                <last-name>Franklin</last-name>
            </author>
            <price>8.99</price>
        </book>
    </bookstore>";

// Create an XmlReader
using (XmlReader reader = XmlReader.Create(new StringReader(xmlString)))
{
    reader.ReadToFollowing("book");
    reader.MoveToFirstAttribute();
    string genre = reader.Value;
    output.AppendLine("The genre value: " + genre);

    reader.ReadToFollowing("title");
    output.AppendLine("Content of the title element: " + reader.ReadElementContentAsString());
}

OutputTextBlock.Text = output.ToString();
Muhammad Awais
fonte
0
    XmlDataDocument xmldoc = new XmlDataDocument();
    XmlNodeList xmlnode ;
    int i = 0;
    string str = null;
    FileStream fs = new FileStream("product.xml", FileMode.Open, FileAccess.Read);
    xmldoc.Load(fs);
    xmlnode = xmldoc.GetElementsByTagName("Product");

Você pode percorrer o xmlnode e obter os dados ...... Leitor C # XML

Elvarism
fonte
4
Esta classe está obsoleta. Não use.
nawfal
@Elvarism Existem muitas outras formas de ler xml no site que você compartilha, e isso me ajuda muito. Vou votar em você. Aqui está outro exemplo de XmlReader de fácil compreensão .
劉鎮 瑲
0

Não tenho experiência. Mas acho que o XmlReader é desnecessário. É muito difícil de usar.
O XElement é muito fácil de usar.
Se você precisa de desempenho (mais rápido), deve alterar o formato do arquivo e usar as classes StreamReader e StreamWriter.

Mehmet
fonte