XML XSLT 使用 SAXON EE10.6 流式传输大型 xml 文件

Posted

技术标签:

【中文标题】XML XSLT 使用 SAXON EE10.6 流式传输大型 xml 文件【英文标题】:XML XSLT Stream large xml file with SAXON EE10.6 【发布时间】:2021-11-15 07:15:43 【问题描述】:

我必须将大型 xml 文件 (>5Gb) 导入 SOLR。我想先用 SAXON EE10.6 和流式 xsl 转换一个 xml 文件。我已阅读 SAXON EE10.6 应该可以,但我收到以下错误:

mytest.xsl 的第 20 行第 34 列出错: XTSE3430 模板规则不可流式处理

有多个消费操作数: 在线 21 和 xsl:apply-templates 在第 27 行 模板规则的结果可以包含流式节点 模板规则不可流式处理 消费操作数不止一个:第 21 行的 和第 27 行的 xsl:apply-templates李> 模板规则的结果可以包含流式节点

我不熟悉流式传输 xslt 和 Saxon。如何让我的 xslt 适合流式传输以输出所需的 Solr 添加文档 xml。

我在这里有一个简化版本的 xml 和我使用的 xslt:https://xsltfiddle.liberty-development.net/asoTKU

它适用于较小的 xml 文件 (

【问题讨论】:

从saxonica.com/html/documentation10/sourcedocs/streaming开始,尝试学习。还要解释您的样式表试图实现的目标,并在帖子中显示相关部分。通常,进行两个向下选择的最简单方法是切换到非流式处理模式,该模式处理流式节点的copy-of(),该流式节点足够“小”(例如,可能是Property 元素)以与其所有子节点一起实现/子孙。但是,不要假装我们理解或猜测您为什么在 node() 上匹配,例如,您似乎有明确的意图来处理元素节点。 如果你很绝望,另一个选择是使用xsl:fork 有两个向下选择分支,然后处理器需要找到一个缓冲策略,例如收集一个类别的所有子值,但也需要单独处理它们。但是没有一种方法可以神奇地使您的代码可流式传输,您将需要花一些时间来了解流式传输的局限性(仅转发解析,“缓冲”当前节点(例如具有属性或注释的元素节点或一个文本节点,维护一些祖先层次结构而不是兄弟层次结构)。 【参考方案1】:

假设您的 Properties 元素和 Category “小”到足以被缓冲

<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" expand-text="yes">

  <xsl:output method="xml" encoding="utf-8" indent="yes" />
  
  <xsl:strip-space elements="*"/>
  
  <xsl:mode streamable="yes" on-no-match="shallow-skip"/>
  
  <xsl:mode name="grounded"/>
  
  <xsl:template match="Properties | Category">
    <xsl:apply-templates select="copy-of()" mode="grounded"/>
  </xsl:template>
  
  <xsl:template match="Category" mode="grounded">
    <field name="Category">.</field>
    <xsl:apply-templates mode="#current"/>
  </xsl:template>
  
  <xsl:template match="Properties" mode="grounded">
    <field name="Properties">.</field>
    <xsl:apply-templates mode="#current"/>
  </xsl:template>
  
  <xsl:template match="Category/*" mode="grounded">
    <field name="CAT_local-name()_s">.</field>
  </xsl:template>

  <xsl:template match="Property" mode="grounded">
    <field name="key_s">value</field>
  </xsl:template>

  <xsl:template match="Item/*[not(self::Category | self::Properties)]">
    <field name="local-name()">.</field>
  </xsl:template>

  <xsl:template match='/Items'>
    <add>
      <xsl:apply-templates select="Item"/>
    </add>
  </xsl:template>

  <xsl:template match="Item">
    <xsl:variable name="pos" select="position()"/>
    <doc>
      <xsl:apply-templates>
        <xsl:with-param name="pos"><xsl:value-of select="$pos"/></xsl:with-param>
      </xsl:apply-templates>
    </doc>
  </xsl:template>

</xsl:stylesheet>

但是您的代码(在&lt;xsl:template match="Property"&gt; 中执行&lt;xsl:apply-templates select="Property"/&gt;)表明,也许Property 元素可以递归嵌套,如果代码尝试像上面那样缓冲第一个元素,那么任意嵌套可能会导致内存问题Property 在内存中遇到,使用 copy-of()

但是,您的示例 XML 没有任何嵌套的 Property 元素。

我评论的xsl:fork策略的一部分用于

<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" expand-text="yes">

  <xsl:output method="xml" encoding="utf-8" indent="yes" />
  
  <xsl:strip-space elements="*"/>
  
  <xsl:mode streamable="yes"/>
  
  <xsl:mode name="text" streamable="yes"/>
  
  <xsl:mode name="grounded"/>
  
  <xsl:template match="Category">
    <xsl:apply-templates select="copy-of()" mode="grounded"/>
  </xsl:template>
  
  <xsl:template match="Properties">
    <xsl:fork>
      <xsl:sequence>
        <field name="Properties">
          <xsl:apply-templates mode="text"/>
        </field>
      </xsl:sequence>
      <xsl:sequence>
        <xsl:apply-templates/>
      </xsl:sequence>
    </xsl:fork>
  </xsl:template>
  
  <xsl:template match="Category" mode="grounded">
    <field name="Category">.</field>
    <xsl:apply-templates mode="#current"/>
  </xsl:template>
  
  <xsl:template match="Category/*" mode="grounded">
    <field name="CAT_local-name()_s">.</field>
  </xsl:template>
  
  <xsl:template match="Property">
    <xsl:apply-templates select="copy-of()" mode="grounded"/>
  </xsl:template>

  <xsl:template match="Property" mode="grounded">
    <field name="key_s">value</field>
  </xsl:template>

  <xsl:template match="Item/*[not(self::Category | self::Properties)]">
    <field name="local-name()">.</field>
  </xsl:template>

  <xsl:template match='/Items'>
    <add>
      <xsl:apply-templates select="Item"/>
    </add>
  </xsl:template>

  <xsl:template match="Item">
    <xsl:variable name="pos" select="position()"/>
    <doc>
      <xsl:apply-templates>
        <xsl:with-param name="pos"><xsl:value-of select="$pos"/></xsl:with-param>
      </xsl:apply-templates>
    </doc>
  </xsl:template>

</xsl:stylesheet>

这避免了为每个 Properties 元素显式构造“一棵树”,但我不知道 Saxon 应用什么策略来确保 xsl:fork 的两个分支都可以访问子内容或后代内容。

【讨论】:

关于 xsl:fork,fork 的所有分支(叉子?)都会在输入事件发生时得到通知,有效地并行(尽管这一切都发生在单个线程中)。您需要注意的是,各种插脚的输出是缓冲的,因此可以按正确的顺序组装。所以 xsl:fork 在输入很大但输出很小的情况下效果很好。 属性没有嵌套,它在 xsl 中不正确。我会尝试您的建议以正确进行流式传输。谢谢 @MarcoDuindam,其中一项建议是否适用于 5 GB 输入?【参考方案2】:

XSLT 3.0 流的规则非常复杂,教程介绍很少也无济于事。一个非常有用的资源是 Abel Braaksma 在 XML 布拉格 2014 上的演讲:在https://www.xfront.com/Transcript-of-Abel-Braaksma-talk-on-XSLT-Streaming-at-XML-Prague-2014.pdf有一个抄本和 YouTube 录音的链接

要记住的最重要的规则是:模板规则只能进行一次向下选择(它只有一次机会扫描后代树)。这就是你在写作时打破的规则:

<xsl:template match="node()">
   <xsl:element name="field">
      <xsl:attribute name="name">
        <xsl:value-of select="local-name()"/>
      </xsl:attribute>
      <xsl:value-of select="."/>
   </xsl:element>
   <xsl:apply-templates select="*"/>
</xsl:template>

实际上,该代码可以简化为

<xsl:template match="node()">
   <field name="local-name()">.</field>
   <xsl:apply-templates select="*"/>
</xsl:template>

但这不会影响流能力:您要处理匹配节点的后代两次,一次获取字符串值 (.),一次将模板应用到子节点。

现在,在我看来,这个模板规则似乎只用于处理“叶元素”,即具有文本节点子节点但没有子元素的元素。如果是这种情况,那么 &lt;xsl:apply-templates select="*"/&gt; 永远不会选择任何东西:它是多余的,可以删除,这使得规则可流化。

您会收到另一条错误消息,即模板规则可以返回流式节点。不允许返回流式节点的原因有点微妙。它基本上使处理器无法进行数据流分析以证明流式传输是否可行。但又是 &lt;xsl:apply-templates select="*"/&gt; 导致问题的原因,摆脱它可以解决问题。

您的下一个问题是属性元素的模板规则。你把它写成

   <xsl:template match="Property">
        <xsl:element name="field">
            <xsl:attribute name="name">
               <xsl:value-of select="key"/>_s</xsl:attribute>
            <xsl:value-of select="value"/>
        </xsl:element>
        <xsl:apply-templates select="Property"/>
    </xsl:template>

它简化为:

<xsl:template match="Property">
    <field name="key_s">value</field>
    <xsl:apply-templates select="Property"/>
</xsl:template>

这是进行三个向下的选择:child::keychild::valuechild::Property。在您的数据样本中,没有Property 元素有一个名为Property 的子元素,因此&lt;xsl:apply-templates/&gt; 可能又是多余的。对于keyvalue,一个有用的技巧是将它们读入地图:

<xsl:template match="Property">
    <xsl:variable name="pair" as="map(*)">
      <xsl:map>
        <xsl:map-entry key="'key'" select="string(key)"/>
        <xsl:map-entry key="'value'" select="string(value)"/>
      </xsl:map>
    </xsl:variable>
    <field name="$pair?key_s">$pair?value</field>
</xsl:template>

之所以有效,是因为xsl:map(如xsl:fork)是“向下选择”规则的一个例外——地图可以在一次输入中构建。通过调用string(),我们注意不要将任何流式节点放入映射中,因此我们稍后需要的数据已经在映射中捕获,我们不需要返回流式输入文档来读取它第二次。

我希望这能让您对前进的道路有所了解。 XSLT 中的流式传输不适合胆小的人,但如果您有 >5Gb 的输入文档,那么您就没有太多选择了。

【讨论】:

谢谢你,我会试试你的建议。属性没有嵌套,我在 xsl 中的错。另一种方法是将 xml 文件拆分为较小的文件并跳过流部分。 是的,如果您能够独立于所有其他人转换输入中的每个“记录”(无论“记录”是什么),这是一种常见的设计方法。使用与源中的“记录”匹配的模板规则编写流模式;此模板规则执行&lt;xsl:apply-templates select="copy-of(.)" mode="ns"/&gt;,其中ns 是一种独立处理每个“记录”的非流模式。【参考方案3】:

给定的 xsl 解决方案适用于简化版本。然而,在完整的 xml 格式的 >5Gb 上,我没有让它工作。我已经解决了将 xml 文件拆分为大约 1Gb 的文件,然后在没有流式传输的情况下执行 xsl。

如果有人想要挑战,请私下联系我;)

【讨论】:

嗯,您的解决方案中有趣的部分仍然是您如何将 5GB 文档拆分为更小的文档?您是使用 SAX 或 Stax 还是如何做到的?【参考方案4】:

我的 xml 文件在每个项目后都有一个换行符。所以我创建了一个简单的控制台应用程序,它将文件拆分为 500.000 行,删除空字符并使用 xsl 转换结果:

cleanxml.exe items.xml temp-items-solr.xml import.xsl

        static void Main(string[] args)
        
            string line;

            XslCompiledTransform xsltTransform = new XslCompiledTransform();
            xsltTransform.Load(@args[2]);

            string fileToWriteTo = args[1];
            StreamWriter writer = new StreamWriter(fileToWriteTo);
            StreamReader file = new System.IO.StreamReader(@args[0]);

            string fileOriginal = @args[1];
            string firstLine = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><Items>";

            int i = 0;
            int j = 1;
            while ((line = file.ReadLine()) != null)
            

                writer.WriteLine(CleanInvalidXmlChars(line)); 

                if(i > 500000)
                
                    writer.WriteLine("</Items>"); 
                    writer.Flush();
                    writer.Dispose();

                    xsltTransform.Transform(fileToWriteTo, fileToWriteTo.Replace("temp-",""));

                    System.IO.File.Delete(fileToWriteTo);
                    fileToWriteTo = fileOriginal.Replace(".xml", "-" + j.ToString() + ".xml");
                    writer = new StreamWriter(fileToWriteTo);
                    writer.WriteLine(firstLine);

                    i = 0;
                    j += 1;
                
                i += 1;
            

            writer.Flush();
            writer.Dispose();

            xsltTransform.Transform(fileToWriteTo, fileToWriteTo.Replace("temp-", ""));
            System.IO.File.Delete(fileToWriteTo);

            file.Close();
        


        private static MemoryStream ApplyXSLT(string xmlInput, string xsltFilePath)
        
            XmlDocument xmlDocument = new XmlDocument();
            xmlDocument.LoadXml(xmlInput);

            XslCompiledTransform xsltTransform = new XslCompiledTransform();
            xsltTransform.Load(xsltFilePath);

            MemoryStream memoreStream = new MemoryStream();
            xsltTransform.Transform(xmlDocument, null, memoreStream);
            memoreStream.Position = 0;

            return memoreStream;
        


        public static string CleanInvalidXmlChars(string text)
        
            string re = @"[^\x09\x0A\x0D\x20-\xD7FF\xE000-\xFFFD\x10000-x10FFFF]";
            return Regex.Replace(text, re, "");
        

【讨论】:

以上是关于XML XSLT 使用 SAXON EE10.6 流式传输大型 xml 文件的主要内容,如果未能解决你的问题,请参考以下文章

XSLT 函数返回不同的结果 [Saxon-EE vs Saxon-HE/PE]

使用 SAXON 和 XSLT 合并 XML 文件

从 Saxon 9.4he 中的嵌入式资源加载 xml 和 xslt

XSLT 2 (Saxon):如何将多个文件读入内存

XSLT 3.0 xsl:mode on-no-match="shallow-skip"

是否有任何XSLT处理命令行工具? [关闭]