C#でXmlReaderを使用してXmlを読み取る


97

次のXmlドキュメントをできるだけ早く読み、追加のクラスに各サブブロックの読み込みを管理させようとしています。

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

ただし、私はXmlReaderオブジェクトを使用して各アカウントを読み取り、続いて「StatementsAvailable」を読み取ろうとしています。XmlReader.Readを使用して各要素をチェックして処理することをお勧めしますか?

各ノードを適切に処理するためにクラスを分離することを考えました。そのため、NameOfKinおよびアカウントに関する他のいくつかのプロパティを読み取るXmlReaderインスタンスを受け入れるAccountBaseクラスがあります。次に、ステートメントをやり取りして、別のクラスにステートメントについて記入してもらいたいと思っていました(その後、IListに追加します)。

これまでのところ、XmlReader.ReadElementString()を実行して「クラスごと」の部分を実行していますが、StatementsAvailable要素に移動するようにポインターに指示し、それらを反復させて、別のクラスに各プロパティを読み取らせる方法を見つけることができません。

簡単そうですね!


1
編集ボックスの右上隅にあるオレンジ色の疑問符をクリックすると、編集のヘルプが表示されます。おそらく、コードブロックを作成する必要があります。これは、最初に空白行を作成し、次に各行を4つのスペースでインデントして作成します。
Anders Abel

または、コード/ XMLの行を選択してから、エディターのツールバーの「コード」ボタン(101 010)をクリックするだけです。
marc_s

回答:


163

私の経験でXmlReaderは、誤って読みすぎることは非常に簡単です。できるだけ早く読みたいと言っていましたが、代わりにDOMモデルを使用してましたか?LINQ to XMLを使用すると、XMLがはるか簡単に機能することわかりました。

ドキュメントが特に大きい場合は、「外部」要素のそれぞれに対してfromをストリーミング方式でXmlReader作成することにより、LINQ to XMLを組み合わせることができます。これにより、ほとんどの変換作業をLINQ to XMLで実行できますが、必要なのはいつでもメモリ内のドキュメントのごく一部。ここにいくつかのサンプルコードがあります(このブログ投稿から少し変更したもの):XElementXmlReader

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のユーザーデータ(巨大)を別の形式に以前に変換しました-非常にうまく機能します。

Jonによって再フォーマットされたradarbobからの編集-どの「遠すぎる読み取り」問題が参照されているかは明確ではありませんが...

これにより、ネストが簡単になり、「遠すぎる読み取り」問題が解決されます。

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

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

    reader.ReadEndElement();
}

これは、古典的なwhileループパターンを実装しているため、「read too far」の問題に対処します。

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

17
XNode.ReadFromを呼び出すと、要素が読み取られて次の要素に移動し、次のreader.Read()が次の要素を再度読み取ります。それらが偶然同じ名前を持ち、連続している場合、基本的に要素を見逃します。
pbz

3
@pbz:ありがとう。自分が正しく編集できると自分が信じているかどうかはわかりません(それが、私がXmlReaderをどれだけ嫌っているのかです)。正しく編集できますか?
Jon Skeet、2011

1
@JonSkeet-私は何かが足りないかもしれませんが、pbzによって指摘された問題if(reader.Name == elementName)while(reader.Name == elementName)修正するために単に変更しませんか?
David McLean

1
@pbz:次の行を変更しました:XElement el = XNode.ReadFrom(reader)as XElement; XElement el = XElement.Load(reader.ReadSubtree()); これにより、連続する要素のスキップのバグが修正されます。
Dylan Hogg 2016年

1
他のコメントで述べたように、現在のバージョンのSimpleStreamAxis()は、XMLがインデントされていない場合に要素をスキップします。これは、読み込まれた要素のNode.ReadFrom()次のノードリーダーを配置するためRead()です。次のノードが空白の場合、すべて問題ありません。そうでなければ、そうではない。この問題のないバージョンについてここここ、またはここを参照してください。
dbc

29

3年後、おそらくWebApiとxmlデータに再び重点が置かれたため、この質問に出くわしました。コード的に私はパラシュートなしで飛行機からスキートを追跡する傾向があり、MS Xmlチームの記事と彼の最初のコードが、大きなXmlドキュメントの BOL ストリーミング変換の例と二重に裏付けられているのを見て、他のコメントをすぐに見落としました、最も具体的には 'pbz'から。名前で同じ要素が連続している場合、二重読み取りのために1つおきにスキップされることを指摘しました。そして実際、BOLとMSのブログ記事はどちらも、第2レベルよりも深くネストされたターゲット要素を持つソースドキュメントを解析し、この副作用を隠していました。

他の答えはこの問題に対処します。これまでうまく機能しているように思える少し単純なリビジョンを提供したいと思っています。URIだけでなく、XMLがさまざまなソースから取得される可能性があることを考慮しているため、拡張機能はユーザーが管理するXmlReaderで機能します。1つの仮定は、リーダーが初期状態にあることです。それ以外の場合は、最初の「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();
    }
}

1
「if(reader.Name.Equals(elementName))」ステートメントに対応する「else reader.Read();」がありません ステートメント。要素が希望どおりでない場合は、読み続けてください。それは私のためにそれを機能させるために追加しなければならなかったものです。
2014年

1
@Wes 2つの条件(NodeTypeとName)を折りたたむことで問題が修正else Read()され、両方に適用されます。それをキャッチしてくれてありがとう。
mdisibio 2014年

1
私はあなたに賛成票を投じましたが、Readメソッドの呼び出しが2回書かれたのを見るのはとても不満です。ここでdo whileループを使用できますか?:)
nawfal 2015

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リーダー


4
このクラスは非推奨です。使ってはいけません。
nawfal

@Elvarismあなたが共有するウェブサイトには他にも多くのxmlの読み取り方法があり、それは私を大いに助けます。投票します。XmlReaderの例を簡単に理解できる別の例を次に示します。
劉鎮瑲

0

私は経験がありませんが、XmlReaderは不要だと思います。とても使いにくいです。
XElementは非常に使いやすいです。
パフォーマンス(より高速)が必要な場合は、ファイル形式を変更し、StreamReaderクラスとStreamWriterクラスを使用する必要があります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.