在C#代码中解析(大)XML的最佳方法是什么?

66

我正在使用C#编写GIS客户端工具,从服务器检索基于GML的XML模式(以下是示例)中的“要素”。提取被限制为100,000个要素。

我估算最大的extract.xml可能会达到约150兆字节,因此显然DOM解析器不适用。我一直在尝试在XmlSerializerXSD.EXE生成的绑定之间进行决策--或者使用XmlReader和手工创建的对象图。

或许还有更好的方法我还没有考虑?像XLINQ或其他的方式?

请问有人可以指导我吗?特别是有关任何给定方法的内存效率。如果没有,我将不得不“原型”两种解决方案并将它们并排进行剖析。

我在.NET方面还比较新手。非常感谢您的指导。

谢谢您。Keith。


示例XML-最多有100,000个要素,每个要素最多有234,600个坐标。

<feature featId="27168306" fType="vegetation" fTypeId="1129" fClass="vegetation" gType="Polygon" ID="0" cLockNr="51598" metadataId="51599" mdFileId="NRM/TIS/VEGETATION/9543_22_v3" dataScale="25000">
  <MultiGeometry>
    <geometryMember>
      <Polygon>
        <outerBoundaryIs>
          <LinearRing>
            <coordinates>153.505004,-27.42196 153.505044,-27.422015 153.503992 .... 172 coordinates omitted to save space ... 153.505004,-27.42196</coordinates>
          </LinearRing>
        </outerBoundaryIs>
      </Polygon>
    </geometryMember>
  </MultiGeometry>
</feature>
5个回答

68

使用 XmlReader 解析大型 XML 文档。 XmlReader 提供快速的、仅向前的、非缓存式访问 XML 数据。(仅向前意味着您可以从文件开头到结尾读取 XML 文件,但无法在文件中向后移动。)XmlReader 使用少量内存,并且相当于使用简单的 SAX 读取器。

    using (XmlReader myReader = XmlReader.Create(@"c:\data\coords.xml"))
    {
        while (myReader.Read())
        {
           // Process each node (myReader.Value) here
           // ...
        }
    }

您可以使用XmlReader处理大小不超过2 GB的文件。

参考:如何使用Visual C#从文件中读取XML


4
据我所知,从.NET 2.0开始,微软建议直接使用XmlReader类而不是XmlTextReader。 - Cerebrus
2
在 CF 中也要使用特殊的 ctor。XmlReader.Create 会进行一些优化,当然记得利用 .Skip() 跳过不感兴趣的元素! - Quibblesome
能否在分块字节数组输入上使用它? - Andrius Bentkus
2
“文件大小最多为2GB的文件” - 我找不到解释这个限制的参考资料,也没有其他人提到它。您有解释此限制的链接吗? - Nickolay
1
@Nickolay MSDN在这里提到了2GB的限制:https://msdn.microsoft.com/zh-cn/library/ff647804.aspx:“您只能使用XmlTextReader和XmlValidatingReader来处理大小不超过2 GB的文件。如果需要处理更大的文件,请将源文件分成多个较小的文件或流。” - abrown
显示剩余2条评论

21

2009年5月14日:我已经改用混合方法...请参见下面的代码。

这个版本有两者的优点:
  * XmlReader/XmlTextReader(内存效率-->速度);和
  * XmlSerializer(代码生成-->开发灵活性和便利性)。

它使用XmlTextReader遍历文档,并创建“文档小片段”,然后使用XmlSerializer和使用XSD.EXE生成的“XML绑定”类反序列化它们。

我想这个技巧是通用的,而且很快...我正在解析一个包含56,000个GML要素的201 MB XML文档,大约需要7秒钟...这个应用程序的旧VB6实现需要几分钟(甚至几个小时)才能解析大的数据提取...所以我走得很顺利。

再次感谢论坛网友们捐赠宝贵的时间。我非常感激。

祝大家好运。Keith。

using System;
using System.Reflection;
using System.Xml;
using System.Xml.Serialization;
using System.IO;
using System.Collections.Generic;

using nrw_rime_extract.utils;
using nrw_rime_extract.xml.generated_bindings;

namespace nrw_rime_extract.xml
{
    internal interface ExtractXmlReader
    {
        rimeType read(string xmlFilename);
    }

    /// <summary>
    /// RimeExtractXml provides bindings to the RIME Extract XML as defined by
    /// $/Release 2.7/Documentation/Technical/SCHEMA and DTDs/nrw-rime-extract.xsd
    /// </summary>
    internal class ExtractXmlReader_XmlSerializerImpl : ExtractXmlReader
    {
        private Log log = Log.getInstance();

        public rimeType read(string xmlFilename)
        {
            log.write(
                string.Format(
                    "DEBUG: ExtractXmlReader_XmlSerializerImpl.read({0})",
                    xmlFilename));
            using (Stream stream = new FileStream(xmlFilename, FileMode.Open))
            {
                return read(stream);
            }
        }

        internal rimeType read(Stream xmlInputStream)
        {
            // create an instance of the XmlSerializer class, 
            // specifying the type of object to be deserialized.
            XmlSerializer serializer = new XmlSerializer(typeof(rimeType));
            serializer.UnknownNode += new XmlNodeEventHandler(handleUnknownNode);
            serializer.UnknownAttribute += 
                new XmlAttributeEventHandler(handleUnknownAttribute);
            // use the Deserialize method to restore the object's state
            // with data from the XML document.
            return (rimeType)serializer.Deserialize(xmlInputStream);
        }

        protected void handleUnknownNode(object sender, XmlNodeEventArgs e)
        {
            log.write(
                string.Format(
                    "XML_ERROR: Unknown Node at line {0} position {1} : {2}\t{3}",
                    e.LineNumber, e.LinePosition, e.Name, e.Text));
        }

        protected void handleUnknownAttribute(object sender, XmlAttributeEventArgs e)
        {
            log.write(
                string.Format(
                    "XML_ERROR: Unknown Attribute at line {0} position {1} : {2}='{3}'",
                    e.LineNumber, e.LinePosition, e.Attr.Name, e.Attr.Value));
        }

    }

    /// <summary>
    /// xtractXmlReader provides bindings to the extract.xml 
    /// returned by the RIME server; as defined by:
    ///   $/Release X/Documentation/Technical/SCHEMA and 
    /// DTDs/nrw-rime-extract.xsd
    /// </summary>
    internal class ExtractXmlReader_XmlTextReaderXmlSerializerHybridImpl :
        ExtractXmlReader
    {
        private Log log = Log.getInstance();

        public rimeType read(string xmlFilename)
        {
            log.write(
                string.Format(
                    "DEBUG: ExtractXmlReader_XmlTextReaderXmlSerializerHybridImpl." +
                    "read({0})",
                    xmlFilename));

            using (XmlReader reader = XmlReader.Create(xmlFilename))
            {
                return read(reader);
            }

        }

        public rimeType read(XmlReader reader)
        {
            rimeType result = new rimeType();
            // a deserializer for featureClass, feature, etc, "doclets"
            Dictionary<Type, XmlSerializer> serializers = 
                new Dictionary<Type, XmlSerializer>();
            serializers.Add(typeof(featureClassType), 
                newSerializer(typeof(featureClassType)));
            serializers.Add(typeof(featureType), 
                newSerializer(typeof(featureType)));

            List<featureClassType> featureClasses = new List<featureClassType>();
            List<featureType> features = new List<featureType>();
            while (!reader.EOF)
            {
                if (reader.MoveToContent() != XmlNodeType.Element)
                {
                    reader.Read(); // skip non-element-nodes and unknown-elements.
                    continue;
                }

                // skip junk nodes.
                if (reader.Name.Equals("featureClass"))
                {
                    using (
                        StringReader elementReader =
                            new StringReader(reader.ReadOuterXml()))
                    {
                        XmlSerializer deserializer =
                            serializers[typeof (featureClassType)];
                        featureClasses.Add(
                            (featureClassType)
                            deserializer.Deserialize(elementReader));
                    }
                    continue;
                    // ReadOuterXml advances the reader, so don't read again.
                }

                if (reader.Name.Equals("feature"))
                {
                    using (
                        StringReader elementReader =
                            new StringReader(reader.ReadOuterXml()))
                    {
                        XmlSerializer deserializer =
                            serializers[typeof (featureType)];
                        features.Add(
                            (featureType)
                            deserializer.Deserialize(elementReader));
                    }
                    continue;
                    // ReadOuterXml advances the reader, so don't read again.
                }

                log.write(
                    "WARNING: unknown element '" + reader.Name +
                    "' was skipped during parsing.");
                reader.Read(); // skip non-element-nodes and unknown-elements.
            }
            result.featureClasses = featureClasses.ToArray();
            result.features = features.ToArray();
            return result;
        }

        private XmlSerializer newSerializer(Type elementType)
        {
            XmlSerializer serializer = new XmlSerializer(elementType);
            serializer.UnknownNode += new XmlNodeEventHandler(handleUnknownNode);
            serializer.UnknownAttribute += 
                new XmlAttributeEventHandler(handleUnknownAttribute);
            return serializer;
        }

        protected void handleUnknownNode(object sender, XmlNodeEventArgs e)
        {
            log.write(
                string.Format(
                    "XML_ERROR: Unknown Node at line {0} position {1} : {2}\t{3}",
                    e.LineNumber, e.LinePosition, e.Name, e.Text));
        }

        protected void handleUnknownAttribute(object sender, XmlAttributeEventArgs e)
        {
            log.write(
                string.Format(
                    "XML_ERROR: Unknown Attribute at line {0} position {1} : {2}='{3}'",
                    e.LineNumber, e.LinePosition, e.Attr.Name, e.Attr.Value));
        }
    }
}

13

简单来说,为了让任何通过谷歌搜索找到这个帖子的人更加明显,需要做如下总结:

在.NET 2之前,XmlTextReader是标准API中最节约内存的XML解析器(感谢Mitch;-)。

.NET 2引入了更好的XmlReader类。它是一种仅向前遍历元素的迭代器(有点像StAX解析器)。(感谢Cerebrus;-)

还有要记住的是,如果任何XML实例可能大于约500k,请勿使用DOM!

谢谢大家。Keith.


6

1
有没有人尝试过比较Sax和XmlTextReader的性能?这将会很有趣。 - MrTelly
我也很感兴趣,我还没有进行过比较。 - Andy White
.NET没有提供本地的SAX解析器,但我读过一篇文章(我想是在Slashdot上),展示了使用XmlReader“基元”轻松制作自己的SAX解析器。 - corlettk

2

我想分享一个简单的扩展方法,作为使用XmlReader的示例(如Mitch所回答的):

public static bool SkipToElement (this XmlReader xmlReader, string elementName)
{
    if (!xmlReader.Read ())
        return false;

    while (!xmlReader.EOF)
    {
        if (xmlReader.NodeType == XmlNodeType.Element && xmlReader.Name == elementName)
            return true;

        xmlReader.Skip ();
    }

    return false;
}

使用方法:

using (var xml_reader = XmlReader.Create (this.source.Url))
{
    if (!SkipToElement (xml_reader, "Root"))
        throw new InvalidOperationException ("XML element \"Root\" was not found.");

    if (!SkipToElement (xml_reader, "Users"))
        throw new InvalidOperationException ("XML element \"Root/Users\" was not found.");

    ...
}

不错...一个建议的改进:如果所寻找的元素不存在,它总是终止当前操作(必须跳过我们的读取器到EOF),因此在SkipTo中直接抛出异常而不是返回false...你已经有了要报告的所寻找的元素名称,所以在错误消息中使用它而不是重复自己。 - corlettk
是的,你说得对。只是在我的特定情况下,我需要告诉缺失元素的完整路径,而不仅仅是它的名称。 - Michael Logutov
ReadToFollowing会为您完成此操作。 - 5andr0

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