Lettura di Xml con XmlReader in C #


97

Sto cercando di leggere il seguente documento Xml il più velocemente possibile e lasciare che classi aggiuntive gestiscano la lettura di ogni sottoblocco.

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

Tuttavia, sto cercando di utilizzare l'oggetto XmlReader per leggere ogni account e successivamente "StatementsAvailable". Suggerite di usare XmlReader.Read e controllare ogni elemento e gestirlo?

Ho pensato di separare le mie classi per gestire correttamente ogni nodo. Quindi c'è una classe AccountBase che accetta un'istanza XmlReader che legge NameOfKin e molte altre proprietà sull'account. Poi volevo interagire attraverso le Dichiarazioni e lasciare che un'altra classe si compilasse sulla Dichiarazione (e successivamente la aggiungesse a un IList).

Finora ho fatto la parte "per classe" eseguendo XmlReader.ReadElementString () ma non riesco a capire come dire al puntatore di spostarsi sull'elemento StatementsAvailable e lasciarmi scorrere attraverso di essi e lasciare che un'altra classe legga ciascuna di quelle proprietà .

Sembra facile!


1
Fare clic sul punto interrogativo arancione nell'angolo in alto a destra della casella di modifica per ottenere assistenza per la modifica. Probabilmente vuoi creare un blocco di codice, che viene fatto prima da una riga vuota e poi ogni riga rientrata con quattro spazi.
Anders Abel

oppure seleziona semplicemente le tue righe di codice / XML e quindi fai clic sul pulsante "codice" (101 010) nella barra degli strumenti dell'editor - così semplice!
marc_s

Risposte:


163

La mia esperienza XmlReaderè che è molto facile leggere accidentalmente troppo. So che hai detto che vuoi leggerlo il più rapidamente possibile, ma hai invece provato a utilizzare un modello DOM? Ho scoperto che LINQ to XML rende il lavoro XML molto più semplice.

Se il tuo documento è particolarmente grande, puoi combinare XmlReadere LINQ to XML creando un XElementda un XmlReaderper ciascuno dei tuoi elementi "esterni" in streaming: questo ti consente di eseguire la maggior parte del lavoro di conversione in LINQ to XML, ma è comunque necessario solo una piccola porzione del documento in memoria in qualsiasi momento. Ecco un po 'di codice di esempio (adattato leggermente da questo post del 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;
          }
        }
      }
    }
  }
}

L'ho già usato per convertire i dati utente di StackOverflow (che è enorme) in un altro formato prima: funziona molto bene.

MODIFICA da Radarbob, riformattato da Jon - anche se non è del tutto chiaro a quale problema di "lettura troppo lontana" si fa riferimento ...

Questo dovrebbe semplificare l'annidamento e risolvere il problema "una lettura troppo lontana".

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

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

    reader.ReadEndElement();
}

Questo risolve il problema "una lettura troppo lontana" perché implementa il classico modello di ciclo while:

initial read;
(while "we're not at the end") {
    do stuff;
    read;
}

17
La chiamata a XNode.ReadFrom legge l'elemento e passa a quello successivo, quindi il lettore successivo.Read () legge di nuovo quello successivo. In sostanza, mancherebbe un elemento se hanno lo stesso nome e sono consecutivi.
pbz

3
@pbz: grazie. Non sono sicuro di fidarmi di me stesso per modificarlo correttamente (questo è quanto non mi piace XmlReader :) Sei in grado di modificarlo correttamente?
Jon Skeet

1
@ JonSkeet - Potrei mancare qualcosa ma non cambierò semplicemente if(reader.Name == elementName)per while(reader.Name == elementName)risolvere il problema segnalato da pbz?
David McLean

1
@pbz: ho cambiato la riga: XElement el = XNode.ReadFrom (reader) as XElement; essere: XElement el = XElement.Load (reader.ReadSubtree ()); poiché questo risolve il bug di salto di elementi consecutivi.
Dylan Hogg

1
Come accennato in altri commenti, la versione corrente di SimpleStreamAxis()salterà gli elementi quando l'XML non è rientrato, perché Node.ReadFrom()posiziona il lettore nel nodo successivo dopo il caricamento dell'elemento, che verrà ignorato dal successivo incondizionato Read(). Se il nodo successivo è uno spazio bianco, va tutto bene. Altrimenti no. Per le versioni senza questo problema vedere qui , qui o qui .
DC

29

Tre anni dopo, forse con la rinnovata enfasi sui dati WebApi e xml, mi sono imbattuto in questa domanda. Poiché a livello di codice sono incline a seguire Skeet fuori da un aereo senza paracadute, e vedendo il suo codice iniziale doppiamente corroborato dall'articolo del team di MS Xml e da un esempio in BOL Streaming Transform of Large Xml Docs , ho molto rapidamente trascurato gli altri commenti , in particolare da 'pbz', che ha sottolineato che se si hanno gli stessi elementi per nome in successione, tutti gli altri vengono saltati a causa della doppia lettura. In effetti, gli articoli del blog BOL e MS stavano analizzando i documenti di origine con elementi di destinazione annidati più in profondità del secondo livello, mascherando questo effetto collaterale.

Le altre risposte affrontano questo problema. Volevo solo offrire una revisione leggermente più semplice che sembra funzionare bene fino ad ora e tiene conto del fatto che l'xml potrebbe provenire da fonti diverse, non solo un uri, e quindi l'estensione funziona sull'XmlReader gestito dall'utente. L'unica ipotesi è che il lettore sia nel suo stato iniziale, poiché altrimenti il ​​primo 'Read ()' potrebbe avanzare oltre un nodo desiderato:

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

1
Nell'istruzione "if (reader.Name.Equals (elementName))" manca un corrispondente "else reader.Read ();" dichiarazione. Se l'elemento non è quello che vuoi, continua a leggere. Questo è quello che ho dovuto aggiungere per farlo funzionare per me.
Wes

1
@Wes ha risolto il problema comprimendo i due condizionali (NodeType e Name) in modo che si else Read()applicasse a entrambi. Grazie per averlo capito.
mdisibio

1
Ti ho votato positivamente, ma non sono molto felice di vedere la chiamata al metodo Read scritta due volte. Potresti usare un ciclo do while qui? :)
nawfal

Un'altra risposta che ha notato e risolto lo stesso problema con la documentazione MSDN: stackoverflow.com/a/18282052/3744182
DBC

17

Facciamo questo tipo di analisi XML tutto il tempo. La chiave è definire dove il metodo di analisi lascerà il lettore in uscita. Se lasci sempre il lettore sull'elemento successivo dopo l'elemento che è stato letto per primo, puoi leggere in modo sicuro e prevedibile nel flusso XML. Quindi, se il lettore sta attualmente indicizzando l' <Account>elemento, dopo aver analizzato il lettore indicizzerà il </Accounts>tag di chiusura.

Il codice di analisi ha un aspetto simile a questo:

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 Statementsclasse legge solo nel <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 Statementclasse sarebbe più o meno la stessa

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

6

Per gli oggetti secondari, ReadSubtree()ti offre un lettore xml limitato agli oggetti secondari, ma penso davvero che tu lo stia facendo nel modo più duro. A meno che tu non abbia requisiti molto specifici per la gestione di xml insoliti / imprevedibili, usa XmlSerializer(magari abbinato a sgen.exese lo desideri).

XmlReaderè ... complicato. Contrasto a:

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

3

L'esempio seguente esplora il flusso per determinare il tipo di nodo corrente e quindi usa XmlWriter per l'output del contenuto 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();

L'esempio seguente usa i metodi XmlReader per leggere il contenuto di elementi e attributi.

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

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

È possibile eseguire il ciclo tramite xmlnode e ottenere i dati ...... C # XML Reader


4
Questa classe è deprecata. Non usare.
nawfal

@Elvarism Ci sono molti altri modi di leggere xml nel sito web che condividi e questo mi aiuta molto. Ti voterò. Ecco un altro esempio di XmlReader facilmente comprensibile .
劉鎮 瑲

0

Non ho esperienza, ma penso che XmlReader non sia necessario. È molto difficile da usare.
XElement è molto facile da usare.
Se hai bisogno di prestazioni (più veloci) devi cambiare il formato del file e usare le classi StreamReader e StreamWriter.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.