Lire Xml avec XmlReader en C #

97

J'essaie de lire le document Xml suivant aussi vite que possible et de laisser des classes supplémentaires gérer la lecture de chaque sous-bloc.

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

Cependant, j'essaye d'utiliser l'objet XmlReader pour lire chaque compte et par la suite le "StatementsAvailable". Proposez-vous d'utiliser XmlReader.Read et vérifier chaque élément et le gérer?

J'ai pensé à séparer mes classes pour gérer correctement chaque nœud. Donc, il y a une classe AccountBase qui accepte une instance XmlReader qui lit le NameOfKin et plusieurs autres propriétés sur le compte. Ensuite, je voulais interagir à travers les déclarations et laisser une autre classe se renseigner sur la déclaration (et l'ajouter ensuite à un IList).

Jusqu'à présent, j'ai fait la partie "par classe" en faisant XmlReader.ReadElementString () mais je ne peux pas m'entraîner sur la manière de dire au pointeur de se déplacer vers l'élément StatementsAvailable et de me laisser les parcourir et laisser une autre classe lire chacune de ces propriétés .

Cela semble facile!

Gloria Huang
la source
1
Cliquez sur le point d'interrogation orange dans le coin supérieur droit de la zone d'édition pour obtenir de l'aide sur l'édition. Vous souhaitez probablement créer un bloc de code, ce qui se fait d'abord par une ligne vierge, puis chaque ligne en retrait de quatre espaces.
Anders Abel
ou sélectionnez simplement vos lignes de code / XML puis cliquez sur le bouton «code» (101 010) dans la barre d'outils de l'éditeur - aussi simple que cela!
marc_s

Réponses:

163

D'après mon expérience, XmlReaderil est très facile de trop lire accidentellement. Je sais que vous avez dit que vous vouliez le lire le plus rapidement possible, mais avez-vous essayé d' utiliser un modèle DOM à la place? J'ai trouvé que LINQ to XML rend le travail XML beaucoup plus facile.

Si votre document est particulièrement volumineux, vous pouvez combiner XmlReaderet LINQ to XML en créant un XElementfrom an XmlReaderpour chacun de vos éléments "externes" de manière continue: cela vous permet d'effectuer la plupart des travaux de conversion en LINQ to XML, mais vous n'avez encore besoin que une petite partie du document en mémoire à tout moment. Voici un exemple de code (légèrement adapté de ce billet 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;
          }
        }
      }
    }
  }
}

J'ai déjà utilisé cela pour convertir les données utilisateur de StackOverflow (ce qui est énorme) dans un autre format - cela fonctionne très bien.

EDIT de radarbob, reformaté par Jon - bien qu'il ne soit pas tout à fait clair de quel problème de "lecture trop loin" il est fait référence ...

Cela devrait simplifier l'imbrication et résoudre le problème "une lecture trop loin".

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

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

    reader.ReadEndElement();
}

Cela résout le problème de "lecture trop loin" car il implémente le modèle de boucle while classique:

initial read;
(while "we're not at the end") {
    do stuff;
    read;
}
Jon Skeet
la source
17
L'appel de XNode.ReadFrom lit l'élément et passe au suivant, puis le lecteur suivant.Read () lit à nouveau le suivant. Vous manqueriez essentiellement un élément s'il avait le même nom et était consécutif.
pbz le
3
@pbz: Merci. Je ne suis pas sûr de me faire confiance pour le modifier correctement (c'est à quel point je n'aime pas XmlReader :) Pouvez-vous le modifier correctement?
Jon Skeet
1
@JonSkeet - Quelque chose me manque peut-être, mais je ne changerai pas simplement if(reader.Name == elementName)pour while(reader.Name == elementName)résoudre le problème signalé par pbz?
David McLean
1
@pbz: j'ai changé la ligne: XElement el = XNode.ReadFrom (lecteur) comme XElement; être: XElement el = XElement.Load (reader.ReadSubtree ()); puisque cela corrige le bug de saut d'éléments consécutifs.
Dylan Hogg
1
Comme mentionné dans d'autres commentaires, la version actuelle de SimpleStreamAxis()ignorera les éléments lorsque le XML n'est pas indenté, car Node.ReadFrom()positionne le lecteur sur le nœud suivant après le chargement de l'élément - qui sera ignoré par le prochain inconditionnel Read(). Si le nœud suivant est un espace blanc, tout va bien. Sinon, non. Pour les versions sans ce problème, voir ici , ici ou ici .
dbc
29

Trois ans plus tard, peut-être avec l'accent renouvelé sur les données WebApi et xml, je suis tombé sur cette question. Comme je suis enclin à suivre Skeet depuis un avion sans parachute, et voyant son code initial doublement corraboré par l'article de l'équipe MS Xml ainsi qu'un exemple dans BOL Streaming Transform of Large Xml Docs , j'ai très vite négligé les autres commentaires , plus spécifiquement de «pbz», qui a souligné que si vous avez les mêmes éléments par nom successivement, tous les autres sont ignorés à cause de la double lecture. Et en fait, les articles du blog BOL et MS analysaient tous deux des documents sources avec des éléments cibles imbriqués plus profondément que le deuxième niveau, masquant cet effet secondaire.

Les autres réponses abordent ce problème. Je voulais juste proposer une révision légèrement plus simple qui semble bien fonctionner jusqu'à présent, et prend en compte le fait que le xml peut provenir de différentes sources, pas seulement d'un uri, et donc l'extension fonctionne sur le XmlReader géré par l'utilisateur. La seule hypothèse est que le lecteur est dans son état initial, sinon le premier 'Read ()' pourrait avancer au-delà d'un nœud souhaité:

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
la source
1
Votre instruction "if (reader.Name.Equals (elementName))" ne contient pas de "else reader.Read ();" correspondant. déclaration. Si l'élément ne correspond pas à ce que vous souhaitez, vous souhaitez continuer à lire. C'est ce que j'ai dû ajouter pour que cela fonctionne pour moi.
Wes
1
@Wes Correction du problème en réduisant les deux conditions (NodeType et Name) afin que le else Read()s'applique aux deux. Merci d'avoir attrapé ça.
mdisibio
1
Je vous ai voté pour, mais je ne suis pas très heureux de voir l'appel de la méthode Read écrit deux fois. Peut-être pourriez-vous utiliser une boucle do while ici? :)
nawfal
Une autre réponse qui a remarqué et résolu le même problème avec la documentation MSDN: stackoverflow.com/a/18282052/3744182
dbc
17

Nous faisons ce genre d'analyse XML tout le temps. La clé est de définir où la méthode d'analyse laissera le lecteur à la sortie. Si vous laissez toujours le lecteur sur l'élément suivant après l'élément qui a été lu en premier, vous pouvez lire en toute sécurité et de manière prévisible dans le flux XML. Donc, si le lecteur indexe actuellement l' <Account>élément, après l'analyse, le lecteur indexera la </Accounts>balise de fermeture.

Le code d'analyse ressemble à ceci:

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 lit juste dans le <StatementsAvailable>nœud

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 se ressemblerait beaucoup

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
la source
6

Pour les sous-objets, ReadSubtree()vous donne un lecteur xml limité aux sous-objets, mais je pense vraiment que vous faites cela à la dure. Sauf si vous avez des exigences très spécifiques pour gérer des fichiers XML inhabituels / imprévisibles, utilisez XmlSerializer(peut-être associé à sgen.exesi vous le souhaitez vraiment).

XmlReaderest ... délicat. Contraste avec:

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
la source
3

L'exemple suivant navigue dans le flux pour déterminer le type de nœud actuel, puis utilise XmlWriter pour générer le contenu 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'exemple suivant utilise les méthodes XmlReader pour lire le contenu des éléments et des attributs.

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

Vous pouvez parcourir xmlnode et obtenir les données ...... C # XML Reader

Elvarisme
la source
4
Cette classe est obsolète. Ne pas utiliser.
nawfal
@Elvarism Il existe de nombreuses autres méthodes de lecture xml dans le site Web que vous partagez, et cela m'aide beaucoup. Je vous voterai. Voici un autre exemple XmlReader facile à comprendre .
劉鎮 瑲
0

Je ne suis pas expérimenté. Mais je pense que XmlReader est inutile. C'est très difficile à utiliser.
XElement est très simple à utiliser.
Si vous avez besoin de performances (plus rapides), vous devez changer le format de fichier et utiliser les classes StreamReader et StreamWriter.

Mehmet
la source