使用C#中的XmlReader读取Xml

103
我正试图尽可能快地读取以下Xml文档,并让其他类来管理每个子块的读取。
<ApplicationPool>
    <Accounts>
        <Account>
            <NameOfKin></NameOfKin>
            <StatementsAvailable>
                <Statement></Statement>
            </StatementsAvailable>
        </Account>
    </Accounts>
</ApplicationPool>

然而,我正在尝试使用XmlReader对象读取每个“Account”,随后读取“StatementsAvailable”。您建议使用XmlReader.Read并检查每个元素并处理它吗?

我考虑分离我的类以正确处理每个节点。因此,有一个AccountBase类,它接受一个XmlReader实例,该实例读取NameOfKin和帐户的几个其他属性。然后,我想遍历Statements并让另一个类填充有关Statement的信息(随后将其添加到IList中)。

到目前为止,通过使用XmlReader.ReadElementString()完成了“每个类”的部分,但我无法弄清如何告诉指针移动到StatementsAvailable元素,并让我遍历它们并让另一个类读取每个属性。

听起来很容易!


1
点击编辑框右上角的橙色问号获取编辑帮助。您可能想要创建代码块,这可以通过首先插入一个空行,然后将每行缩进四个空格来完成。 - Anders Abel
只需选择您的代码/XML行,然后单击编辑器工具栏中的“代码”按钮(101 010)- 就是这么简单! - marc_s
7个回答

171

我的经验是,XmlReader 很容易意外地读取过多内容。我知道你想尽可能快地读取它,但你是否尝试使用 DOM 模型呢?我发现 LINQ to XML 让 XML 处理变得更加容易。

如果你的文档特别大,你可以将 XmlReader 和 LINQ to XML 结合起来,通过为每个 "外部" 元素从 XmlReader 创建一个 XElement 来以流式方式处理:这让你可以在 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编辑,由Jon重新格式化 - 虽然不太清楚指的是哪个“读取过多”的问题...

这应该简化嵌套并解决“读取过多”的问题。

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

18
调用XNode.ReadFrom读取元素并移到下一个元素,接着下面的reader.Read()再次读取下一个元素。如果这些元素名称相同且连续,则您将错过其中一个元素。 - pbz
3
@pbz:谢谢。我不确定自己能否正确编辑它(这就是我有多讨厌 XmlReader :))。您能够正确地编辑它吗? Translated: @pbz:谢谢。我不确定自己是否能够正确编辑它(这就是我对 XmlReader 多么不喜欢的原因 :))。您能否正确编辑它? - Jon Skeet
@Jon: :-) 我最终采用了Paul Alexander(下面)建议的方法。使用一个标签(yuk)并使其跳转到“while”循环中的第一条指令,从而跳过额外的“read”“应该”起作用,我想 :-) - pbz
1
@JonSkeet - 我可能漏掉了什么,但是将if(reader.Name == elementName)简单地更改为while(reader.Name == elementName)不会修复pbz指出的问题吗? - David McLean
1
@pbz:我将代码行更改为:XElement el = XElement.Load(reader.ReadSubtree()); 这样可以修复跳过连续元素的错误。 - Dylan Hogg
1
如其他评论所述,当前版本的SimpleStreamAxis()会在XML未缩进时跳过元素,因为Node.ReadFrom()将读取器定位到加载的元素之后的下一个节点,这将被下一个无条件的Read()跳过。如果下一个节点是空格,则一切正常。否则就不行了。有关没有此问题的版本,请参见此处此处此处 - dbc

33
三年后,也许是由于WebApi和xml数据的重新强调,我遇到了这个问题。由于我的编码倾向于跟随Skeet一起从飞机上跳下来而没有降落伞,再加上看到他的初始代码得到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();
    }
}

1
你的 "if(reader.Name.Equals(elementName))" 语句缺少相应的 "else reader.Read();" 语句。如果元素不是你想要的,你需要继续读取。这就是我必须添加的内容,才能让它对我起作用。 - Wes
1
@Wes 通过合并两个条件(NodeType和Name),使else Read()适用于两者,解决了问题。感谢你的发现。 - mdisibio
1
我给你点了赞,但是看到Read方法被写了两次,我并不是很高兴。也许你可以在这里使用do while循环? :) - nawfal
另一个回答注意到并解决了与MSDN文档相同的问题:https://dev59.com/d0vSa4cB1Zd3GeqPie4o#18282052 - 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
        }       
    }
}

8
对于子对象,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 {}}}}
        });
    }
}

似乎微软现在也建议使用XmlSerializer作为默认选项:https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/serialization/how-to-read-object-data-from-an-xml-file - Jan

5
以下示例通过流导航来确定当前节点类型,然后使用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类。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接