合并两个 XML 文件并添加缺少的标签和属性

Posted

技术标签:

【中文标题】合并两个 XML 文件并添加缺少的标签和属性【英文标题】:Merge two XML files and add missing tags and attributes 【发布时间】:2018-09-04 03:15:26 【问题描述】:

我有两个基本格式相同的 XML 文件,但 Master.XML 中的一些标签和属性不包含在 Child.XML 中。

我需要将 XML 文件合并到一个新的 XML 文件中,其中缺少标签和属性。

如果 Master.XML 和 Child.XML 中的值不同,则应使用 Child.XML 中的值。

我尝试将 Union 和 Concat 与节点一起使用,但它不起作用。

  master.DescendantNodes().Union(child.DescendantNodes());  

任何建议都会有所帮助。

Master.XML

<SysConfig IsRuntime="False" BarcodeEnabled="false" version="1.2.0.0">
    <DbPath>C:\Agilent_i1000\ICPT_DB.sqlite</DbPath> 
 <CardDiagonsticsDelayTime>10</CardDiagonsticsDelayTime>   
    <ScreenSpecs NameID="CoreID" XrelativeID="X" YrelativeID="Y">
        <ScreenSpec Name="MainCtrlPanel" Xrelative="0" Yrelative="0" ></ScreenSpec>
        <ScreenSpec Name="1" Xrelative="75" Yrelative="0"  NotToUse="1"></ScreenSpec>
        <ScreenSpec Name="2" Xrelative="75" Yrelative="25"  NotToUse="1"></ScreenSpec>        
    </ScreenSpecs>  
</SysConfig>

Child.XML

<SysConfig IsRuntime="False" BarcodeEnabled="false" version="1.2.0.0">
<CardDiagonsticsDelayTime>20</CardDiagonsticsDelayTime>   
       <ScreenSpecs NameID="CoreID" XrelativeID="X" YrelativeID="Y">
        <ScreenSpec Name="MainCtrlPanel" Xrelative="0" Yrelative="0" ></ScreenSpec>
        <ScreenSpec Name="1" Xrelative="100" Yrelative="0" ></ScreenSpec>
        <ScreenSpec Name="2" Xrelative="75" Yrelative="25"></ScreenSpec> 
        <ScreenSpec Name="3" Xrelative="175" Yrelative="25"></ScreenSpec>        
    </ScreenSpecs>  
</SysConfig>

预期输出

    <SysConfig IsRuntime="False" BarcodeEnabled="false" version="1.2.0.0">
    <DbPath>C:\Agilent_i1000\ICPT_DB.sqlite</DbPath> 
    <CardDiagonsticsDelayTime>20</CardDiagonsticsDelayTime>   
           <ScreenSpecs NameID="CoreID" XrelativeID="X" YrelativeID="Y">
            <ScreenSpec Name="MainCtrlPanel" Xrelative="0" Yrelative="0" ></ScreenSpec>
            <ScreenSpec Name="1" Xrelative="100" Yrelative="0" NotToUse="1" ></ScreenSpec>
            <ScreenSpec Name="2" Xrelative="75" Yrelative="25" NotToUse="1"></ScreenSpec>  
            <ScreenSpec Name="3" Xrelative="175" Yrelative="25">
</ScreenSpec>      
        </ScreenSpecs>  
    </SysConfig>

【问题讨论】:

XSLT 或许可以为您解决这个问题。 会有单主多子场景吗? 【参考方案1】:

这是一组能够合并两个 XML 文档的实用程序类。整个内容非常通用,但您可能需要对其进行修改以满足其他特定需求。

这是处理您的案例的代码:

    var result = new XmlXPathDocument();
    result.Load("master.xml");

    var child = new XmlXPathDocument();
    child.Load("child.xml");

    // here we tell the class that "Name" is a discriminant attribute.
    // two nodes at the same level with the same discriminant attributes are considered the same, so will be merged
    // if we don't do this, they will considered different and they will be added if the set of their attributes is different
    child.AddDiscriminantAttribute("Name", string.Empty);
    child.InjectXml(result);

    result.Save("output.xml");

这些类派生自标准 XmlDocument 类层次结构。他们使用自动计算的 XPATH 表达式来逐个节点地合并文档。

public class XmlXPathDocument : XmlDocument

    public const string XmlNamespaceUri = "http://www.w3.org/2000/xmlns/";
    public const string XmlNamespacePrefix = "xmlns";

    internal List<Tuple<string, string>> _discriminantAttributes = new List<Tuple<string, string>>();

    public XmlXPathDocument() => Construct();
    public XmlXPathDocument(XmlNameTable nameTable) : base(nameTable) => Construct();
    public XmlXPathDocument(XmlImplementation implementation) : base(implementation) => Construct();

    protected virtual void Construct() => XPathNamespaceManager = new XmlNamespaceManager(new NameTable());

    public virtual XmlNamespaceManager XPathNamespaceManager  get; private set; 

    public override XmlElement CreateElement(string prefix, string localName, string namespaceURI) => new XmlXPathElement(prefix, localName, namespaceURI, this);

    public override XmlCDataSection CreateCDataSection(string data) => new XmlXPathCDataSection(data, this);

    public override XmlText CreateTextNode(string text) => new XmlXPathText(text, this);

    public virtual void AddDiscriminantAttribute(string name, string namespaceURI)
    
        if (name == null)
            throw new ArgumentNullException(nameof(name));

        _discriminantAttributes.Add(new Tuple<string, string>(name, namespaceURI));
    

    public virtual bool IsDiscriminant(XmlAttribute attribute)
    
        if (attribute == null)
            throw new ArgumentNullException(nameof(attribute));

        foreach (var pair in _discriminantAttributes)
        
            string ns = Nullify(attribute.NamespaceURI);
            string dns = Nullify(pair.Item2);
            if (ns == dns && pair.Item1 == attribute.LocalName)
                return true;
        
        return false;
    

    private static string Nullify(string text)
    
        if (text == null)
            return null;

        text = text.Trim();
        if (text.Length == 0)
            return null;

        return text;
    

    internal string GetPrefix(string namespaceURI)
    
        if (string.IsNullOrEmpty(namespaceURI))
            return null;

        string prefix = XPathNamespaceManager.LookupPrefix(namespaceURI);
        if (!string.IsNullOrEmpty(prefix))
        
            XPathNamespaceManager.AddNamespace(prefix, namespaceURI);
            return prefix;
        

        string newPrefix;
        int index = 0;
        do
        
            newPrefix = "ns" + index;
            if (XPathNamespaceManager.LookupNamespace(newPrefix) == null)
                break;

            index++;
        
        while (true);
        XPathNamespaceManager.AddNamespace(newPrefix, namespaceURI);
        return newPrefix;
    

    private static bool IsNamespaceAttribute(XmlAttribute attribute)
    
        if (attribute == null)
            return false;

        return attribute.NamespaceURI == XmlNamespaceUri && attribute.Prefix == XmlNamespacePrefix;
    

    private static IEnumerable<XmlAttribute> GetAttributes(IXmlXPathNode node)
    
        var xe = node as XmlElement;
        if (xe == null)
            yield break;

        foreach (XmlAttribute att in xe.Attributes)
        
            yield return att;
        
    

    private static XmlAttribute GetAttribute(IXmlXPathNode node, string name) => node is XmlElement xe ? xe.Attributes[name] : null;
    private static XmlAttribute GetAttribute(IXmlXPathNode node, string localName, string ns) => node is XmlElement xe ? xe.Attributes[localName, ns] : null;

    public virtual bool InjectXml(XmlDocument target)
    
        if (target == null)
            throw new ArgumentNullException(nameof(target));

        if (DocumentElement == null)
            return false;

        bool changed = false;
        foreach (XmlNode node in SelectNodes("//node()"))
        
            var xelement = node as IXmlXPathNode;
            if (xelement == null)
                continue;

            if (string.IsNullOrEmpty(xelement.XPathExpression))
                continue;

            XmlNode other = target.SelectSingleNode(xelement.XPathExpression, XPathNamespaceManager);
            if (other != null)
            
                if (other is XmlElement otherElement)
                
                    foreach (XmlAttribute att in GetAttributes(xelement))
                    
                        if (IsNamespaceAttribute(att))
                            continue;

                        if (otherElement.Attributes[att.LocalName, att.NamespaceURI]?.Value != att.Value)
                        
                            otherElement.SetAttribute(att.LocalName, att.NamespaceURI, att.Value);
                            changed = true;
                        
                    
                    continue;
                
            

            if (node is XmlXPathElement element)
            
                XmlElement parent = EnsureTargetParent(xelement, target, out changed);
                XmlElement targetElement = target.CreateElement(element.LocalName, element.NamespaceURI);
                changed = true;
                if (parent == null)
                
                    target.AppendChild(targetElement);
                
                else
                
                    parent.AppendChild(targetElement);
                

                foreach (XmlAttribute att in GetAttributes(xelement))
                
                    if (IsNamespaceAttribute(att))
                        continue;

                    targetElement.SetAttribute(att.LocalName, att.NamespaceURI, att.Value);
                
                continue;
            

            if (node is XmlXPathCDataSection cdata)
            
                XmlElement parent = EnsureTargetParent(xelement, target, out changed);
                var targetCData = target.CreateCDataSection(cdata.Value);
                changed = true;
                if (parent == null)
                
                    target.AppendChild(targetCData);
                    AppendNextTexts(node, targetCData, target);
                
                else
                
                    if (parent.ChildNodes.Count == 1 && parent.ChildNodes[0] is XmlCharacterData)
                    
                        parent.RemoveChild(parent.ChildNodes[0]);
                    
                    parent.AppendChild(targetCData);
                    AppendNextTexts(node, targetCData, parent);
                
                continue;
            

            if (node is XmlXPathText text)
            
                XmlElement parent = EnsureTargetParent(xelement, target, out changed);
                var targetText = target.CreateTextNode(text.Value);
                changed = true;
                if (parent == null)
                
                    target.AppendChild(targetText);
                    AppendNextTexts(node, targetText, target);
                
                else
                
                    if (parent.ChildNodes.Count == 1 && parent.ChildNodes[0] is XmlCharacterData)
                    
                        parent.RemoveChild(parent.ChildNodes[0]);
                    
                    parent.AppendChild(targetText);
                    AppendNextTexts(node, targetText, parent);
                
                continue;
            
        
        return changed;
    

    private static void AppendNextTexts(XmlNode textNode, XmlNode targetTextNode, XmlNode parent)
    
        do
        
            if (textNode.NextSibling is XmlText text)
            
                var newText = targetTextNode.OwnerDocument.CreateTextNode(text.Value);
                parent.AppendChild(newText);
            
            else
            
                var cdata = textNode.NextSibling as XmlCDataSection;
                if (cdata == null)
                    break;

                var newCData = targetTextNode.OwnerDocument.CreateCDataSection(cdata.Value);
                parent.AppendChild(newCData);
            
            textNode = textNode.NextSibling;
        
        while (true);
    

    private static XmlElement EnsureTargetParent(IXmlXPathNode element, XmlDocument target, out bool changed)
    
        changed = false;
        if (element.ParentNode is XmlXPathElement parent)
        
            if (string.IsNullOrEmpty(parent.XPathExpression))
                return null;

            if (target.SelectSingleNode(parent.XPathExpression, element.OwnerDocument.XPathNamespaceManager) is XmlElement targetElement)
                return targetElement;

            var parentElement = EnsureTargetParent(parent, target, out changed);
            targetElement = target.CreateElement(parent.LocalName, parent.NamespaceURI);
            parentElement.AppendChild(targetElement);
            changed = true;
            return targetElement;
        
        return target.DocumentElement;
    


public class XmlXPathElement : XmlElement, IXmlXPathNode

    private Lazy<string> _xPathExpression;

    public XmlXPathElement(string prefix, string localName, string namespaceURI, XmlXPathDocument doc) : base(prefix, localName, namespaceURI, doc)
    
        _xPathExpression = new Lazy<string>(() => GetXPathExpression());
    

    public new XmlXPathDocument OwnerDocument => (XmlXPathDocument)base.OwnerDocument;
    public virtual string XPathExpression => _xPathExpression.Value;

    private static string GetAttEscapedValue(string value)
    
        if (value.IndexOf('\'') >= 0)
            return "=\"" + value.Replace("\"", "&quot;") + "\"";

        return "='" + value + "'";
    

    private string GetDiscriminantAttributeXPath()
    
        foreach (var att in OwnerDocument._discriminantAttributes)
        
            XmlAttribute disc;
            if (string.IsNullOrEmpty(att.Item2))
            
                disc = GetAttributeNode(att.Item1);
            
            else
            
                disc = GetAttributeNode(att.Item1, att.Item2);
            

            if (disc != null)
            
                string newPrefix = OwnerDocument.GetPrefix(NamespaceURI);
                string name = Name + "[@" + disc.Name + GetAttEscapedValue(disc.Value) + "]";
                if (newPrefix != null)
                
                    name = newPrefix + ":" + name;
                
                return name;
            
        
        return null;
    

    private string GetAttributesXPath()
    
        if (Attributes.Count == 0)
            return null;

        var sb = new StringBuilder();
        foreach (XmlAttribute att in Attributes)
        
            if (sb.Length > 0)
            
                sb.Append(" and ");
            

            sb.Append("@");
            sb.Append(att.Name);
            sb.Append(GetAttEscapedValue(att.Value));
            OwnerDocument.GetPrefix(att.NamespaceURI);
        

        var text = sb.ToString().Trim();
        if (text.Length == 0)
            return null;

        return "[" + text + "]";
    

    private string GetXPath(XmlNodeList parentNodes)
    
        string discriminant = GetDiscriminantAttributeXPath();
        if (discriminant != null)
            return discriminant;

        string name = Name;
        string newPrefix = OwnerDocument.GetPrefix(NamespaceURI);
        if (newPrefix != null)
        
            name = newPrefix + ":" + LocalName;
        

        if (parentNodes.Count == 1)
            return name;

        var sameName = new List<XmlElement>();
        foreach (XmlNode node in parentNodes)
        
            if (node.NodeType != XmlNodeType.Element)
                continue;

            if (node.Name == Name)
            
                sameName.Add((XmlElement)node);
            
        

        if (sameName.Count == 1)
            return name;

        string byIndex = null;
        var sameAtts = new List<XmlElement>();
        for (int i = 0; i < sameName.Count; i++)
        
            if (sameName[i] == this)
            
                byIndex = name + "[" + (i + 1) + "]";
                continue;
            

            bool same = true;
            foreach (XmlAttribute att in Attributes)
            
                XmlAttribute sameAtt = sameName[i].Attributes[att.LocalName, att.NamespaceURI];
                if (sameAtt == null || string.Compare(sameAtt.Value, att.Value, StringComparison.OrdinalIgnoreCase) != 0)
                
                    same = false;
                    break;
                
            

            if (same)
            
                sameAtts.Add(sameName[i]);
            
        

        if (sameAtts.Count == 0)
            return name + GetAttributesXPath();

        return byIndex;
    

    private string GetXPathExpression()
    
        if (ParentNode == null)
        
            string name = Name;
            string newPrefix = OwnerDocument.GetPrefix(NamespaceURI);
            if (newPrefix != null)
            
                name = newPrefix + ":" + name;
            
            return name;
        

        string expr = GetXPath(ParentNode.ChildNodes);
        if (ParentNode is XmlXPathElement parent)
        
            expr = parent.XPathExpression + "/" + expr;
        

        if (ParentNode.NodeType == XmlNodeType.Document)
        
            expr = "/" + expr;
        
        return expr;
    


public class XmlXPathText : XmlText, IXmlXPathNode

    private Lazy<string> _xPathExpression;

    public XmlXPathText(string data, XmlXPathDocument doc) : base(data, doc)
    
        _xPathExpression = new Lazy<string>(() => GetTextXPathExpression(this));
    

    public new XmlXPathDocument OwnerDocument => (XmlXPathDocument)base.OwnerDocument;
    public virtual string XPathExpression => _xPathExpression.Value;

    internal static string GetTextXPathExpression(XmlNode node)
    
        if (node.ParentNode is IXmlXPathNode element)
            return element.XPathExpression + "/text()";

        return null;
    


public class XmlXPathCDataSection : XmlCDataSection, IXmlXPathNode

    private Lazy<string> _xPathExpression;

    public XmlXPathCDataSection(string data, XmlXPathDocument doc) : base(data, doc)
    
        _xPathExpression = new Lazy<string>(() => XmlXPathText.GetTextXPathExpression(this));
    

    public new XmlXPathDocument OwnerDocument => (XmlXPathDocument)base.OwnerDocument;
    public virtual string XPathExpression => _xPathExpression.Value;


public interface IXmlXPathNode

    string XPathExpression  get; 
    XmlNode ParentNode  get; 
    XmlXPathDocument OwnerDocument  get; 

【讨论】:

看到您的 ScreenSpecs 元素具有 NameID 属性,我相信您的文档中可能有多个这些节点。如果是这种情况,您还需要为 NameID 添加另一个判别属性,因为这将使您的 ScreenSpec 节点通过其 NameID 保持唯一。 @TwistedStem - 我(无意中)把它作为练习留给读者:-)。是的,确实,添加 NameID 似乎是个好主意,我只是测试了输出,没有深入了解它的语义。【参考方案2】:

试试 xml linq:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;

namespace ConsoleApplication1

    class Program
    
        const string MASTER_FILENAME = @"c:\temp\test.xml";
        const string CHILD_FILENAME = @"c:\temp\test1.xml";
        static void Main(string[] args)
        
            XDocument master = XDocument.Load(MASTER_FILENAME);
            XElement masterSysConfig = master.Descendants("SysConfig").FirstOrDefault();
            masterSysConfig.Nodes().Remove();

            XDocument child = XDocument.Load(CHILD_FILENAME);
            var children = child.Nodes().Cast<XElement>().Select(x => x).FirstOrDefault();
            masterSysConfig.Add(children.Elements());

        
    

【讨论】:

评论不用于扩展讨论;这个对话是moved to chat。

以上是关于合并两个 XML 文件并添加缺少的标签和属性的主要内容,如果未能解决你的问题,请参考以下文章

Android:权限缺少名称和标签

LOAD XML 命令导致缺少字段/节点

连接/加入/合并两个缺少一列的数据框

ant过滤 - 如果未设置属性,则失败

“在 AndroidManifest.xml:11:5-64 的元素包上缺少‘包’关键属性”

R文件缺少android