XML 使用 SAXON EE10.6 的 XSLT 流大型 xml 文件

XML XSLT Stream large xml file with SAXON EE10.6

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

mytest.xsl 第 20 行第 34 列错误: XTSE3430 模板规则不可流化

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

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

它非常适合较小的 xml 文件 (<1Gb)

假设您的 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>

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

但是,您的示例 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 的两个分支都可以访问子或后代内容.

XSLT 3.0 streaming 的规则非常复杂,教程介绍少也于事无补。一个非常有用的资源是 Abel Braaksma 在 XML 布拉格 2014 的演讲:在 https://www.xfront.com/Transcript-of-Abel-Braaksma-talk-on-XSLT-Streaming-at-XML-Prague-2014.pdf

上有一份抄本和 link 的 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>

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

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

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

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

   <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 的子元素,因此 <xsl:apply-templates/> 可能又是多余的。对于 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 格式的 >5Gb 上,我没有让它工作。我已经解决了将 xml 文件拆分为大约 1Gb 文件的问题,然后在不进行流式传输的情况下执行 xsl。

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

我的 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, "");
        }