代码之家  ›  专栏  ›  技术社区  ›  aybrady

C#XSLT快速转换大型XML文件

  •  2
  • aybrady  · 技术社区  · 6 年前

    我正在用XSLT中的查找模板转换一个>2GB文件。 我希望这个跑得更快,但找不到任何低挂的水果来提高性能。任何帮助都将不胜感激。 说到转变,我是个新手。

    这是XML文件的当前格式。

    <?xml version="1.0" encoding="utf-8" ?>
    <contacts>
        <contact>
            <attribute>
                <name>text12</name>
                <value>B00085590</value>
            </attribute>
            <attribute>
                <name>text34</name>
                <value>Atomos</value>
            </attribute>
            <attribute>
                <name>date866</name>
                <value>02/21/1991</value>
            </attribute>
        </contact>
        <contact>
            <attribute>
                <name>text12</name>
                <value>B00058478</value>
            </attribute>
            <attribute>
                <name>text34</name>
                <value>Balderas</value>
            </attribute>
            <attribute>
                <name>date866</name>
                <value>11/24/1997</value>
            </attribute>
        </contact>
    </contacts>
    

    用于转换的xslt。

    <?xml version="1.0" encoding="utf-8"?>
    <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
        xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl"
    >
        <xsl:output method="xml" indent="yes"/>
    
    
        <!--Identify location of the lookup xml-->
        <xsl:param name="lookupDoc" select="document('C:\Projects\Attributes.xml')" />  
    
        <!--Main Template-->
        <xsl:template match="/contacts">        
    
                <!--Apply Formatted Contacts Template-->
                <xsl:apply-templates select="contact" />            
    
        </xsl:template>
    
        <!--Formatted Contacts Template-->
        <xsl:template match="contact">
            <contact>
                <xsl:for-each select="attribute">
                    <!--Create variable to hold New Name after passing the Data Name to the Lookup Template-->
                    <xsl:variable name="newName">
                        <xsl:apply-templates select="$lookupDoc/attributes/attribute">
                            <xsl:with-param name="nameToMatch" select="name" />
                        </xsl:apply-templates>
                    </xsl:variable>     
                    <!--Format Contact Element with New Name variable-->
                    <xsl:element name="{$newName}">
                        <xsl:value-of select="value"/>
                    </xsl:element>          
                </xsl:for-each>
            </contact>
        </xsl:template>
    
        <!--Lookup Template-->
        <xsl:template match="attributes/attribute">
            <xsl:param name="nameToMatch" />            
                <xsl:value-of select='translate(translate(self::node()[name = $nameToMatch]/mappingname, "()*%$#@!~&lt;&gt;&apos;&amp;,.?[]=-+/\:1234567890", "")," ","")' />
            </xsl:template>
    
    
    </xsl:stylesheet>
    

    示例查找XML

    <?xml version="1.0" encoding="utf-8" ?>
    <attributes>
        <attribute>
            <name>text12</name>
            <mappingname>ID</mappingname>
            <datatype>Varchar2</datatype>
            <size>30</size>
        </attribute>
        <attribute>
            <name>text34</name>
            <mappingname>Last Name</mappingname>
            <datatype>Varchar2</datatype>
            <size>30</size>
        </attribute>
        <attribute>
            <name>date866</name>
            <mappingname>DOB</mappingname>
            <datatype>Date</datatype>
            <size></size>
        </attribute>
    </attributes>
    

    转换的XML

    <?xml version="1.0" encoding="utf-8" ?>
    <contacts>
        <contact>
            <ID>B00085590</ID>
            <LastName>Brady</LastName>
            <DOB>02/21/1991</DOB>
        </contact>
        <contact>
            <ID>B00058478</ID>
            <LastName>Balderas</LastName>
            <DOB>11/24/1997</DOB>
        </contact>
    </contacts>
    

    C类#

    XsltSettings settings = new XsltSettings(true, true);
    XslCompiledTransform ContactsXslt = new XslCompiledTransform();
    ContactsXslt.Load(@"C:\Projects\ContactFormat.xslt", settings, new XmlUrlResolver());
    
    using (XmlReader r = XmlReader.Create(@"C:\Projects\Contacts.xml")){
       using (XmlWriter w = XmlWriter.Create(@"C:\Projects\FormattedContacts.xml")) {
          w.WriteStartElement("contacts");
          while (r.Read()) {                        
             if (r.NodeType == XmlNodeType.Element && r.Name == "contact") {
                XmlReader temp = new XmlTextReader(new StringReader(r.ReadOuterXml()));                                
                ContactsXslt.Transform(temp, null, w);                            
             }
          }                        
       }
    }
    

    我采用的方法是一次转换一个节点,以避免OutOfMemoryException。我是不是应该把大块的食物喂进去以加快这个过程?还是说我做错了?

    3 回复  |  直到 6 年前
        1
  •  1
  •   Martin Honnen    6 年前

    我认为您可以简化XSLT代码

           <xsl:for-each select="attribute">
                <!--Create variable to hold New Name after passing the Data Name to the Lookup Template-->
                <xsl:variable name="newName">
                    <xsl:apply-templates select="$lookupDoc/attributes/attribute">
                        <xsl:with-param name="nameToMatch" select="name" />
                    </xsl:apply-templates>
                </xsl:variable> 
    

    使用模板

       <xsl:template match="attributes/attribute">
        <xsl:param name="nameToMatch" />            
            <xsl:value-of select='translate(translate(self::node()[name = $nameToMatch]/mappingname, "()*%$#@!~&lt;&gt;&apos;&amp;,.?[]=-+/\:1234567890", "")," ","")' />
        </xsl:template>
    

           <xsl:for-each select="attribute">
                <!--Create variable to hold New Name after passing the Data Name to the Lookup Template-->
                <xsl:variable name="newName">
                    <xsl:apply-templates select="$lookupDoc/attributes/attribute[name = current()/name]"/>
                </xsl:variable> 
    

    模板被简化为

       <xsl:template match="attributes/attribute">
            <xsl:value-of select='translate(translate(mappingname, "()*%$#@!~&lt;&gt;&apos;&amp;,.?[]=-+/\:1234567890", "")," ","")' />
        </xsl:template>
    

    我认为这无疑是一种更简洁和XSLT的方法,它是否提高了性能是您必须测试的。

    一般来说,对于XSLT,为了提高交叉引用/查找的性能,建议使用一个键,以便使用

    <xsl:key name="att-lookup" match="attributes/attribute" use="name"/>
    

    然后把它当作

                <xsl:variable name="name" select="name"/>
                <xsl:variable name="newName">
                    <!-- in XSLT 1 we need to change the context doc for the key lookup -->
                    <xsl:for-each select="$lookupDoc">
                       <xsl:apply-templates select="key('att-lookup', $name)"/>
                </xsl:variable> 
    

    我认为这将大大加快在单个转换中的查找速度,因为您将XmlReader和XSLT结合起来,在XmlReader发现的许多元素上多次运行XSLT我无法判断它是否有很大帮助,您需要尝试一下。

    正如在XSLT 3建议中指出的,我还将考虑首先转换查找文件,并转换一次,以避免重复所有这些 translate 调用以创建正确的XML元素名称。要么在现有XSLT之外执行该操作,要么使用变量在内部执行该操作,然后 exsl:node-set 将结果树片段转换为变量。但是在您反复运行XSLT的情况下,我认为最好首先在主XSLT之外转换查找文档,以避免不得不执行所有这些操作 翻译 一次又一次。

        2
  •  0
  •   jdweng    6 年前

    当读取巨大的xml文件时,总是使用XmlReader。我喜欢使用XmlReader和Xml linq的组合。我也喜欢用字典。请参见下面的代码:

    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Xml;
    using System.Xml.Linq;
    
    namespace ConsoleApplication1
    {
        class Program
        {
            const string FILENAME = @"c:\temp\test.xml";
            static void Main(string[] args)
            {
    
                XmlReader reader = XmlReader.Create(FILENAME);
                while (!reader.EOF)
                {
                    if (reader.Name != "contact")
                    {
                        reader.ReadToFollowing("contact");
                    }
                    if (!reader.EOF)
                    {
                        XElement xContact = (XElement)XElement.ReadFrom(reader);
                        Contact newContact = new Contact();
                        Contact.contacts.Add(newContact);
    
                        newContact.attributes = xContact.Descendants("attribute")
                            .GroupBy(x => (string)x.Element("name"), y => (string)y.Element("value"))
                            .ToDictionary(x => x.Key, y => y.FirstOrDefault());
                    }
                }
            }
        }
        public class Contact
        {
            public static List<Contact> contacts = new List<Contact>();
    
            public Dictionary<string, string> attributes { get; set; }
        }
     }
    
        3
  •  0
  •   Martin Honnen    6 年前

    作为替代方案,您可能希望研究如何使用XSLT 3及其流式处理功能解决该任务( https://www.w3.org/TR/xslt-30/#streaming-concepts )在那里,您可以只向前但声明性地处理巨大的输入文件,其中您只在 attribute 元素,您需要确保使用故意创建的该元素的完整副本,以允许对子元素进行XPath导航。另外,我认为在查找文档中只读取一次并执行 translate 只调用一次以创建正确的元素名称。因此,下面是一个使用Saxon 9.8ee运行的流式XSLT 3解决方案,它将查找文档转换为xpath3.1映射( https://www.w3.org/TR/xpath-31/#id-maps )或者使用可流模式处理大型主输入:

    <?xml version="1.0" encoding="UTF-8"?>
    <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
        xmlns:xs="http://www.w3.org/2001/XMLSchema"
        xmlns:map="http://www.w3.org/2005/xpath-functions/map"
        exclude-result-prefixes="xs map"
        version="3.0">
    
        <!-- could of course load the document using select="document('lookup.xml')" instead of inlining it as done here just for the example and testing -->
        <xsl:param name="lookup-doc">
            <attributes>
                <attribute>
                    <name>text12</name>
                    <mappingname>ID</mappingname>
                    <datatype>Varchar2</datatype>
                    <size>30</size>
                </attribute>
                <attribute>
                    <name>text34</name>
                    <mappingname>Last Name</mappingname>
                    <datatype>Varchar2</datatype>
                    <size>30</size>
                </attribute>
                <attribute>
                    <name>date866</name>
                    <mappingname>DOB</mappingname>
                    <datatype>Date</datatype>
                    <size></size>
                </attribute>
            </attributes>      
        </xsl:param>
    
        <xsl:variable 
            name="lookup-map"
            as="map(xs:string, xs:string)"
            select="map:merge(
            $lookup-doc/attributes/attribute 
            ! 
            map { 
            string(name) : translate(translate(mappingname, '()*%$#@!~&lt;&gt;''&amp;,.?[]=-+/\:1234567890', ''), ' ','')
            }
            )"/>
    
        <xsl:mode on-no-match="shallow-copy" streamable="yes"/>
    
        <xsl:output method="xml" indent="yes"/>
    
        <xsl:template match="contact/attribute">
            <xsl:variable name="attribute-copy" select="copy-of()"/>
            <xsl:element name="{$lookup-map($attribute-copy/name)}">
                <xsl:value-of select="$attribute-copy/value"/>
            </xsl:element>
        </xsl:template>
    
    </xsl:stylesheet>
    

    在线示例(在Saxon 9.8he中运行,它忽略了流并执行普通的XSLT处理)位于 https://xsltfiddle.liberty-development.net/bFDb2Ct/1 .

    使用Saxon 9.8和C运行流式XSLT 3 http://saxonica.com/html/documentation/dotnetdoc/Saxon/Api/Xslt30Transformer.html 并建立 ApplyTemplates 输入时 Stream 使用您的大输入XML( http://saxonica.com/html/documentation/dotnetdoc/Saxon/Api/Xslt30Transformer.html#ApplyTemplates(System.IO.Stream,Saxon.Api.XmlDestination) ).