news 2026/7/21 17:30:03

开源完美模块组件化可扩展的Xml解析器Hand.ParseXml

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
开源完美模块组件化可扩展的Xml解析器Hand.ParseXml

一、首先对比Xml反序列化

  1. 性能对比
    1.1 性能测试结果如下
    Deserialize是反序列化结果
    GetResult是基于反射的对象构建方法,比反序列化快33%
    GetResult2是定制对象构建组件,避免了反射,比GetResult快一点
    Custom继承重写EntityParser,避免字典查找,比GetResult2更快一点
    Method Mean Error StdDev Ratio Gen0 Gen1 Allocated Alloc Ratio
    Deserialize 1,355.5 ns 4.44 ns 4.75 ns 1.00 0.7240 0.0300 12.2 KB 1.00
    GetResult 912.7 ns 5.07 ns 5.84 ns 0.67 0.6750 0.0250 11.38 KB 0.93
    GetResult2 798.3 ns 0.40 ns 0.42 ns 0.59 0.6500 - 10.95 KB 0.90
    Custom 779.4 ns 6.46 ns 7.43 ns 0.57 0.6500 - 10.95 KB 0.90
    1.2 相关代码如下
    private static readonly XmlSerializer _serializer = new(typeof(User));
    private static readonly EntityParser _parser = HandXml.Default.Entity()
    .WithItem(nameof(User.Id))
    .WithItem(nameof(User.Name))
    .WithItem(nameof(User.Age));
    private static readonly EntityParser _parser2 = HandXml.Default.Entity(UserBuilder.Creater)
    .WithItem(nameof(User.Id))
    .WithItem(nameof(User.Name))
    .WithItem(nameof(User.Age));
    private static readonly UserParser _customParser = new(HandXml.Default);

    [Benchmark(Baseline = true)]
    public User? Deserialize()
    {
    using var stringReader = new StringReader(text);
    return (User?)_serializer.Deserialize(stringReader);
    }
    [Benchmark]
    public User GetResult()
    {
    return _parser.Get(text);
    }
    [Benchmark]
    public User GetResult2()
    {
    return _parser2.Get(text);
    }
    [Benchmark]
    public User Custom()
    {
    return _customParser.Get(text);
    }
    [Serializable]
    public class User
    {
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
    }
    ///
    /// 建造者模式
    ///
    public class UserBuilder2
    : IMemberBuilder
    {
    private readonly User _original = new();
    ///
    /// 原始对象
    ///
    public User Original
    => _original;
    ///
    public User Build()
    => _original;
    ///
    public void Save(string name, TMember value)
    {
    switch (name)
    {
    case nameof(User.Id):
    if (value is int idValue)
    _original.Id = idValue;
    break;
    case nameof(User.Name):
    if (value is string nameValue)
    _original.Name = nameValue;
    break;
    case nameof(User.Age):
    if (value is int ageValue)
    _original.Age = ageValue;
    break;
    default:
    break;
    }
    }
    ///
    /// 工厂模式
    ///
    public static readonly ICreator Creater = new DefaultCreater();
    }
    public class UserParser(HandXml xml)
    : EntityParser(xml, UserBuilder2.Creater, null, true)
    {
    #region 配置
    private readonly IXmlParser _id = xml.Content();
    private readonly ContentReader _name = xml.Content();
    private readonly IXmlParser _age = xml.Content();
    #endregion
    ///
    public override void ReadAttributes(IMemberStore entity, XmlReader reader) { }
    ///
    public override void ReadItem(IMemberBuilder entity, XmlReader reader, string name)
    {
    if (entity is UserBuilder2 builder)
    ReadItem(builder.Original, reader, name);
    else
    base.ReadItem(entity, reader, name);
    }
    ///
    /// 使用自定义构造器
    ///
    ///
    ///
    ///
    public void ReadItem(User entity, XmlReader reader, string name)
    {
    switch (name)
    {
    case nameof(User.Id):
    if (_id.TryParse(reader, out var idResult))
    entity.Id = idResult;
    break;
    case nameof(User.Name):
    if (_name.TryParse(reader, out var nameResult))
    entity.Name = nameResult;
    break;
    case nameof(User.Age):
    if (_age.TryParse(reader, out var ageResult))
    entity.Age = ageResult;
    break;
    }
    }
    }
    1.3 性能分析
    ParseXml开箱即用性能就比XmlSerializer好不少
    通过定制对象构建组件或扩展重写能进一步提高性能
    定制对象构建组件和扩展重写解析器可以通过SG来生成,之后会开发相应的源码生成器

  2. 通用性对比
    2.1 XmlSerializer通过Attribute标记映射
    通过XmlRoot、XmlAttribute、XmlElement和XmlText等来映射属性
    一个类只能支持一种格式的xml文件
    [XmlRoot(“Root”)]
    public class Person
    {
    [XmlAttribute(“PersonId”)]
    public int Id { get; set; }
    [XmlElement(“PersonName”)]
    public string Name { get; set; }

    private static readonly XmlSerializer _serializer = new(typeof(Person));

    [Fact]
    public void Deserialize()
    {
    var text = @“<?xml version=""1.0"" encoding=""utf-8""?>
    <Root PersonId=”“1"”>
    张三
    “;
    using var stringReader = new StringReader(text);
    Person? person = _serializer.Deserialize(stringReader) as Person;
    Assert.NotNull(person);
    Assert.Equal(1, person.Id);
    Assert.Equal(“张三”, person.Name);
    }
    [Fact]
    public void Throw()
    {
    var text = @”<?xml version=""1.0"" encoding=""utf-8""?>

    1
    张三
    “;
    using var stringReader = new StringReader(text);
    Assert.ThrowsAny(() => _serializer.Deserialize(stringReader));
    }
    }
    2.2 ParseXml通过Fluent API配置
    WithItem配置节点
    WithAttribute配置属性
    Entity支持可选参数配置当前标签映射
    映射的成员可以是属性也可以是字段和构造函数的参数
    配置的解析Xml对象可以定义为静态或注入IOC容器,该对象解析Xml不产生状态支持多线程同时调用(线程安全)
    同一个类可以配置多个解析对象用来把不同格式的Xml转化为相同类型的对象
    public record User(int Id, string Name);
    .
    [Fact]
    public void WithNode()
    {
    var id = 123;
    var name = “Jxj”;
    var text = @$”<?xml version=""1.0"" encoding=""utf-8""?>

    {id}
    {name}
    ";

    var userParser = HandXml.Default.Entity<User>() .WithItem<int>(nameof(User.Id)) .WithItem(nameof(User.Name)); User result = userParser.Get(text); Assert.NotNull(result); Assert.Equal(id, result.Id); Assert.Equal(name, result.Name);

    }
    [Fact]
    public void WithAttribute()
    {
    var id = 123;
    var name = “Jxj”;
    var text = @$“<?xml version=""1.0"" encoding=""utf-8""?>
    <User Id=”“{id}”" Name=“”{name}“” />";

    var userParser = HandXml.Default.Entity<User>() .WithAttribute<int>(nameof(User.Id)) .WithAttribute(nameof(User.Name)) .First("User"); User result = userParser.Parse(text); Assert.NotNull(result); Assert.Equal(id, result.Id); Assert.Equal(name, result.Name);

    }
    [Fact]
    public void Content()
    {
    var id = 123;
    var name = “Jxj”;
    var text = @$“<?xml version=""1.0"" encoding=""utf-8""?>
    <User Id=”“{id}”“>{name}”;

    var userParser = HandXml.Default.Entity<User>(nameof(User.Name)) .WithAttribute<int>(nameof(User.Id)) .First("User"); User result = userParser.Parse(text); Assert.NotNull(result); Assert.Equal(id, result.Id); Assert.Equal(name, result.Name);

    }
    2.3 通用性分析
    特性 XmlSerializer ParseXml
    同类Xml格式 只支持一种格式 支持多种
    映射对象 只映射属性 映射属性、字段和构造函数参数
    映射Xml结构 支持属性、子节点和当前节点 支持属性、子节点和当前节点
    结果类型 object需要转化 当前类型,直接使用
    二、ParseXml主要功能

  3. 读取单个节点
    1.1 读取原始值
    使用First方法读取单个节点原始值
    var summaryReader = HandXml.Default.First(“summary”);
    string summary = summaryReader.Parse(xmlReader);
    1.2 读取强类型值
    使用First泛型方法读取单个节点强类型值
    var idParser = HandXml.Default.First(“Id”);
    int id = idParser.Parse(xmlReader);

  4. 读取单个属性
    2.1 读取原始值
    使用Attribute方法读取属性原始值
    支持使用属性名或属性索引
    var text = @"<member name = ““F:GenerateConvertTests.Supports.ColumnType.Identity””>

    自增列

";

var nameReader = HandXml.Default.Attribute(“name”)
.First();
string name = nameReader.Parse(text);
2.2 读取强类型值
使用Attribute泛型方法读取属性强类型值
支持使用属性名或属性索引
var text = @$“<?xml version=""1.0"" encoding=""utf-8""?>
<User Id=”“123"”>Jxj";

var idReader = HandXml.Default.Attribute(0)
.Element(“User”)
.First();
int result = idReader.Parse(text);
3. 读取单个节点名
使用Name方法读取节点名
var text = @“<?xml version=""1.0"" encoding=""utf-8""?>

1
”;
var eachParser = HandXml.Default.Name()
.First();
var result = eachParser.Parse(text);
4. 解析到实体
xml反序列化
4.1 从子节点解析
Entity泛型方法指定实体类型
WithItem方法读取子节点原始值并绑到实体
WithItem泛型方法读取子节点强类型值并绑到实体
var text = @“<?xml version=""1.0"" encoding=""utf-8""?>

123
Jxj
20
”;

// Id、Name和Age节点转化为实体User的属性
var userParser = HandXml.Default.Entity()
.WithItem(nameof(User.Id))
.WithItem(nameof(User.Name))
.WithItem(nameof(User.Age));
User result = userParser.Get(text);
4.2 从属性解析
Entity泛型方法指定实体类型
WithAttribute方法读取属性原始值并绑到实体
WithAttribute泛型方法读取属性强类型值并绑到实体
var text = @$“<?xml version=""1.0"" encoding=""utf-8""?>
<User Id=”“123"” Name=““Jxj”” Age=““20"” />”;

// 第一个User节点的属性转化为实体User的属性
var userParser = HandXml.Default.Entity()
.WithAttribute(nameof(User.Id))
.WithAttribute(nameof(User.Name))
.WithAttribute(nameof(User.Age))
.Element(nameof(User))
.First();
User result = userParser.Parse(text);
4.3 从本节点解析原始值
Entity可选参数contentName指定本节点原始值绑定实体的成员名
var text = @$“<?xml version=""1.0"" encoding=""utf-8""?>
<User Id=”“123"” Age=““age””>Jxj";

var config = HandXml.Default;
// 第一个User节点的属性转化为实体User的属性,User节点的文本转化为实体User的Name
var userParser = config.Entity(nameof(User.Name))
.WithAttribute(nameof(User.Id))
.WithAttribute(nameof(User.Age))
.Element(nameof(User))
.First();
User result = userParser.Parse(text);
5. 解析集合
通过Each方法定义集合解析
var text = @“<?xml version=""1.0"" encoding=""utf-8""?>


1
张三


2
李四


3
<![CDATA[王二]]>

”;

var config = HandXml.Default;
// 每一个User节点转化为实体User
var repeatReader = config.Entity()
.WithItem(nameof(User.Id))
.WithItem(nameof(User.Name))
.Element(nameof(User))
.Each();
User[] result = repeatReader.Get(text)
.ToArray();
6. 解析字典
6.1 把列表解析为字典
使用Dictionary扩展方法解析字典,acceptDefault可选参数配置是否包含默认值
先定义键的解析规则
以子节点Id为键,子节点Name为值
var text = @“<?xml version=""1.0"" encoding=""utf-8""?>


1
张三


2


3
<![CDATA[王二]]>

”;
var config = HandXml.Default;
var dictionaryReader = config.Element(nameof(User.Id))
.Dictionary(config.Element(“Name”).First());
IDictionary<int,string> result = dictionaryReader.Get(text);
6.2 把子节点解析给字典
通过Name获取节点名
使用Dictionary扩展方法解析字典,acceptDefault可选参数配置是否包含默认值
var text = @“<?xml version=""1.0"" encoding=""utf-8""?>

1
张三
10
”;
var config = HandXml.Default;
var dictionaryReader = config.Name()
.Dictionary(config.Content().First())
// 跳过当前User节点
.MoveIn()
.Element(“User”)
.First();
IDictionary<string,string> result = dictionaryReader.Parse(text);
7. 包装类型转化
Convert方法转入一个转化器可以变成另一个类型的解析器
var text = @$“<?xml version=""1.0"" encoding=""utf-8""?>
<User Id=”“123"”>Jxj";

// 第一个User节点的Id属性转化为UserId
var idReader = HandXml.Default.Attribute(“Id”)
.Element(nameof(User))
.First()
.Convert(id => new UserId(id));
UserId result = idReader.Parse(text);
8. 复杂类型
通过WithItem可以一个解析器绑定到主解析器的成员上
通过WithEache可以把集合解析器绑定到主解析器的成员上
8.1 复杂类型的Case
using var fs = new FileStream(“rss.xml”, FileMode.Open, FileAccess.Read);
using var xmlReader = XmlReader.Create(fs);
var config = HandXml.Default;
// url和title节点分别转化为RssImage实体的Url和Title
var imageParser = config.Entity()
.WithItem(“url”, nameof(RssImage.Url))
.WithItem(“title”, nameof(RssImage.Title));
// 每一个item节点的title和link分别转化为RssItem实体的Title和Link
var itemParser = config.Entity()
.WithItem(“title”, nameof(RssItem.Title))
.WithItem(“link”, nameof(RssItem.Link))
.Element(“item”)
.Each();
var rssParser = config.Entity()
// title节点转化为Rss实体的Title
.WithItem(“title”, nameof(Rss.Title))
// image节点交给imageParser处理,处理的结果转化为Rss实体的Image
.WithItem(imageParser, “image”, nameof(Rss.Image))
// item节点交给itemParser处理,处理的结果转化为Rss实体的Items
.With(itemParser, “item”, nameof(Rss.Items));
Rss rss = rssParser.Get(xmlReader);
8.2 支持绑定子解析器的WithItem重载方法
item为子解析器
node为调用该解析器的子节点名
member为子解析器解析结果绑定实体成员名
TItem为子解析器解析结果和实体成员共同的类型
如果类型不一致结果会抛异常或被抛弃
EntityParser WithItem(IParser<XmlReader, TItem> item, string node, string member);
三、ParseXml的模块化

  1. HandXml是ParseXml的配置基座
    HandXml包含Builders、Converters和DefaultValues
    Builders、Converters和DefaultValues参与到解析Xml的各个过程内
    通过Builders、Converters和DefaultValues配置可以调整Xml解析的结果和性能
    class HandXml
    {
    ///
    /// 构造器提供者
    ///
    IMemberBuilderProvider Builders { get; }
    ///
    /// 转化器
    ///
    XmlConvertCacher Converters { get; }
    ///
    /// 默认值提供者
    ///
    DefaultValueBuilder DefaultValues { get; }
    }
    1.1 Builders负责对象构建
    默认使用反射构建
    支持配置
    ///
    /// 注册工厂
    ///
    ///
    ///
    ///
    ///
    ///
    TProvider UseCreator<TProvider, TEntity>(this TProvider provider, ICreator<IMemberBuilder> creator);
    ///
    /// 注册工厂
    ///
    ///
    ///
    ///
    ///
    ///
    TProvider UseCreator<TProvider, TEntity>(this TProvider provider, Func<IMemberBuilder> func);
    1.2 Converters负责xml节点原始处理
    默认使用XmlConvert处理
    默认支持18种基础类型(bool、byte、short、char、sbyte、ushort、int、uint、long、ulong、float、double、decimal、DateTime、string、Guid、DateTimeOffset和TimeSpan等)
    支持配置
    ///
    /// 配置转化器
    ///
    ///
    ///
    ///
    HandXml Use<TValue, TConverter>(TConverter converter);
    ///
    /// 配置转化器
    ///
    ///
    ///
    HandXml Use(Converter<string, TValue> converter);
    1.3 DefaultValues负责默认值
    各类型的默认值默认是default
    支持配置
    ///
    /// 设置默认值
    ///
    ///
    ///
    ///
    DefaultValueBuilder Use(TValue value);
    四、ParseXml的组件化
  2. Xml解析接口IXmlParser
    对Xml单节点、属性、重复节点或者复杂节点的解析类都实现了IXmlParser
    ///
    /// Xml解析接口
    ///
    ///
    interface IXmlParser
    {
    ///
    /// 解析
    ///
    ///
    ///
    ///
    bool TryParse(XmlReader reader, out TResult result);
    }
  3. IXmlParser的主要实现类
    AttributeReader和ContentReader是基础实现
    其他类都是通过组装IXmlParser实例来实现
    通过完美的组件化可以模拟和解析复杂的Xml文件
    类 作用
    AttributeReader 读取属性的原始文本
    ContentReader 读取节点的原始文本
    PrimitiveReader 调用AttributeReader或ContentReader,使用Converters把文本转化为需要的类型
    ConvertParser<TSource, TDest> 把一种类型的解析器封装为另一种类型的解析器
    FirstReader 解析碰到的第一个该节点
    EachReader 解析连续重复的该节点
    DictionaryParser<TKey, TValue> 解析为字典
    EntityParser 解析包含多个属性或子节点为实体类型的成员
    class PrimitiveReader(IXmlParser original, IConverter<string, TPrimitive> converter, TPrimitive defaultValue);
    class ConvertParser<TSource, TDest>(HandXml xml, IXmlParser original, IConverter<TSource, TDest> converter, TDest defaultValue);
    class FirstReader(string element, IXmlParser original, TResult defaultValue);
    class RepeatReader(HandXml xml, string name, IXmlParser item);
    class DictionaryParser<TKey, TValue>(IParser<XmlReader, TKey> key, IParser<XmlReader, TValue> value, IEqualityComparer comparer, bool acceptDefault);
    class EntityParser(HandXml xml, ICreator<IMemberBuilder> creator, IMemberParser? content, bool hasItem = false);
    五、ParseXml的可扩展性
  4. EntityParser的扩展性
    EntityParser可以用来处理复杂Xml和复杂类型
    可扩展性在处理复杂问题时可简化配置或提供性能
  5. UserParser扩展EntityParser的示例
    UserParser封装了对象解析器,提高性能
    UserParser封装了节点配置,简化使用
    重写ReadAttributes跳过属性解析提高性能
    重写ReadItem,使用switch代替字典查找提高性能
    public class UserParser(HandXml xml)
    : EntityParser(xml, UserBuilder2.Creater, null, true)
    {
    #region 配置
    private readonly IXmlParser _id = xml.Content();
    private readonly ContentReader _name = xml.Content();
    private readonly IXmlParser _age = xml.Content();
    #endregion
    ///
    public override void ReadAttributes(IMemberStore entity, XmlReader reader) { }
    ///
    public override void ReadItem(IMemberBuilder entity, XmlReader reader, string name)
    {
    if (entity is UserBuilder2 builder)
    ReadItem(builder.Original, reader, name);
    else
    base.ReadItem(entity, reader, name);
    }
    ///
    /// 使用自定义构造器
    ///
    ///
    ///
    ///
    public void ReadItem(User entity, XmlReader reader, string name)
    {
    switch (name)
    {
    case nameof(User.Id):
    if (_id.TryParse(reader, out var idResult))
    entity.Id = idResult;
    break;
    case nameof(User.Name):
    if (_name.TryParse(reader, out var nameResult))
    entity.Name = nameResult;
    break;
    case nameof(User.Age):
    if (_age.TryParse(reader, out var ageResult))
    entity.Age = ageResult;
    break;
    }
    }
    }
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/21 17:28:12

C语言动态内存管理:从malloc/free原理到实战避坑指南

1. 先搞清楚“动态内存管理”到底要解决什么问题很多人一看到malloc、free就头疼&#xff0c;觉得是 C 语言里最复杂、最容易出错的部分。其实它的核心目标就一个&#xff1a;在程序运行时&#xff0c;按需申请和释放内存。这和你提前定义好int arr[100]有本质区别。arr[100]在…

作者头像 李华
网站建设 2026/7/21 17:29:31

python实习面试基础题

面试日期&#xff1a;2026-07-19 考察方向&#xff1a;Python 基础&#xff08;数据结构、语言特性&#xff09; 难度等级&#xff1a;⭐ 基础必问 一、列表&#xff08;list&#xff09;和元组&#xff08;tuple&#xff09;的区别 核心答案 对比维度列表&#xff08;list&…

作者头像 李华
网站建设 2026/7/21 17:28:20

电商返利平台稳定性评判标准:从接口、分佣、服务器架构解读

电商返利平台稳定性评判标准&#xff1a;从接口、分佣、服务器架构解读 大家好&#xff0c;我是省赚客APP研发者微赚淘客&#xff01; 在返利行业摸爬滚打多年&#xff0c;我见过太多平台因为技术短板而昙花一现。用户最关心的“返利不到账”、“优惠券失效”、“APP卡顿”等问…

作者头像 李华
网站建设 2026/7/20 15:38:22

财务报表数据零散难分析?一体化财务报表可视化平台怎么搭建

多数企业财务报表长期存在数据零散、割裂孤立的痛点&#xff1a;资产负债、利润、现金流量分散在ERP总账&#xff0c;应收、存货、费用、预算数据分别存于WMS、CRM、OA费控多套系统&#xff0c;历史年度数据散落大量Excel台账。财务每月需要跨系统导出十几份表格手工拼接、核对…

作者头像 李华
网站建设 2026/7/20 15:35:46

终极赛博朋克2077存档修改指南:CyberpunkSaveEditor完整使用教程

终极赛博朋克2077存档修改指南&#xff1a;CyberpunkSaveEditor完整使用教程 【免费下载链接】CyberpunkSaveEditor A tool to edit Cyberpunk 2077 sav.dat files 项目地址: https://gitcode.com/gh_mirrors/cy/CyberpunkSaveEditor 你是否厌倦了《赛博朋克2077》中某些…

作者头像 李华