如何在 C# 中仅反序列化 XML 文档的一部分

Posted

技术标签:

【中文标题】如何在 C# 中仅反序列化 XML 文档的一部分【英文标题】:How to deserialize only part of an XML document in C# 【发布时间】:2010-09-27 01:19:07 【问题描述】:

这是我要解决的问题的虚构示例。如果我在 C# 中工作,并且有这样的 XML:

<?xml version="1.0" encoding="utf-8"?>
<Cars>
  <Car>
    <StockNumber>1020</StockNumber>
    <Make>Nissan</Make>
    <Model>Sentra</Model>
  </Car>
  <Car>
    <StockNumber>1010</StockNumber>
    <Make>Toyota</Make>
    <Model>Corolla</Model>
  </Car>
  <SalesPerson>
    <Company>Acme Sales</Company>
    <Position>
       <Salary>
          <Amount>1000</Amount>
          <Unit>Dollars</Unit>
    ... and on... and on....
  </SalesPerson>
</Cars>

SalesPerson 中的 XML 可能非常长,大小为兆字节。我想反序列化标记,不反序列化 SalesPerson XML 元素,而是将其保持为原始格式“供以后使用”。

基本上我希望能够将其用作 XML 的对象表示。

[System.Xml.Serialization.XmlRootAttribute("Cars", Namespace = "", IsNullable = false)]
public class Cars

    [XmlArrayItem(typeof(Car))]
    public Car[] Car  get; set; 

    public Stream SalesPerson  get; set; 


public class Car

    [System.Xml.Serialization.XmlElementAttribute("StockNumber")]
    public string StockNumber get; set; 

    [System.Xml.Serialization.XmlElementAttribute("Make")]
    public string Make get; set; 

    [System.Xml.Serialization.XmlElementAttribute("Model")]
    public string Model get; set; 

其中 Cars 对象上的 SalesPerson 属性将包含一个流,该流具有在通过 XmlSerializer 运行后位于 xml 元素中的原始 xml。

这可以吗?我可以选择只反序列化 xml 文档的“一部分”吗?

谢谢! -迈克

附言从How to Deserialize XML document窃取的示例xml

【问题讨论】:

【参考方案1】:

这可能是一个有点旧的线程,但无论如何我都会发布。我遇到了同样的问题(需要从一个超过 1MB 的文件中反序列化 10kb 的数据)。在主对象(它有一个需要反序列化器的 InnerObject)中,我实现了一个 IXmlSerializable 接口,然后更改了 ReadXml 方法。

我们有 xmlTextReader 作为输入,第一行是读取到一个 XML 标签:

reader.ReadToDescendant("InnerObjectTag"); //tag which matches the InnerObject

然后为我们要反序列化的对象类型创建 XMLSerializer 并对其进行反序列化

XmlSerializer   serializer = new XmlSerializer(typeof(InnerObject));

this.innerObject = serializer.Deserialize(reader.ReadSubtree()); //this gives serializer the part of XML that is for  the innerObject data

reader.close(); //now skip the rest 

这为我节省了大量反序列化的时间,并允许我只读取 XML 的一部分(只是描述文件的一些细节,这可能有助于用户确定文件是否是他想要加载的文件)。

【讨论】:

很好的解决方案,但是我发现我还需要设置片段的 xml 根以避免出现异常,内部异常说 ... xmlns=''> 不是预期的。由于评论长度的限制,我为我的解决方案添加了另一个答案。【参考方案2】:

来自 user271807 的接受 answer 是一个很好的解决方案,但我发现,我还需要设置片段的 xml 根以避免异常,内部异常如下所示:

...xmlns=''> was not expected

当我尝试仅反序列化此 xml 文档的内部 Authentication 元素时,出现此异常:

<?xml version=""1.0"" encoding=""UTF-8""?>
<Api>
  <Authentication>                       
      <sessionid>xxx</sessionid>
      <errormessage>xxx</errormessage>                
  </Authentication>
</ApI>

所以我最终创建了这个扩展方法作为可重用的解决方案- 警告包含内存泄漏,见下文:

public static T DeserializeXml<T>(this string @this, string innerStartTag = null)
        
            using (var stringReader = new StringReader(@this))
            using (var xmlReader = XmlReader.Create(stringReader)) 
                if (innerStartTag != null) 
                    xmlReader.ReadToDescendant(innerStartTag);
                    var xmlSerializer = new XmlSerializer(typeof(T), new XmlRootAttribute(innerStartTag));
                    return (T)xmlSerializer.Deserialize(xmlReader.ReadSubtree());
                
                return (T)new XmlSerializer(typeof(T)).Deserialize(xmlReader);
            
        

2017 年 3 月 20 日更新:正如下面的评论所指出的,在使用 XmlSerializer 的构造函数之一时存在内存泄漏问题,因此我最终使用了如下所示的缓存解决方案:

    /// <summary>
    ///     Deserialize XML string, optionally only an inner fragment of the XML, as specified by the innerStartTag parameter.
    /// </summary>
    public static T DeserializeXml<T>(this string @this, string innerStartTag = null) 
        using (var stringReader = new StringReader(@this)) 
            using (var xmlReader = XmlReader.Create(stringReader)) 
                if (innerStartTag != null) 
                    xmlReader.ReadToDescendant(innerStartTag);
                    var xmlSerializer = CachingXmlSerializerFactory.Create(typeof (T), new XmlRootAttribute(innerStartTag));
                    return (T) xmlSerializer.Deserialize(xmlReader.ReadSubtree());
                
                return (T) CachingXmlSerializerFactory.Create(typeof (T), new XmlRootAttribute("AutochartistAPI")).Deserialize(xmlReader);
            
        
    
/// <summary>
///     A caching factory to avoid memory leaks in the XmlSerializer class.
/// See http://dotnetcodebox.blogspot.dk/2013/01/xmlserializer-class-may-result-in.html
/// </summary>
public static class CachingXmlSerializerFactory 
    private static readonly ConcurrentDictionary<string, XmlSerializer> Cache = new ConcurrentDictionary<string, XmlSerializer>();
    public static XmlSerializer Create(Type type, XmlRootAttribute root) 
        if (type == null) 
            throw new ArgumentNullException(nameof(type));
        
        if (root == null) 
            throw new ArgumentNullException(nameof(root));
        
        var key = string.Format(CultureInfo.InvariantCulture, "0:1", type, root.ElementName);
        return Cache.GetOrAdd(key, _ => new XmlSerializer(type, root));
    
    public static XmlSerializer Create<T>(XmlRootAttribute root) 
        return Create(typeof (T), root);
    
    public static XmlSerializer Create<T>() 
        return Create(typeof (T));
    
    public static XmlSerializer Create<T>(string defaultNamespace) 
        return Create(typeof (T), defaultNamespace);
    
    public static XmlSerializer Create(Type type) 
        return new XmlSerializer(type);
    
    public static XmlSerializer Create(Type type, string defaultNamespace) 
        return new XmlSerializer(type, defaultNamespace);
    

【讨论】:

我正在研究一个类似的问题,我发现你的问题和this blog post 关于使用构造函数 XmlSerializer(Type, XmlRootAttribute) 时的内存泄漏。你需要检查你的代码。我认为您的方法每次调用时都会创建一个新的临时程序集。您可能必须为每个 Type+innerStartTag 组合执行手动缓存。 是的,谢谢你提醒我。我已通过修复更新了我的答案。【参考方案3】:

您可以通过在类中实现 ISerializable 接口来控制序列化的完成方式。请注意,这也意味着具有方法签名(SerializationInfo 信息,StreamingContext 上下文)的构造函数,并确保您可以按照您的要求进行操作。

但是,请仔细查看您是否真的需要使用流式传输来执行此操作,因为如果您不必使用流式传输机制,那么使用 Linq to XML 实现相同的功能会更容易,并且更易于维护从长远来看(国际海事组织)

【讨论】:

【参考方案4】:

我认为前面的评论者在他的评论中是正确的,即 XML 可能不是这里后备存储的最佳选择。

如果您遇到规模问题并且没有利用 XML 的其他一些优点,例如转换,您最好使用数据库来存储您的数据。您所做的操作似乎更适合该模型。

我知道这并不能真正回答您的问题,但我想我会强调您可能会使用的替代解决方案。一个好的数据库和适当的 OR 映射器(如 .netTiers、NHibernate 或最近的 LINQ to SQL / Entity Framework)可能会让您在对代码库的其余部分进行最小更改的情况下恢复并运行。

【讨论】:

他可能只是 esb 上的消费者。所以他不能更改他的数据存储。读取 xmls 的部分是合法的过程。使用低级 xmlreader 可以索引文件/流和直接搜索/跳转到文档中的任何位置。【参考方案5】:

通常,XML 反序列化是一个开箱即用的全有或全无的命题,因此您可能需要自定义。如果您不进行完全反序列化,您将面临 xml 在 SalesPerson 元素中格式错误的风险,因此文档无效。

如果您愿意接受这种风险,您可能需要进行一些基本的文本解析,以使用纯文本处理工具将 SalesPerson 元素分解为不同的文档,然后处理 XML。

这是一个很好的例子,说明了为什么 XML 并不总是正确的答案。

【讨论】:

【参考方案6】:

请尝试将 SalesPerson 属性定义为类型 XmlElement。这适用于使用 XML 序列化的 ASMX Web 服务的输出。我认为它也适用于输入。我希望整个 &lt;SalesPerson&gt; 元素最终出现在 XmlElement 中。

【讨论】:

他们可能还需要该成员的 XmlAnyAttribute。 实际上我可能弄错了,因为看起来 XmlAny 是一个返回 XmlElements array 的属性,而不仅仅是一个。 我只是更仔细地重新阅读了描述,看起来 XmlAnyElement 和 XmlAnyAttribute 是用于切片的。它们是 XSD 找不到位置的东西的包罗万象。 我不是在谈论XmlElementAttribute。我说的是System.Xml.XmlElement【参考方案7】:

您可以控制 Cars 类的哪些部分被反序列化,方法是在 Cars 类上实现 IXmlSerializable 接口,然后在您将阅读的 ReadXml(XmlReader) 方法中并反序列化 Car 元素,但是当您到达 SalesPerson 元素时,您会将其子树作为字符串读取,然后使用 StreamWriter 在文本内容上构造一个 Stream。

如果您不希望 XmlSerializer 写出 SalesPerson 元素,请使用 [XmlIgnore] 属性。当您将 Cars 类序列化为其 XML 表示时,我不确定您想要发生什么。您是否试图仅阻止 SalesPerson 的反序列化,同时仍然能够序列化由 Stream 表示的 SalesPerson 的 XML 表示?

如果你想要一个具体的实现,我可能会提供一个代码示例。

【讨论】:

【参考方案8】:

如果您只想解析 SalesPerson 元素但将其保留为字符串,则应使用 Xsl 转换而不是“反序列化”。另一方面,如果您想解析 SalesPerson 元素并仅从所有其他非 SalesPerson 元素填充内存中的对象,那么 Xsl Transform 也可能是要走的路。如果文件很大,您可以考虑将它们分开并使用 Xsl 组合不同的 xml 文件,以便仅在需要时才发生 SalesPerson I/O。

【讨论】:

用例是我想要的汽车数据作为对象,以便我的程序可以与之交互。 SalesPerson XML 只是通过线路发送到另一个系统,所以我什至不需要检查它。基本上,我需要获取所有数据,但只关心 Car 元素包含的内容。 如果是这种情况,那么您所要做的就是不提供 XmlElementAttributes 来序列化非汽车数据。【参考方案9】:

我建议您使用任何轻量级方法(如 XmlReader、XPathDocument 或 LINQ-to-XML)手动读取 Xml。

当您只需要读取 3 个属性时,我想您可以编写从该节点手动读取的代码,并完全控制它的执行方式,而不是依赖于序列化/反序列化

【讨论】:

以上是关于如何在 C# 中仅反序列化 XML 文档的一部分的主要内容,如果未能解决你的问题,请参考以下文章

如何在 C# 中仅从 DateTime 序列化 Xml 日期

使用Boost将派生类部分反序列化为基类时输入流错误

如何将 XML 反序列化为 C# 中的对象? [复制]

如何反序列化 C# 对象中复杂的 XML 格式并读取它以使用其值?

将 xml 反序列化为 c# 对象时,XML 文档 (2, 2) 出现错误

C# XML 反序列化为一张表中的 DataSet