C#将xml属性转换为元素

本文关键字:元素 转换 属性 xml | 更新日期: 2023-09-27 18:00:41

我需要将所有属性转换为XML文件中的节点,根节点中的属性除外。

我在这里发现了一个类似的问题:xquery将属性转换为标记,但我需要在C#中进行转换。

我还在这里找到了一个使用XLS的可能解决方案:将属性值转换为元素。但是,该解决方案实质上将节点名称更改为属性名称并删除该属性。

我需要使用属性的名称和值创建新的同级节点,并删除这些属性,但仍然保留包含这些属性的节点。

给定以下XML:

<Something xmlns="http://www.something.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.xomething.com segments.xsd">
  <Version>4.0.8</Version>
  <Segments>
    <Segment Name="Test">
      <SegmentField>
        <SegmentIndex>0</SegmentIndex>
        <Name>RecordTypeID</Name>
        <Value Source="Literal">O</Value>
      </SegmentField>
      <SegmentField>
        <SegmentIndex>1</SegmentIndex>
        <Name>OrderSequenceNumber</Name>
        <Value Source="Calculated" Initial="1">Sequence</Value>
      </SegmentField>
      <SegmentField>
        <SegmentIndex>3</SegmentIndex>
        <Name>InstrumentSpecimenID</Name>
        <Value Source="Property">BarCode</Value>
      </SegmentField>
    </Segment>
  </Segments>
</Something>

我需要生成以下XML:

<Something xmlns="http://www.something.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.xomething.com segments.xsd">
  <Version>4.0.8</Version>
  <Segments>
    <Segment>
      <Name>Test</Name>
      <SegmentField>
        <SegmentIndex>0</SegmentIndex>
        <Name>RecordTypeID</Name>
        <Value>O</Value>
        <Source>Literal</Source>
      </SegmentField>
      <SegmentField>
        <SegmentIndex>1</SegmentIndex>
        <Name>OrderSequenceNumber</Name>
        <Value>Sequence</Value>
        <Source>Calculated</Source>
        <Initial>1</Initial>
      </SegmentField>
      <SegmentField>
        <SegmentIndex>3</SegmentIndex>
        <Name>InstrumentSpecimenID</Name>
        <Value>BarCode</Value>
        <Source>Property</Source>
      </SegmentField>
    </Segment>
  </Segments>
</Something>

我已经编写了以下方法来创建一个新的XML对象,从source元素的属性创建新元素:

private static XElement ConvertAttribToElement(XElement source)
{
    var result = new XElement(source.Name.LocalName);
    if (source.HasElements)
    {
        foreach (var element in source.Elements())
        {
            var orphan = ConvertAttribToElement(element);
            result.Add(orphan);
        }
    }
    else
    {
        result.Value = source.Value.Trim();
    }
    if (source.Parent == null)
    {
        // ERROR: The prefix '' cannot be redefined from '' to 'http://www.something.com' within the same start element tag.
        //foreach (var attrib in source.Attributes())
        //{
        //    result.SetAttributeValue(attrib.Name.LocalName, attrib.Value);
        //}
    }
    else
    {
        while (source.HasAttributes)
        {
            var attrib = source.LastAttribute;
            result.AddFirst(new XElement(attrib.Name.LocalName, attrib.Value.Trim()));
            attrib.Remove();
        }
    }
    return result;
}

此方法生成以下XML:

<Something>
  <Version>4.0.8</Version>
  <Segments>
    <Segment>
      <Name>Test</Name>
      <SegmentField>
        <SegmentIndex>0</SegmentIndex>
        <Name>RecordTypeID</Name>
        <Value>
          <Source>Literal</Source>O</Value>
      </SegmentField>
      <SegmentField>
        <SegmentIndex>1</SegmentIndex>
        <Name>OrderSequenceNumber</Name>
        <Value>
          <Source>Calculated</Source>
          <Initial>1</Initial>Sequence</Value>
      </SegmentField>
      <SegmentField>
        <SegmentIndex>3</SegmentIndex>
        <Name>InstrumentSpecimenID</Name>
        <Value>
          <Source>Property</Source>BarCode</Value>
      </SegmentField>
    </Segment>
  </Segments>
</Something>

输出存在两个直接问题:
1) 根元素中的属性将丢失
2) "Value"元素中的属性被创建为子元素,而不是兄弟元素。

为了解决第一个问题,我尝试将source元素的属性分配给result元素,但这导致"前缀"无法从"重新定义为"http://www.something.com'在同一个起始元素标记内"错误。我注释掉了导致错误的代码以供说明。

为了解决第二个问题,我尝试将根据属性创建的元素添加到source.Parent元素中,但这导致新元素根本没有出现。

我还重写了直接在source元素上操作的方法:

private static void ConvertAttribToElement2(XElement source)
{
    if (source.HasElements)
    {
        foreach (var element in source.Elements())
        {
            ConvertAttribToElement2(element);
        }
    }
    if (source.Parent != null)
    {
        while (source.HasAttributes)
        {
            var attrib = source.LastAttribute;
            source.Parent.AddFirst(new XElement(attrib.Name.LocalName, attrib.Value.Trim()));
            attrib.Remove();
        }
    }
}

重写产生了以下XML:

<Something xmlns="http://www.something.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.xomething.com segments.xsd">
  <Version>4.0.8</Version>
  <Segments>
    <Name xmlns="">Test</Name>
    <Segment>
      <SegmentField>
        <Source xmlns="">Literal</Source>
        <SegmentIndex>0</SegmentIndex>
        <Name>RecordTypeID</Name>
        <Value>O</Value>
      </SegmentField>
      <SegmentField>
        <Source xmlns="">Calculated</Source>
        <Initial xmlns="">1</Initial>
        <SegmentIndex>1</SegmentIndex>
        <Name>OrderSequenceNumber</Name>
        <Value>Sequence</Value>
      </SegmentField>
      <SegmentField>
        <Source xmlns="">Property</Source>
        <SegmentIndex>3</SegmentIndex>
        <Name>InstrumentSpecimenID</Name>
        <Value>BarCode</Value>
      </SegmentField>
    </Segment>
  </Segments>
</Something>

重写确实解决了保留根元素属性的第一个问题。它还部分解决了第二个问题,但产生了一个新问题:新元素有一个空白的xmlns属性。

C#将xml属性转换为元素

使用此方法将Xml属性转换为Xml节点:

public static void ReplaceAttributesByNodes(XmlDocument document, XmlNode node)
{
    if (document == null)
    {
        throw new ArgumentNullException("document");
    }
    if (node == null)
    {
        throw new ArgumentNullException("node");
    }
    if (node.HasChildNodes)
    {
        foreach (XmlNode tempNode in node.ChildNodes)
        {
            ReplaceAttributesByNodes(document, tempNode);
        }
    }
    if (node.Attributes != null)
    {
        foreach (XmlAttribute attribute in node.Attributes)
        {
            XmlNode element = document.CreateNode(XmlNodeType.Element, attribute.Name, null);
            element.InnerText = attribute.InnerText;
            node.AppendChild(element);
        }
        node.Attributes.RemoveAll();
    }
}

//how to use it
static void Main()
{
    string eventNodeXPath = "Something/Segments/Segment";//your segments nodes only
    XmlDocument document = new XmlDocument();
    document.Load(@"your playlist file full path");//your input playlist file
    XmlNodeList nodes = document.SelectNodes(eventNodeXPath);
    if (nodes != null)
    {
        foreach (XmlNode node in nodes)
        {
            ReplaceAttributesByNodes(document, node);
        }
    }
    doc.Save("your output file full path");
}

此XSLT转换

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:x="http://www.something.com">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:strip-space elements="*"/>
 <xsl:variable name="vNamespace" select="namespace-uri(/*)"/>
 <xsl:template match="node()|@*">
  <xsl:copy>
   <xsl:apply-templates select="node()|@*"/>
  </xsl:copy>
 </xsl:template>
 <xsl:template match="*/*/@*">
  <xsl:element name="{name()}" namespace="{$vNamespace}">
   <xsl:value-of select="."/>
  </xsl:element>
 </xsl:template>
 <xsl:template match="x:Value">
  <xsl:copy>
   <xsl:apply-templates/>
  </xsl:copy>
  <xsl:apply-templates select="@*"/>
 </xsl:template>
</xsl:stylesheet>

应用于所提供的XML文档时

<Something xmlns="http://www.something.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.xomething.com segments.xsd">
    <Version>4.0.8</Version>
    <Segments>
        <Segment Name="Test">
            <SegmentField>
                <SegmentIndex>0</SegmentIndex>
                <Name>RecordTypeID</Name>
                <Value Source="Literal">O</Value>
            </SegmentField>
            <SegmentField>
                <SegmentIndex>1</SegmentIndex>
                <Name>OrderSequenceNumber</Name>
                <Value Source="Calculated" Initial="1">Sequence</Value>
            </SegmentField>
            <SegmentField>
                <SegmentIndex>3</SegmentIndex>
                <Name>InstrumentSpecimenID</Name>
                <Value Source="Property">BarCode</Value>
            </SegmentField>
        </Segment>
    </Segments>
</Something>

生成所需的正确结果

<Something xmlns="http://www.something.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.xomething.com segments.xsd">
   <Version>4.0.8</Version>
   <Segments>
      <Segment>
         <Name>Test</Name>
         <SegmentField>
            <SegmentIndex>0</SegmentIndex>
            <Name>RecordTypeID</Name>
            <Value>O</Value>
            <Source>Literal</Source>
         </SegmentField>
         <SegmentField>
            <SegmentIndex>1</SegmentIndex>
            <Name>OrderSequenceNumber</Name>
            <Value>Sequence</Value>
            <Source>Calculated</Source>
            <Initial>1</Initial>
         </SegmentField>
         <SegmentField>
            <SegmentIndex>3</SegmentIndex>
            <Name>InstrumentSpecimenID</Name>
            <Value>BarCode</Value>
            <Source>Property</Source>
         </SegmentField>
      </Segment>
   </Segments>
</Something>

解释

  1. 标识规则/模板"按原样"复制每个节点。

  2. 标识规则由两个模板覆盖——一个匹配任何不是文档顶部元素的元素的任何属性,另一个匹配任意Value元素。

  3. 模板匹配属性(第一个覆盖模板)创建一个与匹配属性具有相同本地名称和值的元素来代替属性。此外,元素名称与文档顶部元素所属的名称空间相同(这避免了xmlns="")。

  4. 匹配任何Value元素的模板复制它并处理它的所有子树(派生节点),然后处理它的属性。通过这种方式,从属性生成的元素成为Value元素的兄弟元素,而不是子元素。

您可以构建一个扩展方法来压平每个元素:

public static IEnumerable<XElement> Flatten(this XElement element)
{
    // first return ourselves
    yield return new XElement(
        element.Name,
        // Output our text if we have no elements
        !element.HasElements ? element.Value : null,
        // Or the flattened sequence of our children if they exist
        element.Elements().SelectMany(el => el.Flatten()));
    // Then return our own attributes (that aren't xmlns related)
    foreach (var attribute in element.Attributes()
                                     .Where(aa => !aa.IsNamespaceDeclaration))
    {
        // check if the attribute has a namespace,
        // if not we "borrow" our element's
        var isNone = attribute.Name.Namespace == XNamespace.None;
        yield return new XElement(
            !isNone ? attribute.Name
                    : element.Name.Namespace + attribute.Name.LocalName,
            attribute.Value);
    }
}

你会像这样使用:

public static XElement Flatten(this XDocument document)
{
    // used to fix the naming of the namespaces
    var ns = document.Root.Attributes()
                          .Where(aa => aa.IsNamespaceDeclaration
                                    && aa.Name.LocalName != "xmlns")
                          .Select(aa => new { aa.Name.LocalName, aa.Value });
    return new XElement(
        document.Root.Name,
        // preserve "specific" xml namespaces
        ns.Select(n => new XAttribute(XNamespace.Xmlns + n.LocalName, n.Value)),
        // place root attributes right after the root element
        document.Root.Attributes()
                     .Where(aa => !aa.IsNamespaceDeclaration)
                     .Select(aa => new XAttribute(aa.Name, aa.Value)),
        // then flatten our children
        document.Root.Elements().SelectMany(el => el.Flatten()));
}

这会产生您已经指出的输出,除了xsi:schemaLocation属性,正如我发现的那样,这是有问题的。它选择了一个默认的名称空间名称(p1),但最终它可以工作。

生成以下内容:

<Something xmlns="http://www.something.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.xomething.com segments.xsd">
  <Version>4.0.8</Version>
  <Segments>
    <Segment>
      <SegmentField>
        <SegmentIndex>0</SegmentIndex>
        <Name>RecordTypeID</Name>
        <Value>O</Value>
        <Source>Literal</Source>
      </SegmentField>
      <SegmentField>
        <SegmentIndex>1</SegmentIndex>
        <Name>OrderSequenceNumber</Name>
        <Value>Sequence</Value>
        <Source>Calculated</Source>
        <Initial>1</Initial>
      </SegmentField>
      <SegmentField>
        <SegmentIndex>3</SegmentIndex>
        <Name>InstrumentSpecimenID</Name>
        <Value>BarCode</Value>
        <Source>Property</Source>
      </SegmentField>
    </Segment>
    <Name>Test</Name>
  </Segments>
</Something>