Leer XML con XmlReader en C #

97

Estoy tratando de leer el siguiente documento XML lo más rápido que puedo y dejar que clases adicionales administren la lectura de cada subbloque.

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

Sin embargo, estoy tratando de utilizar el objeto XmlReader para leer cada cuenta y, posteriormente, las "Declaraciones disponibles". ¿Sugieres usar XmlReader. Lee y verifica cada elemento y manejalo?

He pensado en separar mis clases para manejar cada nodo correctamente. Entonces, hay una clase AccountBase que acepta una instancia de XmlReader que lee el NameOfKin y varias otras propiedades sobre la cuenta. Luego quería interactuar a través de las Declaraciones y dejar que otra clase se completara sobre la Declaración (y luego agregarla a una IList).

Hasta ahora he hecho la parte "por clase" haciendo XmlReader.ReadElementString () pero no puedo entrenar cómo decirle al puntero que se mueva al elemento StatementsAvailable y déjeme iterar a través de ellos y dejar que otra clase lea cada uno de esos proeprties .

¡Suena fácil!

Gloria Huang
fuente
1
Haga clic en el signo de interrogación naranja en la esquina superior derecha del cuadro de edición para obtener ayuda con la edición. Probablemente desee crear un bloque de código, que se hace primero con una línea en blanco y luego cada línea con una sangría de cuatro espacios.
Anders Abel
o simplemente seleccione sus líneas de código / XML y luego haga clic en el botón "código" (101 010) en la barra de herramientas del editor - ¡tan simple como eso!
marc_s

Respuestas:

163

Mi experiencia XmlReaderes que es muy fácil leer demasiado accidentalmente. Sé que ha dicho que desea leerlo lo más rápido posible, pero ¿ha intentado utilizar un modelo DOM en su lugar? Descubrí que LINQ to XML hace que XML funcione mucho más fácilmente.

Si su documento es particularmente grande, puede combinar XmlReadery LINQ to XML creando un XElementdesde y XmlReaderpara cada uno de sus elementos "externos" en forma de transmisión: esto le permite hacer la mayor parte del trabajo de conversión en LINQ to XML, pero aún así solo necesita una pequeña parte del documento en la memoria en cualquier momento. Aquí hay un código de muestra (adaptado ligeramente de esta publicación de 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;
          }
        }
      }
    }
  }
}

He usado esto para convertir los datos del usuario de StackOverflow (que es enorme) a otro formato antes; funciona muy bien.

EDITAR desde radarbob, reformateado por Jon, aunque no está del todo claro a qué problema de "lectura demasiado lejos" se hace referencia ...

Esto debería simplificar el anidamiento y solucionar el problema de "una lectura demasiado lejana".

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

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

    reader.ReadEndElement();
}

Esto soluciona el problema de "una lectura demasiado lejana" porque implementa el patrón de bucle while clásico:

initial read;
(while "we're not at the end") {
    do stuff;
    read;
}
Jon Skeet
fuente
17
Llamar a XNode.ReadFrom lee el elemento y pasa al siguiente, luego el siguiente reader.Read () vuelve a leer el siguiente. Básicamente, perderías un elemento si tuvieran el mismo nombre y fueran consecutivos.
pbz
3
@pbz: Gracias. No estoy seguro de confiar en mí mismo para editarlo correctamente (eso es lo mucho que me disgusta XmlReader :) ¿Puedes editarlo correctamente?
Jon Skeet
1
@JonSkeet - ¿Puede que me esté perdiendo algo, pero no cambiaré simplemente if(reader.Name == elementName)para while(reader.Name == elementName)solucionar el problema señalado por pbz?
David McLean
1
@pbz: Cambié la línea: XElement el = XNode.ReadFrom (reader) as XElement; para ser: XElement el = XElement.Load (reader.ReadSubtree ()); ya que esto corrige el error de omitir elementos consecutivos.
Dylan Hogg
1
Como se mencionó en otros comentarios, la versión actual de SimpleStreamAxis()omitirá elementos cuando el XML no esté sangrado, porque Node.ReadFrom()coloca al lector en el siguiente nodo después de que se cargue el elemento, que será omitido por el siguiente incondicional Read(). Si el siguiente nodo es un espacio en blanco, entonces todo está bien. De otra forma no. Para versiones sin este problema, consulte aquí , aquí o aquí .
dbc
29

Tres años más tarde, quizás con el énfasis renovado en WebApi y los datos xml, me encontré con esta pregunta. Desde el código, me inclino a seguir a Skeet fuera de un avión sin paracaídas, y ver su código inicial doblemente corraborado por el artículo del equipo MS Xml, así como un ejemplo en BOL Streaming Transform of Large Xml Docs , rápidamente pasé por alto los otros comentarios. , más específicamente de 'pbz', quien señaló que si tiene los mismos elementos por nombre en sucesión, todos los demás se omiten debido a la doble lectura. Y, de hecho, los artículos de los blogs de BOL y MS analizaban documentos de origen con elementos de destino anidados más profundos que el segundo nivel, enmascarando este efecto secundario.

Las otras respuestas abordan este problema. Solo quería ofrecer una revisión un poco más simple que parece funcionar bien hasta ahora y tiene en cuenta que el xml puede provenir de diferentes fuentes, no solo un uri, por lo que la extensión funciona en el XmlReader administrado por el usuario. La única suposición es que el lector está en su estado inicial, ya que de lo contrario el primer 'Read ()' podría avanzar más allá de un nodo deseado:

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
fuente
1
A su declaración "if (reader.Name.Equals (elementName))" le falta el correspondiente "else reader.Read ();" declaración. Si el elemento no es lo que quieres, quieres seguir leyendo. Eso es lo que tuve que agregar para que funcionara para mí.
Wes
1
@Wes Se solucionó el problema al contraer los dos condicionales (NodeType y Name) para que se else Read()aplique a ambos. Gracias por captar eso.
mdisibio
1
Le voté a favor, pero no estoy muy feliz de ver la llamada al método Read escrita dos veces. ¿Podría usar un bucle do while aquí? :)
nawfal
Otra respuesta que notó y resolvió el mismo problema con los documentos de MSDN: stackoverflow.com/a/18282052/3744182
dbc
17

Hacemos este tipo de análisis XML todo el tiempo. La clave es definir dónde el método de análisis dejará al lector al salir. Si siempre deja al lector en el siguiente elemento que sigue al elemento que se leyó primero, entonces puede leer de manera segura y predecible en el flujo XML. Entonces, si el lector está indexando el <Account>elemento, después de analizar, el lector indexará el</Accounts> etiqueta de cierre.

El código de análisis se parece a esto:

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

La Statementsclase solo lee en el <StatementsAvailable>nodo

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

La Statementclase se vería muy 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
fuente
6

Para los subobjetos, ReadSubtree()le brinda un lector xml limitado a los subobjetos, pero realmente creo que lo está haciendo de la manera difícil. A menos que tenga requisitos muy específicos para manejar xml inusual / impredecible, use XmlSerializer(quizás junto con sgen.exesi realmente lo desea).

XmlReaderes ... complicado. Contraste con:

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
fuente
3

El siguiente ejemplo navega por la ruta para determinar el tipo de nodo actual y luego usa XmlWriter para generar el contenido de 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();

El siguiente ejemplo usa los métodos XmlReader para leer el contenido de elementos y 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
fuente
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");

Puede recorrer xmlnode y obtener los datos ... Lector XML de C #

Elvarismo
fuente
4
Esta clase está obsoleta. No utilice.
nawfal
@Elvarism Hay muchas otras formas de leer xml en el sitio web que comparte, y eso me ayuda mucho. Te votaré. Aquí hay otro ejemplo de XmlReader de fácil comprensión .
劉鎮 瑲
0

No tengo experiencia. Pero creo que XmlReader es innecesario. Es muy dificil de usar.
XElement es muy fácil de usar.
Si necesita rendimiento (más rápido), debe cambiar el formato de archivo y usar las clases StreamReader y StreamWriter.

Mehmet
fuente