XSLT 3.0 Streaming with Grouping and Sum/Accumulator

Posted

技术标签:

【中文标题】XSLT 3.0 Streaming with Grouping and Sum/Accumulator【英文标题】: 【发布时间】:2017-11-01 10:43:19 【问题描述】:

我试图弄清楚如何在需要分组(具有任意数量的组)并对组求和的场景中使用 XSLT Streaming(以减少内存使用)。到目前为止,我还没有找到任何例子。这是一个 XML 示例

<?xml version='1.0' encoding='UTF-8'?>
  <Data>
    <Entry>
      <Genre>Fantasy</Genre>
      <Condition>New</Condition>
      <Format>Hardback</Format>
      <Title>Birds</Title>
      <Count>3</Count>
    </Entry>
    <Entry>
      <Genre>Fantasy</Genre>
      <Condition>New</Condition>
      <Format>Hardback</Format>
      <Title>Cats</Title>
      <Count>2</Count>
    </Entry>
    <Entry>
      <Genre>Non-Fiction</Genre>
      <Condition>New</Condition>
      <Format>Paperback</Format>
      <Title>Dogs</Title>
      <Count>4</Count>
    </Entry>
 </Data>

在 XSLT 2.0 中,我会使用它来按流派、条件和格式分组,并对计数求和。

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="text" indent="yes" />
  <xsl:template match="/">
     <xsl:call-template name="body"/>
  </xsl:template>
  <xsl:template name="body">
    <xsl:for-each-group select="Data/Entry" group-by="concat(Genre,Condition,Format)">
      <xsl:value-of select="Genre"/>
      <xsl:value-of select="Condition"/>
      <xsl:value-of select="Format"/>
      <xsl:value-of select="sum(current-group()/Count)"/>
    </xsl:for-each-group>
  </xsl:template>
</xsl:stylesheet>

对于输出,我会得到两行,Fantasy、New、Hardback 的总和为 5,Non-Fiction、New、Paperback 的总和为 4。

显然这不适用于流式传输,因为总和会访问整个组。我想我需要遍历文档两次。我第一次可以构建组图(如果尚不存在,则创建一个新组)。第二次 问题是我还需要每个组的累加器,其规则与组匹配,看来你不能创建动态累加器。

有没有办法即时创建累加器?是否有另一种/更简单的方法来使用流式传输?

【问题讨论】:

一些想法:在 XSLT 3.0 中,我不会连接要分组的元素,而是使用xsl:for-each-group select="Data/Entry" group-by="Genre,Condition,Format" composite="yes"。然而,对于流式分组,鉴于您想将group-by 与子元素一起使用,您可以做的就是&lt;xsl:for-each-groups select="copy-of(Data/Entry)" group-by="Genre,Condition,Format" composite="yes",否则如果不使用copy-of,您根本无法在group-by 中选择子元素。跨度> 【参考方案1】:

为了能够在 XSLT 3.0 中使用流式分组,我看到的一个选项是首先使用样式表将基于元素的数据转换为基于属性的数据

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:math="http://www.w3.org/2005/xpath-functions/math"
    exclude-result-prefixes="xs math"
    version="3.0">

    <xsl:mode streamable="yes" on-no-match="shallow-copy"/>

    <xsl:output indent="yes"/>
    <xsl:strip-space elements="*"/>

    <xsl:template match="Entry/*">
        <xsl:attribute name="name()" namespace="namespace-uri()" select="."/>
    </xsl:template>

</xsl:stylesheet>

那么您可以完美地使用流式分组(就可以使用流式group-by 而言,据我了解将需要一些缓冲),如下所示:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:math="http://www.w3.org/2005/xpath-functions/math"
    exclude-result-prefixes="xs math"
    version="3.0">

    <xsl:mode streamable="yes"/>

    <xsl:output method="text"/>

    <xsl:template match="/">
        <xsl:fork>
            <xsl:for-each-group select="Data/Entry" composite="yes" group-by="@Genre, @Condition, @Format">
                <xsl:value-of select="current-grouping-key(), sum(current-group()/@Count)"/>
                <xsl:text>&#10;</xsl:text>
            </xsl:for-each-group>
        </xsl:fork>
    </xsl:template>

</xsl:stylesheet>

我不知道首先创建一个以属性为中心的文档是否是一种选择,但我认为最好在答案中与代码分享建议,而不是尝试将它们放入评论中。 XSLT Streaming Chained Transform 中的答案展示了如何使用 Saxon 9 和 Java 或 Scala 来链接两个流转换,而无需为第一个转换步骤编写临时输出文件。

至于在原始输入格式上使用copy-of 执行此操作,Saxon 9.7 EE 将以下内容评估为可流式处理并以正确的结果执行:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:math="http://www.w3.org/2005/xpath-functions/math" exclude-result-prefixes="xs math"
    version="3.0">

    <xsl:mode streamable="yes"/>

    <xsl:output method="text"/>

    <xsl:template match="/">
        <xsl:for-each-group select="copy-of(Data/Entry)" composite="yes"
            group-by="Genre, Condition, Format">
            <xsl:value-of select="current-grouping-key(), sum(current-group()/Count)"/>
            <xsl:text>&#10;</xsl:text>
        </xsl:for-each-group>
    </xsl:template>

</xsl:stylesheet>

我不确定它消耗的内存是否比正常的基于树的分组要少。也许你可以用你的真实输入数据来衡量。

作为第三种选择,要按照您的意愿使用地图,这是一个 xsl:iterate 示例,它遍历 Entry 元素,收集地图中累积的 Count 值:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:math="http://www.w3.org/2005/xpath-functions/math"
    xmlns:map="http://www.w3.org/2005/xpath-functions/map" exclude-result-prefixes="xs math map"
    version="3.0">

    <xsl:mode streamable="yes"/>

    <xsl:output method="text"/>

    <xsl:template match="/">
        <xsl:iterate select="Data/Entry">
            <xsl:param name="groups" as="map(xs:string, xs:integer)" select="map"/>
            <xsl:on-completion>
                <xsl:value-of select="map:keys($groups)!(. || ' ' || $groups(.))" separator="&#10;"/>
            </xsl:on-completion>
            <xsl:variable name="current-entry" select="copy-of()"/>
            <xsl:variable name="key"
                select="string-join($current-entry/(Genre, Condition, Format), '|')"/>
            <xsl:next-iteration>
                <xsl:with-param name="groups"
                    select="
                        if (map:contains($groups, $key)) then
                            map:put($groups, $key, map:get($groups, $key) + xs:integer($current-entry/Count))
                        else
                            map:put($groups, $key, xs:integer($current-entry/Count))"
                />
            </xsl:next-iteration>
        </xsl:iterate>
    </xsl:template>

</xsl:stylesheet>

【讨论】:

@RyanB,当你接受答案时,我很好奇你用你的真实数据解决了哪种方法? 嗨,马丁,感谢您的出色回答。我使用了最后一种方法,它根据输出的组数来调整内存使用量。 xslt3 的答案非常好,加一。

以上是关于XSLT 3.0 Streaming with Grouping and Sum/Accumulator的主要内容,如果未能解决你的问题,请参考以下文章

XSLT 3.0 - 在 XSLT 3.0 xml-to-json() 中出现错误“重复键值”

Marklogic xml 转换中的 XSLT 3.0 支持

BizTalk Server 2016 映射中是不是支持 XSLT 2.0 或 3.0?

BizTalk 2020 XSLT 3.0 - 样式表编译期间报告错误

如何在 Java 应用程序中使用 XSLT 3.0?

使用 XSLT 2.0/3.0 使用多个步骤将 CDATA 中的纯文本解析为 html。那里的一部分