Чтение Xml с помощью XmlReader на C #

97

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

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

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

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

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

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

Глория Хуанг
источник
1
Щелкните оранжевый вопросительный знак в правом верхнем углу поля редактирования, чтобы получить справку по редактированию. Вероятно, вы хотите создать блок кода, который выполняется сначала пустой строкой, а затем каждой строкой с четырьмя пробелами.
Андерс Абель,
или просто выберите свои строки кода / XML и затем нажмите кнопку «код» (101010) на панели инструментов редактора - все очень просто!
marc_s

Ответы:

163

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

Если ваш документ особенно велик, вы можете комбинировать XmlReaderLINQ to XML, создавая XElementиз an 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;
          }
        }
      }
    }
  }
}

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

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

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

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;
}
Джон Скит
источник
17
Вызов XNode.ReadFrom считывает элемент и переходит к следующему, затем следующий reader.Read () снова читает следующий. По сути, вы пропустите элемент, если у них будет одно и то же имя и они будут последовательными.
pbz
3
@pbz: Спасибо. Я не уверен, что доверяю себе правильно его редактировать (вот насколько мне не нравится XmlReader :) Вы можете его правильно редактировать?
Джон Скит
1
@JonSkeet - Возможно, мне что-то не хватает, но я просто не хочу менять, if(reader.Name == elementName)чтобы while(reader.Name == elementName)исправить проблему, указанную pbz?
Дэвид Маклин
1
@pbz: я изменил строку: XElement el = XNode.ReadFrom (reader) как XElement; быть: XElement el = XElement.Load (reader.ReadSubtree ()); поскольку это исправляет ошибку пропуска последовательных элементов.
Дилан Хогг
1
Как упоминалось в других комментариях, текущая версия SimpleStreamAxis()будет пропускать элементы, если XML не имеет отступа, потому что Node.ReadFrom()позиционирует средство чтения на следующем узле после загруженного элемента - который будет пропущен следующим безусловным Read(). Если следующий узел - пробел, значит, все в порядке. В противном случае нет. Версии без этой проблемы можно найти здесь , здесь или здесь .
dbc
29

Три года спустя, возможно, с новым акцентом на данные WebApi и xml, я столкнулся с этим вопросом. Поскольку в коде я склонен следовать за Скитом из самолета без парашюта и видеть его исходный код, дважды подтверждаемый статьей команды MS Xml, а также примером в BOL Streaming Transform of Large Xml Docs , я очень быстро пропустил другие комментарии , в частности от '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();
    }
}
mdisibio
источник
1
В вашем операторе «if (reader.Name.Equals (elementName))» отсутствует соответствующий оператор «else reader.Read ();» заявление. Если элемент не тот, который вам нужен, вы хотите продолжить чтение. Это то, что мне пришлось добавить, чтобы он работал на меня.
Уэс
1
@Wes Исправил проблему, свернув два условных оператора (NodeType и Name), чтобы они применялись else Read()к обоим. Спасибо, что уловили это.
mdisibio
1
Я поддержал вас, но мне не очень приятно, что вызов метода Read написан дважды. Может быть, вы могли бы использовать здесь цикл do while? :)
nawfal
Другой ответ, который заметил и решил ту же проблему с документами MSDN: stackoverflow.com/a/18282052/3744182
dbc
17

Мы постоянно проводим такой анализ 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
        }       
    }
}
Пол Александр
источник
6

Для подобъектов ReadSubtree()дает вам xml-reader, ограниченный подобъектами, но я действительно думаю, что вы делаете это трудным путем. Если у вас нет особых требований к обработке необычного / непредсказуемого 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 {}}}}
        });
    }
}
Марк Гравелл
источник
3

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

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

Эльваризм
источник
4
Этот класс устарел. Не используй.
nawfal
@Elvarism На веб-сайте, которым вы делитесь, есть много других способов чтения xml, которые мне очень помогают. Я проголосую за тебя. Вот еще один простой для понимания пример XmlReader .
劉鎮 瑲
0

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

Мехмет
источник