Czytanie XML za pomocą XmlReader w C #

97

Próbuję jak najszybciej odczytać następujący dokument Xml i pozwolić dodatkowym klasom zarządzać odczytem każdego podbloku.

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

Jednak próbuję użyć obiektu XmlReader, aby odczytać każde konto, a następnie „StatementsAvailable”. Czy sugerujesz użycie XmlReader.Read i sprawdź każdy element i obsłuż go?

Pomyślałem o oddzieleniu moich klas, aby poprawnie obsługiwać każdy węzeł. Więc istnieje klasa AccountBase, która akceptuje wystąpienie XmlReader, które odczytuje NameOfKin i kilka innych właściwości dotyczących konta. Następnie chciałem przejść przez instrukcje i pozwolić innej klasie wypełnić informacje o instrukcji (a następnie dodać ją do IList).

Do tej pory wykonałem część „na klasę”, wykonując XmlReader.ReadElementString (), ale nie mogę poćwiczyć, jak nakazać wskaźnikowi przejście do elementu StatementsAvailable i pozwolić mi je iterować i pozwolić innej klasie odczytać każdą z tych cech .

Brzmi łatwo!

Gloria Huang
źródło
1
Kliknij pomarańczowy znak zapytania w prawym górnym rogu pola edycji, aby uzyskać pomoc dotyczącą edycji. Prawdopodobnie chcesz utworzyć blok kodu, który jest wykonywany najpierw przez pusty wiersz, a następnie każdy wiersz wcięty czterema spacjami.
Anders Abel
lub po prostu wybierz swoje linie kodu / XML, a następnie kliknij przycisk „kod” (101 010) na pasku narzędzi edytora - to takie proste!
marc_s

Odpowiedzi:

163

Z mojego doświadczenia XmlReaderwynika, że ​​bardzo łatwo jest przypadkowo przeczytać za dużo. Wiem, że powiedziałeś, że chcesz go przeczytać tak szybko, jak to możliwe, ale czy próbowałeś zamiast tego użyć modelu DOM? Odkryłem, że LINQ to XML znacznie ułatwia pracę z XML .

Jeśli dokument jest szczególnie duży, możesz łączyć XmlReaderi LINQ to XML, tworząc XElementz an XmlReaderdla każdego z elementów „zewnętrznych” w sposób strumieniowy: pozwala to wykonać większość prac związanych z konwersją w LINQ to XML, ale nadal potrzebujesz tylko niewielka część dokumentu w pamięci w dowolnym momencie. Oto przykładowy kod (nieco dostosowany z tego wpisu na blogu ):

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

Użyłem tego do konwersji danych użytkownika StackOverflow (które są ogromne) na inny format wcześniej - działa bardzo dobrze.

EDIT from radarbob, przeformatowany przez Jona - chociaż nie jest do końca jasne, do którego problemu „czytaj za daleko” jest mowa

Powinno to uprościć zagnieżdżanie i rozwiązać problem „odczyt za daleko”.

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

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

    reader.ReadEndElement();
}

To rozwiązuje problem „odczyt zbyt daleko”, ponieważ implementuje klasyczny wzorzec pętli while:

initial read;
(while "we're not at the end") {
    do stuff;
    read;
}
Jon Skeet
źródło
17
Wywołanie XNode.ReadFrom odczytuje element i przechodzi do następnego, a następnie kolejny reader.Read () czyta ponownie następny. Zasadniczo przegapiłbyś element, jeśli mają taką samą nazwę i następują po sobie.
pbz
3
@pbz: Dzięki. Nie jestem pewien, czy ufam sobie, że edytuję go poprawnie (tak bardzo nie lubię XmlReadera :) Czy jesteś w stanie edytować go poprawnie?
Jon Skeet
1
@JonSkeet - Może coś mi umknęło, ale po prostu nie zmienię, if(reader.Name == elementName)żeby while(reader.Name == elementName)naprawić problem wskazany przez PBZ?
David McLean,
1
@pbz: Zmieniłem linię: XElement el = XNode.ReadFrom (reader) as XElement; być: XElement el = XElement.Load (reader.ReadSubtree ()); ponieważ naprawia to błąd pomijania kolejnych elementów.
Dylan Hogg
1
Jak wspomniano w innych komentarzach, aktualna wersja SimpleStreamAxis()pomija elementy, gdy XML nie jest wcięty, ponieważ Node.ReadFrom()umieszcza czytnik w następnym węźle po załadowanym elemencie - który zostanie pominięty przez następny bezwarunkowy Read(). Jeśli następny węzeł jest białą spacją, wszystko jest w porządku. W przeciwnym razie nie. Wersje bez tego problemu można znaleźć tutaj , tutaj lub tutaj .
dbc
29

Trzy lata później, być może z ponownym naciskiem na dane WebApi i xml, natknąłem się na to pytanie. Ponieważ kodowo jestem skłonny podążać za Skeetem z samolotu bez spadochronu i widząc jego początkowy kod podwójnie potwierdzony przez artykuł zespołu MS Xml, a także przykład w BOL Streaming Transform of Large Xml Docs , bardzo szybko przeoczyłem inne komentarze , w szczególności od „pbz”, który wskazał, że jeśli masz te same elementy z nazwy po kolei, każdy inny jest pomijany z powodu podwójnego odczytu. W rzeczywistości artykuły na blogu BOL i MS analizowały dokumenty źródłowe z elementami docelowymi zagnieżdżonymi głębiej niż na drugim poziomie, maskując ten efekt uboczny.

Inne odpowiedzi dotyczą tego problemu. Chciałem tylko zaoferować nieco prostszą wersję, która wydaje się działać dobrze do tej pory i bierze pod uwagę, że xml może pochodzić z różnych źródeł, a nie tylko z URI, więc rozszerzenie działa na zarządzanym przez użytkownika XmlReader. Jedno założenie jest takie, że czytnik jest w stanie początkowym, ponieważ w przeciwnym razie pierwsza 'Read ()' może przejść przez żądany węzeł:

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
źródło
1
W Twojej instrukcji „if (reader.Name.Equals (elementName))” brakuje odpowiedniego „else reader.Read ();” komunikat. Jeśli element nie jest tym, czego chcesz, kontynuuj czytanie. To właśnie musiałem dodać, aby to działało dla mnie.
Wes
1
@Wes Naprawiono problem, zwijając dwa warunki warunkowe (NodeType i Name), tak aby dotyczyło else Read()obu. Dzięki, że to złapałeś.
mdisibio
1
Głosowałem za tobą, ale nie jestem zbyt szczęśliwy, że wywołanie metody Read zostało napisane dwukrotnie. Może przydałaby Ci się pętla do while? :)
nawfal
Inna odpowiedź, która zauważyła i rozwiązała ten sam problem z dokumentacją MSDN: stackoverflow.com/a/18282052/3744182
dbc
17

Cały czas wykonujemy ten rodzaj analizy XML. Kluczem jest określenie, gdzie metoda parsowania pozostawi czytnik przy wyjściu. Jeśli zawsze zostawisz czytnika na następnym elemencie następującym po elemencie, który został odczytany jako pierwszy, możesz bezpiecznie i przewidywalnie czytać w strumieniu XML. Jeśli więc czytnik aktualnie indeksuje <Account>element, po przeanalizowaniu czytelnik </Accounts>zindeksuje tag zamykający.

Kod parsowania wygląda mniej więcej tak:

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

StatementsKlasa tylko czyta w <StatementsAvailable>węźle

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

StatementKlasa będzie wyglądać bardzo podobnie

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
źródło
6

W przypadku podobiektów ReadSubtree()daje ci czytnik xml ograniczony do podobiektów, ale naprawdę myślę, że robisz to na własnej skórze. Jeśli nie masz bardzo szczegółowych wymagań dotyczących obsługi nietypowego / nieprzewidywalnego XML, użyj XmlSerializer(być może w połączeniu z, sgen.exejeśli naprawdę chcesz).

XmlReaderjest ... trudne. W przeciwieństwie do:

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
źródło
3

Poniższy przykład przechodzi przez strumień, aby określić bieżący typ węzła, a następnie używa XmlWriter do wyprowadzenia zawartości 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();

W poniższym przykładzie zastosowano metody XmlReader do odczytywania zawartości elementów i atrybutów.

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
źródło
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");

Możesz zapętlić xmlnode i pobrać dane ...... C # czytnik XML

Elwaryzm
źródło
4
Ta klasa jest przestarzała. Nie używać.
nawfal
@Elvarism Istnieje wiele innych sposobów czytania XML na udostępnianej witrynie internetowej, co bardzo mi pomaga. Zagłosuję na ciebie. Oto kolejny łatwy do zrozumienia przykład XmlReader .
劉鎮 瑲
0

Nie mam doświadczenia, ale myślę, że XmlReader jest niepotrzebny. Jest bardzo trudny w użyciu.
XElement jest bardzo łatwy w użyciu.
Jeśli potrzebujesz wydajności (szybszej), musisz zmienić format pliku i użyć klas StreamReader i StreamWriter.

Mehmet
źródło