Чтение Xml с XmlReader в C#

Я пытаюсь читать следующий XML-документ так быстро, как могу, и позволяю дополнительным классам управлять чтением каждого субблока.

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

Тем не менее я пытаюсь использовать объект XmlReader для чтения каждой учетной записи, а затем "StatementsAvailable". Вы предлагаете использовать XmlReader.Read и проверять каждый элемент и обрабатывать его?

Я думал о разделении своих классов для правильной обработки каждого узла. Таким образом, существует класс AccountBase, который принимает экземпляр XmlReader, который читает NameOfKin и несколько других свойств учетной записи. Затем я хотел изучить заявления и позволить другому классу заполнить себя в заявлении (и впоследствии добавить его в IList).

До сих пор я выполнял часть "на класс", выполняя XmlReader.ReadElementString(), но я не могу понять, как сказать указателю перейти к элементу StatementsAvailable, и позвольте мне пройтись по ним и позволить другому классу читать каждый из этих объектов,

Звучит просто!

7 ответов

Решение

Мой опыт XmlReader в том, что очень легко случайно прочитать слишком много. Я знаю, что вы сказали, что хотите прочитать это как можно быстрее, но пробовали ли вы вместо этого использовать модель DOM? Я обнаружил, что LINQ to XML значительно упрощает работу XML.

Если ваш документ особенно велик, вы можете объединить XmlReader и LINQ to XML путем создания XElement из XmlReader для каждого из ваших "внешних" элементов в потоковом режиме: это позволяет вам выполнять большую часть работы по преобразованию в LINQ to XML, но при этом все равно требуется только небольшая часть документа в памяти в любой момент времени. Вот пример кода (немного адаптированный из этого поста в блоге):

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

Я использовал это для преобразования пользовательских данных Stackru (которые огромны) в другой формат раньше - это работает очень хорошо.

РЕДАКТИРОВАТЬ от радаробоба, переформатированного Джоном - хотя не совсем понятно, о какой проблеме "читайте слишком далеко" идет речь...

Это должно упростить вложение и решить проблему "слишком большое чтение".

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

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

    reader.ReadEndElement();
}

Это решает проблему "чтения слишком далеко", потому что она реализует классический шаблон цикла while:

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

Три года спустя, возможно, с новым акцентом на данные WebApi и xml, я столкнулся с этим вопросом. Поскольку в коде я склонен следовать за Скитом из самолета без парашюта, и, увидев его первоначальный код, дважды подтвержденный статьей команды MS Xml, а также примером в BOL Streaming Transform для больших документов XML, я очень быстро пропустил другие комментарии в частности, из 'pbz', который указал, что если у вас есть одинаковые элементы по имени подряд, все остальные пропускаются из-за двойного чтения. Фактически, обе статьи в блогах BOL и MS анализировали исходные документы с целевыми элементами, вложенными глубже второго уровня, маскируя этот побочный эффект.

Другие ответы решают эту проблему. Я просто хотел предложить немного более простую ревизию, которая пока работает хорошо, и принимает во внимание, что xml может исходить из разных источников, а не только из uri, и поэтому расширение работает на управляемом пользователем XmlReader. Одно из предположений состоит в том, что читатель находится в своем начальном состоянии, так как в противном случае первый Read() может продвинуться дальше желаемого узла:

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

Мы делаем этот вид парсинга XML все время. Ключ определяет, где метод синтаксического анализа оставит читателя при выходе. Если вы всегда оставляете читателя на следующем элементе, который следует за первым прочитанным элементом, вы можете безопасно и предсказуемо прочитать его в потоке XML. Так что, если читатель в настоящее время индексирует <Account> элемент, после разбора читатель будет индексировать </Accounts> закрывающий тег

Код синтаксического анализа выглядит примерно так:

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

Statements класс просто читает в <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;
            }
        }
    }
}

Statement класс будет выглядеть очень похоже

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

Для подобъектов ReadSubtree() дает вам xml-ридер, ограниченный субобъектами, но я действительно думаю, что вы делаете это нелегко. Если у вас нет особых требований для обработки необычного / непредсказуемого XML, используйте XmlSerializer (возможно, в сочетании с sgen.exe если ты действительно хочешь).

XmlReader это... сложно. Контраст с:

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

В следующем примере выполняется навигация по потоку для определения текущего типа узла, а затем использование XmlWriter для вывода содержимого 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();

В следующем примере методы XmlReader используются для чтения содержимого элементов и атрибутов.

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

Я не испытывал. Но я думаю, что XmlReader не является необходимым. Это очень сложно использовать.
XElement очень прост в использовании.
Если вам нужна производительность (быстрее), вы должны изменить формат файла и использовать классы StreamReader и StreamWriter.

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

Вы можете пройтись по xmlnode и получить данные...... C# XML Reader

Другие вопросы по тегам