【加精】C# XML差异对比 (直接用)

C# XML差异对比

直接可用,使用灵活

csharp 复制代码
    public class XmlDiffer
    {
        #region private object
        private readonly XmlDocument sourceDoc = new XmlDocument();
        private readonly XmlDocument destinationDoc = new XmlDocument();
        private readonly StringBuilder report = new StringBuilder();
        #endregion

        #region constructor
        private readonly bool compareAttributes;
        private readonly bool compareContent;
        private readonly bool compareOrder;
        private readonly bool caseSensitive;
        private readonly HashSet<string> ignoredPaths;
        private readonly List<DiffRecord> diffRecords = new List<DiffRecord>();
        private readonly List<DiffTypeSummary> diffSummary = new List<DiffTypeSummary>();
        #endregion
			         #region public object
        public enum DiffType
        {
            ELEMENT_NAME_MISMATCH,
            ELEMENT_NAME_CASE_MISMATCH,
            CONTENT_MISMATCH,
            CHILD_ORDER_MISMATCH,
            MISSING_ATTRIBUTE,
            ATTRIBUTE_VALUE_MISMATCH,
            MISSING_ELEMENT
        }
        public enum ReportFormat
        {
            TXT,
            XML,
            JSON
        }
        public class CompareResult
        {
            //public string UniqueInfo { get; set; }
            public bool CompareFlag
            {
                get
                {
                    return Summary == null ? false : (Summary.Count == 0 ? true : false);
                }
            }
            public List<DiffTypeSummary> Summary { get; set; }
            public List<DiffRecord> Diff { get; set; }
        }
        public class DiffTypeSummary
        {
            public string DiffType { get; set; }
            public int Count { get; set; }
        }
        public class DiffRecord
        {
            public string Path { get; set; }
            public string DiffType { get; set; }
            public string Expected { get; set; }
            public string Actual { get; set; }
            public string Suggestion { get; set; }
        }

        public ReportFormat reportFormat { get; set; } = ReportFormat.TXT;
        public CompareResult compareResult { get; set; }
        #endregion

        #region Public Function
        /// <summary>
        /// 
        /// </summary>
        /// <param name="sourceXml">基准XML</param>
        /// <param name="destinationXml">与基准xml进行对比的xml</param>
        /// <param name="compareAttributes">比较属性</param>
        /// <param name="compareContent">比较内容</param>
        /// <param name="compareOrder">比较顺序</param>
        /// <param name="caseSensitive">大小写比较</param>
        /// <param name="ignoredPaths">需要忽略的指定路径</param>
        /// <param name="reportFormat">报告格式TXT,XML、Json)</param>
        /// <param name="isFilePath">string or xml file path</param>
        public XmlDiffer(string sourceXml, string destinationXml, bool compareAttributes = false, bool compareContent = false, bool compareOrder = false, bool caseSensitive = true, List<string> ignoredPaths = null, bool isFilePath = false)
        {
            if (isFilePath)
            {
                sourceDoc.Load(sourceXml);
                destinationDoc.Load(destinationXml);
            }
            else
            {
                sourceDoc.LoadXml(sourceXml);
                destinationDoc.LoadXml(destinationXml);
            }

            this.compareAttributes = compareAttributes;
            this.compareContent = compareContent;
            this.compareOrder = compareOrder;
            this.ignoredPaths = ignoredPaths != null ? new HashSet<string>(ignoredPaths) : new HashSet<string>();
            this.reportFormat = reportFormat;
            this.caseSensitive = caseSensitive;
        }

        public string Compare()
        {
            CompareNodes(sourceDoc.DocumentElement, destinationDoc.DocumentElement, sourceDoc.DocumentElement.Name);
            AppendStats();
            GetResultModel();
            return GetReportString(reportFormat);
        }
        #endregion

        #region Generate Report
        public string GetReportString(ReportFormat outFormat = ReportFormat.TXT)
        {
            switch (outFormat)
            {
                case ReportFormat.JSON:
                    return GenerateJsonReport();
                case ReportFormat.XML:
                    return GenerateXmlReport();
                case ReportFormat.TXT:
                default:
                    return report.ToString();
            }
        }
        private void GetResultModel()
        {
            CompareResult result = new CompareResult();
            result.Summary = diffSummary;
            result.Diff = diffRecords;

            compareResult = result;
        }
        private string GenerateJsonReport()
        {
            return Newtonsoft.Json.JsonConvert.SerializeObject(compareResult, Newtonsoft.Json.Formatting.Indented);
        }
        private string GenerateXmlReport()
        {
            var serializer = new XmlSerializer(typeof(CompareResult));

            using (var writer = new StringWriter())
            {
                serializer.Serialize(writer, compareResult);
                return writer.ToString();
            }
        }
        public void SaveReport(string filePath, ReportFormat outFormat = ReportFormat.TXT)
        {
            var content = GetReportString(outFormat);
            File.WriteAllText(filePath, content, Encoding.UTF8);
        }

        #endregion
			         #region Compare XML
        private void CompareNodes(XmlNode sourceNode, XmlNode destNode, string path)
        {
            if (ignoredPaths.Contains(path)) return;

            if (sourceNode != null && destNode != null)
            {
                if (!string.Equals(sourceNode.Name, destNode.Name, caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase))
                {
                    AppendMismatch(path, DiffType.ELEMENT_NAME_MISMATCH, sourceNode.Name, destNode.Name, $"缺失节点 {sourceNode.Name}");
                }

                if (compareAttributes)
                {
                    CompareAttributes(sourceNode, destNode, path);
                }

                // ✅ 只在叶子节点时比较文本内容
                if (compareContent && NodeHasChildNodes(sourceNode) == false && NodeHasChildNodes(destNode) == false)
                {
                    string sourceText = sourceNode.InnerText.Trim();
                    string destText = destNode.InnerText.Trim();
                    if (!string.IsNullOrEmpty(sourceText) || !string.IsNullOrEmpty(destText))
                    {
                        if (sourceText != destText)
                        {
                            AppendMismatch(path, DiffType.CONTENT_MISMATCH, sourceText, destText, "请检查文本内容差异或修改文本内容为期望值");
                        }
                    }
                }

                var sourceChildren = GetChildElements(sourceNode);
                var destChildren = GetChildElements(destNode);

                if (compareOrder && !IsSameOrder(sourceChildren, destChildren))
                {
                    AppendMismatch(path, DiffType.CHILD_ORDER_MISMATCH, "", "顺序不同", "检查子节点顺序");
                }

                var allKeys = new HashSet<string>(sourceChildren.Keys);
                allKeys.UnionWith(destChildren.Keys);

                var processedKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase); // 用于记录已处理大小写不一致的 key

                foreach (var key in allKeys.ToList()) // 使用 ToList() 避免枚举修改异常
                {
                    if (processedKeys.Contains(key)) continue;

                    sourceChildren.TryGetValue(key, out var sList);
                    destChildren.TryGetValue(key, out var dList);

                    int maxCount = Math.Max(sList?.Count ?? 0, dList?.Count ?? 0);
                    for (int i = 0; i < maxCount; i++)
                    {
                        XmlNode sChild = i < (sList?.Count ?? 0) ? sList[i] : null;
                        XmlNode dChild = i < (dList?.Count ?? 0) ? dList[i] : null;

                        string newPath = GetUniquePath(sChild ?? dChild, i, path);

                        if (ignoredPaths.Contains(newPath)) continue;

                        if (sChild != null && dChild != null)
                        {
                            CompareNodes(sChild, dChild, newPath);
                        }
                        else if (sChild != null)
                        {
                            if (caseSensitive && TryMatchCaseInsensitive(key, destChildren, i, out var dCaseMismatch))
                            {
                                AppendMismatch(newPath, DiffType.ELEMENT_NAME_CASE_MISMATCH, key, dCaseMismatch.Name, "目标 XML 中节点名称大小写不一致,请统一大小写");

                                // 标记已处理,避免重复
                                processedKeys.Add(key);
                                processedKeys.Add(dCaseMismatch.Name);
                                continue;
                            }

                            AppendMissingElement(newPath, DiffType.MISSING_ELEMENT, sChild);
                        }
                        else if (dChild != null)
                        {
                            if (caseSensitive && TryMatchCaseInsensitive(key, sourceChildren, i, out var sCaseMismatch))
                            {
                                AppendMismatch(newPath, DiffType.ELEMENT_NAME_CASE_MISMATCH, sCaseMismatch.Name, key, "目标 XML 中节点名称大小写不一致,请统一大小写");

                                // 标记已处理,避免重复
                                processedKeys.Add(key);
                                processedKeys.Add(sCaseMismatch.Name);
                                continue;
                            }

                            AppendMismatch(newPath, DiffType.ELEMENT_NAME_MISMATCH, "", dChild.Name, "目标 XML 中存在多余节点,请确认是否需要删除");
                        }
                    }
                }
            }
            else if (sourceNode != null)
            {
                AppendMissingElement(path, DiffType.MISSING_ELEMENT, sourceNode);
            }
            else
            {
                AppendMismatch(path, DiffType.ELEMENT_NAME_MISMATCH, "", destNode.Name, "请手动检查此节点");
            }
        }
        private void CompareAttributes(XmlNode sourceNode, XmlNode destNode, string path)
        {
            var sourceAttrs = sourceNode.Attributes;
            var destAttrs = destNode.Attributes;

            if (sourceAttrs == null) return;
            if (destNode.Name == "MsgBus")
            {

            }
            foreach (XmlAttribute sourceAttr in sourceAttrs)
            {
                var destAttr = destAttrs?[sourceAttr.Name];
                if (destAttr == null)
                {
                    AppendMismatch(path, DiffType.MISSING_ATTRIBUTE, sourceAttr.Name, "", $"建议: 添加属性 {sourceAttr.Name}=\"{sourceAttr.Value}\"");
                }
                else if (sourceAttr.Value != destAttr.Value)
                {
                    AppendMismatch(path, DiffType.ATTRIBUTE_VALUE_MISMATCH, $"{sourceAttr.Name}=\"{sourceAttr.Value}\"", $"{destAttr.Name}=\"{destAttr.Value}\"", $"建议: 修改属性 {sourceAttr.Name} 为 \"{sourceAttr.Value}\"");
                }
            }
        }

        private Dictionary<string, List<XmlNode>> GetChildElements(XmlNode node)
        {
            var dict = new Dictionary<string, List<XmlNode>>();
            foreach (XmlNode child in node.ChildNodes)
            {
                if (child.NodeType == XmlNodeType.Element)
                {
                    string key = caseSensitive ? child.Name : child.Name.ToLower();
                    if (!dict.ContainsKey(key))
                        dict[key] = new List<XmlNode>();
                    dict[key].Add(child);
                }
            }
            return dict;
        }
        private bool NodeHasChildNodes(XmlNode node)
        {
            if (node.HasChildNodes && node.ChildNodes[0] != null && node.ChildNodes[0].Name != "#text")
            {
                return true;
            }
            else
            {
                return false;
            }
        }
        private bool IsSameOrder(Dictionary<string, List<XmlNode>> sourceChildren, Dictionary<string, List<XmlNode>> destChildren)
        {
            var sourceList = sourceChildren.SelectMany(kvp => kvp.Value.Select(n => caseSensitive ? n.Name : n.Name.ToLower())).ToList();
            var destList = destChildren.SelectMany(kvp => kvp.Value.Select(n => caseSensitive ? n.Name : n.Name.ToLower())).ToList();
            return sourceList.SequenceEqual(destList);
        }
        private string GetUniquePath(XmlNode node, int index, string parentPath)
        {
            string nodeName = caseSensitive ? node.Name : node.Name.ToLower();
            string attrInfo = node.Attributes?["id"]?.Value;
            string path = $"{parentPath}/{nodeName}[{index + 1}]";
            if (!string.IsNullOrEmpty(attrInfo))
                path += $"[@id='{attrInfo}']";
            return path;
        }
        private bool TryMatchCaseInsensitive(string key, Dictionary<string, List<XmlNode>> dict, int index, out XmlNode matchedNode)
        {
            matchedNode = null;
            var matchKey = dict.Keys.FirstOrDefault(k =>
                string.Equals(k, key, StringComparison.OrdinalIgnoreCase) &&
                !string.Equals(k, key, StringComparison.Ordinal));

            if (matchKey != null && dict[matchKey].Count > index)
            {
                matchedNode = dict[matchKey][index];
                return true;
            }
            return false;
        }
        #endregion

        #region Append difference
        private void AppendMismatch(string path, DiffType type, string expected, string actual, string suggestion)
        {
            AddDiffRecord(path, type, expected, actual, suggestion);
        }
        private void AppendMissingElement(string path, DiffType type, XmlNode missingNode)
        {
            string expected = missingNode.OuterXml;
            string actual = "Null";
            string suggestion = $"添加缺失元素: {expected}";

            AddDiffRecord(path, type, expected, actual, suggestion);
        }
        private void AppendStats()
        {
            report.AppendLine("📊 差异统计:");
            if (diffSummary.Count > 0)
            {
                report.AppendLine($"   ▸ 差异结果: 不匹配");
            }
            foreach (var kvp in diffSummary)
            {
                report.AppendLine($"   ▸ {kvp.DiffType}: {kvp.Count} 项");
            }
            report.AppendLine();
        }
        private void AddDiffRecord(string path, DiffType type, string expected, string actual, string suggestion)
        {
            // 文本报告
            report.AppendLine($"✓ [{path}] {type}");
            report.AppendLine($"   ▸ 期望: {expected}");
            report.AppendLine($"   ▸ 实际: {actual}");
            report.AppendLine($"   ▸ 建议: {suggestion}");
            report.AppendLine();

            // 统计
            if (diffSummary.Where(x => x.DiffType.Equals(type.ToString())).ToList().Count > 0)
            {
                diffSummary.Where(s => s.DiffType == type.ToString()).ToList().ForEach(s => s.Count = s.Count + 1);
            }
            else
            {
                diffSummary.Add(new DiffTypeSummary { DiffType = type.ToString(), Count = 1 });
            }
            //if (!diffSummary.ContainsKey(type)) diffSummary[type] = 0;
            //diffSummary[type]++;

            // 结构化记录
            diffRecords.Add(new DiffRecord
            {
                Path = path,
                DiffType = type.ToString(),
                Expected = expected,
                Actual = actual,
                Suggestion = suggestion
            });
        }
        #endregion
    }
相关推荐
刀一寸3 小时前
C# WebAPI下Swagger的配置
ui·c#
大飞pkz4 小时前
【算法】排序算法汇总1
开发语言·数据结构·算法·c#·排序算法
儒雅永缘4 小时前
VBA实现word文档批量转PDF文件
pdf·c#·word
zt1985q4 小时前
本地部署消息中间件 RabbitMQ 并实现外网访问 (Linux 版本)
linux·运维·服务器·windows·分布式·rabbitmq
ITHAOGE154 小时前
下载| Windows 11 ARM版10月官方ISO系统映像 (适合部分笔记本、苹果M系列芯片电脑、树莓派和部分安卓手机平板)
windows·科技·microsoft·电脑
聆风吟º5 小时前
Linux远程控制Windows桌面的cpolar实战指南
linux·运维·windows
love530love5 小时前
【笔记】Podman Desktop 部署 开源数字人 HeyGem.ai
人工智能·windows·笔记·python·容器·开源·podman
张人玉6 小时前
WPF 控件速查 PDF 笔记(可直接落地版)(带图片)
大数据·microsoft·ui·c#·wpf
葛小白16 小时前
Winform控件:Combobox
前端·ui·c#·combobox