【问题标题】:How can I speed up my 'divide and conquer' XSLT template which replaces certain characters in a string?如何加快替换字符串中某些字符的“分而治之”XSLT 模板的速度?
【发布时间】:2011-04-03 06:31:24
【问题描述】:

更新:我添加了一个answer to this question,其中包含了几乎所有已给出的建议。下面代码中给出的原始模板需要 45605ms 来完成一个真实世界的输入文档(关于脚本编程的英文文本)。 community wiki answer 中修改后的模板使运行时间降至 605 毫秒

我正在使用以下 XSLT 模板将字符串中的一些特殊字符替换为其转义变体;它使用分而治之的策略递归地调用自己,最终查看给定字符串中的每个字符。然后它决定是否应该按原样打印字符,或者是否需要任何形式的转义:

<xsl:template name="escape-text">
<xsl:param name="s" select="."/>
<xsl:param name="len" select="string-length($s)"/>
<xsl:choose>
    <xsl:when test="$len >= 2">
        <xsl:variable name="halflen" select="round($len div 2)"/>
        <xsl:variable name="left">
            <xsl:call-template name="escape-text">
                <xsl:with-param name="s" select="substring($s, 1, $halflen)"/>
                <xsl:with-param name="len" select="$halflen"/>
            </xsl:call-template>
        </xsl:variable>
        <xsl:variable name="right">
            <xsl:call-template name="escape-text">
                <xsl:with-param name="s" select="substring($s, $halflen + 1)"/>
                <xsl:with-param name="len" select="$halflen"/>
            </xsl:call-template>
        </xsl:variable>
        <xsl:value-of select="concat($left, $right)"/>
    </xsl:when>
    <xsl:otherwise>
        <xsl:choose>
            <xsl:when test="$s = '&quot;'">
                <xsl:text>&quot;\&quot;&quot;</xsl:text>
            </xsl:when>
            <xsl:when test="$s = '@'">
                <xsl:text>&quot;@&quot;</xsl:text>
            </xsl:when>
            <xsl:when test="$s = '|'">
                <xsl:text>&quot;|&quot;</xsl:text>
            </xsl:when>
            <xsl:when test="$s = '#'">
                <xsl:text>&quot;#&quot;</xsl:text>
            </xsl:when>
            <xsl:when test="$s = '\'">
                <xsl:text>&quot;\\&quot;</xsl:text>
            </xsl:when>
            <xsl:when test="$s = '}'">
                <xsl:text>&quot;}&quot;</xsl:text>
            </xsl:when>
            <xsl:when test="$s = '&amp;'">
                <xsl:text>&quot;&amp;&quot;</xsl:text>
            </xsl:when>
            <xsl:when test="$s = '^'">
                <xsl:text>&quot;^&quot;</xsl:text>
            </xsl:when>
            <xsl:when test="$s = '~'">
                <xsl:text>&quot;~&quot;</xsl:text>
            </xsl:when>
            <xsl:when test="$s = '/'">
                <xsl:text>&quot;/&quot;</xsl:text>
            </xsl:when>
            <xsl:when test="$s = '{'">
                <xsl:text>&quot;{&quot;</xsl:text>
            </xsl:when>
            <xsl:otherwise>
                <xsl:value-of select="$s"/>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:otherwise>
</xsl:choose>
</xsl:template>

这个模板占了我的 XSLT 脚本所需的大部分运行时间。将上面的escape-text 模板替换为just

<xsl:template name="escape-text">
    <xsl:param name="s" select="."/>
    <xsl:value-of select="$s"/>
</xsl:template>

使我的 XSLT 脚本在我的一个文档上的运行时间从 45 秒缩短到不到 1 秒。

因此我的问题是:如何加快我的escape-text 模板?我正在使用xsltproc,我更喜欢纯XSLT 1.0 解决方案。 XSLT 2.0 解决方案也将受到欢迎。但是,外部库可能对这个项目没有用 - 我仍然对使用它们的任何解决方案感兴趣。

【问题讨论】:

  • 好问题 (+1)。查看我的答案,速度提高了 17 倍 :)
  • @Frerich-Raabe、@Tomalak、@Alejandro:看我的下一个答案——比@Frerich-Raabe 的最新组合建议解决方案额外加速 1.5 倍。 :) 现在总加速必须超过 100 倍。

标签: string xslt divide-and-conquer


【解决方案1】:

如果条件translate($s, $vChars, '') = $s 为真,另一种(补充)策略是在字符串长度降至1 之前提前终止递归。这应该可以更快地处理根本不包含特殊字符的字符串,这可能是其中的大多数。当然,结果将取决于 xsltproc 对translate() 的实现效率如何。

【讨论】:

  • +1 非常好的推理。 PS:我刚刚注意到我给了 XSLT 的教父他对 SO 的第一次支持!很高兴在这里见到你! :)
  • +1 推理是合理的;实施这个技巧将运行时间从 5812 毫秒(这是在实施 Tomalaks 和 Dimitre 的建议之后)降低到 657 毫秒。我对此感到非常惊讶,以至于我不得不将输出与之前的输出进行比较。
  • @Tomalak - 有徽章吗? :-) 应该有!
  • 哇...欢迎来到 SO,Kay 博士!当然,你的答案值得知道。 +1。
  • @Michael-Kay、@LarsH、@Tomalak、@Alejandro、@Frerich-Raabe、@Wilfred-Springer:到目前为止,谁的解决方案最快?请将您的解决方案发送给我(谷歌邮件上的 dnovatchev)和您的数据文件——我想我可以进一步加快最快的解决方案。
【解决方案2】:

一个非常小的修正将我的测试速度提高了大约 17 倍

还有其他改进,但我想现在就足够了... :)

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:my="my:my">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:strip-space elements="*"/>

 <xsl:variable name="vChars">"@|#\}&amp;^~/{</xsl:variable>

 <xsl:template match="node()|@*">
  <xsl:copy>
   <xsl:apply-templates select="node()|@*"/>
  </xsl:copy>
 </xsl:template>

 <xsl:template match="text()" name="escape-text">
  <xsl:param name="s" select="."/>
  <xsl:param name="len" select="string-length($s)"/>

  <xsl:choose>
    <xsl:when test="$len >= 2">
        <xsl:variable name="halflen" select="round($len div 2)"/>
        <xsl:variable name="left">
            <xsl:call-template name="escape-text">
                <xsl:with-param name="s" select="substring($s, 1, $halflen)"/>
                <xsl:with-param name="len" select="$halflen"/>
            </xsl:call-template>
        </xsl:variable>
        <xsl:variable name="right">
            <xsl:call-template name="escape-text">
                <xsl:with-param name="s" select="substring($s, $halflen + 1)"/>
                <xsl:with-param name="len" select="$halflen"/>
            </xsl:call-template>
        </xsl:variable>
        <xsl:value-of select="concat($left, $right)"/>
    </xsl:when>
    <xsl:otherwise>
        <xsl:choose>
            <xsl:when test="not(contains($vChars, $s))">
             <xsl:value-of select="$s"/>
            </xsl:when>
            <xsl:when test="$s = '&quot;'">
                <xsl:text>&quot;\&quot;&quot;</xsl:text>
            </xsl:when>
            <xsl:when test="$s = '@'">
                <xsl:text>&quot;@&quot;</xsl:text>
            </xsl:when>
            <xsl:when test="$s = '|'">
                <xsl:text>&quot;|&quot;</xsl:text>
            </xsl:when>
            <xsl:when test="$s = '#'">
                <xsl:text>&quot;#&quot;</xsl:text>
            </xsl:when>
            <xsl:when test="$s = '\'">
                <xsl:text>&quot;\\&quot;</xsl:text>
            </xsl:when>
            <xsl:when test="$s = '}'">
                <xsl:text>&quot;}&quot;</xsl:text>
            </xsl:when>
            <xsl:when test="$s = '&amp;'">
                <xsl:text>&quot;&amp;&quot;</xsl:text>
            </xsl:when>
            <xsl:when test="$s = '^'">
                <xsl:text>&quot;^&quot;</xsl:text>
            </xsl:when>
            <xsl:when test="$s = '~'">
                <xsl:text>&quot;~&quot;</xsl:text>
            </xsl:when>
            <xsl:when test="$s = '/'">
                <xsl:text>&quot;/&quot;</xsl:text>
            </xsl:when>
            <xsl:when test="$s = '{'">
                <xsl:text>&quot;{&quot;</xsl:text>
            </xsl:when>
        </xsl:choose>
    </xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>

【讨论】:

  • +1 微不足道但合理的变化。有时,事情很容易改进。 :-)
  • @Dimitre:+1 表示改进。
  • +1:感谢您的回复。我将对此进行一些尝试,试图找出哪些更改会导致加速。 $vChars的用途? escape-text 模板的模板元素的额外match="text()" 属性?或者也许是匹配node()|@* 的奇怪小模板 - 我首先必须弄清楚它实际上做了什么。 :-}
  • @Frerich Raabe:身份转换和匹配文本节点可以使此样式表适用于任何输入(您尚未提供)。改进之处在于首先丢弃与需要替换的字符不匹配的字符。这就是第一个when/@test="not(contains($vChars, $s))"的意思
  • @Frerich:这是when test="not(contains(...))"(即您的otherwise 案例)移动到choose 中的第一个位置。必须首先检查最常见的情况,以避免几乎总是失败的冗余检查。
【解决方案3】:

根据@Dimitre 的回答,这是一个更完善的版本:

  <xsl:template match="text()" name="escape-text">
    <xsl:param name="s" select="."/>
    <xsl:param name="len" select="string-length($s)"/>

    <xsl:choose>
      <xsl:when test="$len &gt; 1">
        <xsl:variable name="halflen" select="round($len div 2)"/>
        <!-- no "left" and "right" variables necessary! -->
        <xsl:call-template name="escape-text">
          <xsl:with-param name="s" select="substring($s, 1, $halflen)"/>
        </xsl:call-template>
        <xsl:call-template name="escape-text">
          <xsl:with-param name="s" select="substring($s, $halflen + 1)"/>
        </xsl:call-template>
      </xsl:when>
      <xsl:otherwise>
        <xsl:choose>
          <xsl:when test="not(contains($vChars, $s))">
            <xsl:value-of select="$s"/>
          </xsl:when>
          <xsl:when test="contains('\&quot;', $s)">
            <xsl:value-of select="concat('&quot;\', $s, '&quot;')" />
          </xsl:when>
          <!-- all other cases can be collapsed, this saves some time -->
          <xsl:otherwise>
            <xsl:value-of select="concat('&quot;', $s, '&quot;')" />
          </xsl:otherwise>
        </xsl:choose>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>

应该再快一点,但我没有对其进行基准测试。无论如何,它更短。 ;-)

【讨论】:

  • @Tomalak:还有一个" 案例应该是&amp;quot;\&amp;quot;&amp;quot;。我认为您在$len gt; 1 中有错字。正如我们之前所说,那可能是$len &gt; 1
  • @Alejandro:是的,这是一个错字。我会修复它。也感谢您提供额外案例的提示。
  • +1:感谢您的回复!甚至不需要leftright 的想法很棒,我很震惊这并没有发生在我身上。此外,折叠xsl:choose 是个好主意;即使它对性能没有任何帮助,它也需要更少的代码并且不会牺牲可读性。
  • 我觉得没有left和right变量,这样应该会更快,因为RTF到字符串转换和相邻文本节点序列化的区别。
  • @Tomalak、@Frerich-Raabe、@Alejandro:是的,这是一个明显的重构。其他要尝试的事情: 1. 我相信拥有一系列 &lt;xsl:value-of&gt;&lt;xsl:value-of select="concat(...)"/&gt; 快 2. 在 $vChars 中,还可以根据字符的递减频率对字符进行排序。
【解决方案4】:

对于它的价值,这是我当前版本的 escape-text 模板,其中包含了人们在回答我的问题时给出的大部分(非常棒!)建议。作为记录,我的原始版本在我的示例 DocBook 文档上平均花费了大约 45605 毫秒。之后,运行时间在多个步骤中减少:

  • 删除leftright 变量以及concat() 调用将运行时间降低到13052 毫秒;此优化取自 Tomalak's answer
  • 首先在内部&lt;xsl:choose&gt; 元素中移动常见情况(即:给定字符不需要任何特殊转义)使运行时间进一步降低到5812 毫秒。这个优化首先是suggested by Dimitre
  • 通过首先测试给定字符串是否包含任何特殊字符来提前中止递归,从而将运行时间缩短到 612 毫秒。这个优化是suggested by Michael
  • 最后,在阅读了 Dimitre 在Tomalak's answer 中的评论后,我忍不住做了一个微优化:我用&lt;xsl:text&gt;x&lt;/xsl:text&gt;&lt;xsl:value-of select="$s"/&gt;&lt;xsl:text&gt;y&lt;/xsl:text&gt; 替换了&lt;xsl:value-of select="concat('x', $s, 'y')"/&gt; 调用。这使运行时间达到了大约 606 毫秒(因此提高了大约 1%)。

最后,这个函数用了 606ms 而不是 45605ms。令人印象深刻!

<xsl:variable name="specialLoutChars">"@|#\}&amp;^~/{</xsl:variable>

<xsl:template name="escape-text">
    <xsl:param name="s" select="."/>
    <xsl:param name="len" select="string-length($s)"/>
    <xsl:choose>
        <!-- Common case optimization: 
             no need to recurse if there are no special characters -->
        <xsl:when test="translate($s, $specialLoutChars, '') = $s">
            <xsl:value-of select="$s"/>
        </xsl:when>
        <!-- String length greater than 1, use DVC pattern -->
        <xsl:when test="$len > 1">
            <xsl:variable name="halflen" select="round($len div 2)"/>
            <xsl:call-template name="escape-text">
                <xsl:with-param name="s" select="substring($s, 1, $halflen)"/>
                <xsl:with-param name="len" select="$halflen"/>
            </xsl:call-template>
            <xsl:call-template name="escape-text">
                <xsl:with-param name="s" select="substring($s, $halflen + 1)"/>
                <xsl:with-param name="len" select="$len - $halflen"/>
            </xsl:call-template>
        </xsl:when>
        <!-- Special character -->
        <xsl:otherwise>
            <xsl:text>&quot;</xsl:text>
            <!-- Backslash and quot need backslash escape -->
            <xsl:if test="$s = '&quot;' or $s = '\'">
                <xsl:text>\</xsl:text>
            </xsl:if>
            <xsl:value-of select="$s"/>
            <xsl:text>&quot;</xsl:text>
        </xsl:otherwise>
    </xsl:choose>
</xsl:template>

【讨论】:

  • @Frerich-Raabe:76 倍加速——不错,是吗? :)
  • @Frerich-Raabe:在我的测试文件中,这种转换比我回答中的解决方案快 1.45 倍——我相信我们可以做得更好。
  • @Dimitre:太棒了,是的。 :-)
  • 我认为你可以用更紧凑的代码重构这个答案,只需一个 choose 并按以下顺序进行测试:translate...$len &gt; 1、quot 和反斜杠测试,否则。
  • 恭喜!这是本网站上更有趣(也更令人满意!)的主题之一。此外,看到“字符串文字”与“字符串函数”变化相比 1% 的净改进让我微笑。 ;)
【解决方案5】:

使用 EXSLT 怎么样? EXSLT 中的字符串函数有一个名为 replace 的函数。我认为很多 XSLT 实现都支持它。

【讨论】:

  • 确实如此;不幸的是,在我的情况下,替换不是一个选项,因为要替换的字符之一 (") 被用作转义字符本身。
  • @Frerich,你能详细说明一下吗?为什么这会阻止您使用 replace()?然而,这一点没有实际意义,因为列出的 EXSLT 替换函数的唯一实现是在 XSLT 中,因此原则上并不比在 XSLT 中自己做更有效。
  • @LarsH:对不起,我的评论不准确。这是一个例子:考虑字符串"\ ,并记住"应该替换为"\""` should be replaced with "\\"`;无论您执行这两个替换的哪个顺序,结果都是不正确的,因为第二个替换也会替换第一个替换引入的字符。 [我放弃了,我无法用这些该死的反斜杠正确标记!]
  • @Frerich,阅读该函数的规范,听起来替换应该是相互独立的,而不是对彼此的输出进行操作。 “str:replace 函数的工作原理是将搜索字符串列表在第一个参数字符串中 [强调我的] 中的字符串的每个出现 [原文] 替换为替换节点列表中的等效定位节点。” IE。它替换原始字符串中的出现,而不是应用一些替换后中间结果中的出现。太多细节在这里发表评论......我会添加一个答案。
  • @Frerich,没关系,我不会添加答案...我想说 exsl replace() 需要一个搜索字符串的节点集和一个替换字符串的节点集,所以理论上,你可以在一次调用中进行多次替换,它们都搜索原始字符串,而不是彼此的输出。但是,对于该函数的规范的最新版本,根本没有已知的实现,虽然有早期版本的 XSLT 实现,但我没有找到它的规范。所以我不会费心去测试它。
【解决方案6】:

更新:我已将其修复为实际工作;现在,这不是加速!

以@Wilfred 的回答为基础...

在摆弄 EXSLT replace() 函数之后,我认为发布另一个答案很有趣,即使它对 OP 没有用。它可能对其他人有用。

这很有趣,因为算法:而不是这里工作的主要算法(进行二进制递归搜索,在每次递归时分成两半,每当第 2^n 个子字符串中没有特殊字符时修剪,并迭代当长度为 1 的字符串确实包含特殊字符时选择特殊字符),Jeni Tennison 的 EXSLT 算法将迭代放在外部循环中的一组搜索字符串上。因此在循环内部,一次只查找一个字符串,可以使用 substring-before()/substring-after() 对字符串进行分割,而不是盲目地对半分割。

[已弃用:我想这足以显着加快速度。我的测试显示,与@Dimitre 的最新测试相比,速度提高了 2.94 倍(平均 230 毫秒与 676 毫秒)。] 我在 Oxygen XML 分析器中使用 Saxon 6.5.5 进行测试。作为输入,我使用了一个 7MB 的 XML 文档,该文档主要是一个文本节点,由 web pages about javascript 创建,重复。在我看来,这代表了 OP 试图优化的任务。我很想看看其他人通过他们的测试数据和环境得到什么结果。

依赖关系

这使用了依赖于 exsl:node-set() 的替换的 XSLT 实现。看起来 xsltproc 支持这个扩展功能(可能是它的早期版本)。所以这对你来说可能是开箱即用的,@Frerich;对于other processors,就像对撒克逊人所做的那样。

但是,如果我们想要 100% 纯 XSLT 1.0,我认为修改此替换模板以在没有 exsl:node-set() 的情况下工作不会太难,只要第二个和第三个参数作为节点集传入,而不是 RTF。

这是我使用的代码,它调用了替换模板。大部分长度都被我创建搜索/替换节点集的冗长方式占用了......这可能会被缩短。 (但您无法搜索或替换节点 attributes,因为当前编写了替换模板。尝试将属性放在文档元素下时会出错。)

<xsl:stylesheet version="1.0" xmlns:str="http://exslt.org/strings"
    xmlns:foo="http://www.foo.net/something" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:import href="lars.replace.template.xsl"/>

    <foo:replacements>
        <replacement>
            <search>"</search>
            <replace>"\""</replace>
        </replacement>
        <replacement>
            <search>\</search>
            <replace>"\\"</replace>
        </replacement>
        <replacement>
            <search>@</search>
            <replace>"["</replace>
        </replacement>
        <replacement>
            <search>|</search>
            <replace>"["</replace>
        </replacement>
        <replacement>
            <search>#</search>
            <replace>"["</replace>
        </replacement>
        <replacement>
            <search>}</search>
            <replace>"}"</replace>
        </replacement>
        <replacement>
            <search>&amp;</search>
            <replace>"&amp;"</replace>
        </replacement>
        <replacement>
            <search>^</search>
            <replace>"^"</replace>
        </replacement>
        <replacement>
            <search>~</search>
            <replace>"~"</replace>
        </replacement>
        <replacement>
            <search>/</search>
            <replace>"/"</replace>
        </replacement>
        <replacement>
            <search>{</search>
            <replace>"{"</replace>
        </replacement>
    </foo:replacements>

    <xsl:template name="escape-text" match="text()" priority="2">
        <xsl:call-template name="str:replace">
            <xsl:with-param name="string" select="."/>
            <xsl:with-param name="search"
                select="document('')/*/foo:replacements/replacement/search/text()"/>
            <xsl:with-param name="replace"
                select="document('')/*/foo:replacements/replacement/replace/text()"/>
        </xsl:call-template>
    </xsl:template>

    <xsl:template match="node()|@*">
        <xsl:copy>
            <xsl:apply-templates select="node()|@*"/>
        </xsl:copy>
    </xsl:template>
</xsl:stylesheet>

导入的样式表原来是this one

但是,正如@Frerich 指出的那样,这从未给出正确的输出! 这应该教会我不要在不检查正确性的情况下发布性能数据!

我可以在调试器中看到它出错的地方,但我不知道 EXSLT 模板是否从未工作过,或者它是否只是在 Saxon 6.5.5 中不起作用......任何一个选项都会令人惊讶。

无论如何,EXSLT 的 str:replace() 被指定做的比我们需要的更多,所以我修改它以便

  • 要求输入参数已经是节点集
  • 因此,不需要 exsl:node-set()
  • 不按长度对搜索字符串进行排序(在此应用程序中它们都是一个字符)
  • 当对应的搜索字符串为空时,不在每对字符之间插入替换字符串

这是修改后的替换模板:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
   xmlns:str="http://exslt.org/strings">
   <!-- By Lars Huttar
    based on implementation of EXSL str:replace() by Jenni Tennison.
    http://www.exslt.org/str/functions/replace/str.replace.template.xsl
    Modified by Lars not to need exsl:node-set(), not to bother sorting
    search strings by length (in our application, all the search strings are of
    length 1), and not to put replacements between every other character
    when a search string is length zero.
    Search and replace parameters must both be nodesets.
    -->

   <xsl:template name="str:replace">
      <xsl:param name="string" select="''" />
      <xsl:param name="search" select="/.." />
      <xsl:param name="replace" select="/.." />
      <xsl:choose>
         <xsl:when test="not($string)" />
         <xsl:when test="not($search)">
            <xsl:value-of select="$string" />
         </xsl:when>
         <xsl:otherwise>
            <xsl:variable name="search1" select="$search[1]" />
            <xsl:variable name="replace1" select="$replace[1]" />

            <xsl:choose>
               <xsl:when test="contains($string, $search1)">
                  <xsl:call-template name="str:replace">
                     <xsl:with-param name="string"
                        select="substring-before($string, $search1)" />
                     <xsl:with-param name="search"
                        select="$search[position() > 1]" />
                     <xsl:with-param name="replace"
                        select="$replace[position() > 1]" />
                  </xsl:call-template>
                  <xsl:value-of select="$replace1" />
                  <xsl:call-template name="str:replace">
                     <xsl:with-param name="string"
                        select="substring-after($string, $search)" />
                     <xsl:with-param name="search" select="$search" />
                     <xsl:with-param name="replace" select="$replace" />
                  </xsl:call-template>
               </xsl:when>
               <xsl:otherwise>
                  <xsl:call-template name="str:replace">
                     <xsl:with-param name="string" select="$string" />
                     <xsl:with-param name="search"
                        select="$search[position() > 1]" />
                     <xsl:with-param name="replace"
                        select="$replace[position() > 1]" />
                  </xsl:call-template>
               </xsl:otherwise>
            </xsl:choose>
         </xsl:otherwise>
      </xsl:choose>
   </xsl:template>

</xsl:stylesheet>

这个更简单的模板的一个附带好处是,您现在可以为搜索和替换参数的节点使用属性。这将使&lt;foo:replacements&gt; 数据更紧凑,更易于阅读 IMO。

性能:使用这个修改后的模板,工作大约在 2.5 秒内完成,而我最近对主要竞争对手 @Dimitre 的 XSLT 1.0 样式表的测试需要 0.68 秒。所以这不是加速。但同样,其他人的测试结果与我的测试结果大不相同,所以我想听听其他人从这个样式表中得到什么。

【讨论】:

  • 太棒了 (+1)。请把你的数据文件发给我好吗?只是一个单元,还提供重复因子。我想我可以根据您的解决方案进一步改进... :)
  • 感谢您的努力!有趣的是,我最近决定也使用&lt;replacements&gt; 数据表,以便字符串映射实际上是函数的数据,而不是编码为&lt;xsl:choose&gt;。这使得函数变得相当慢(在我目前手头的一个文档中,它从 3201 毫秒变为 3779 毫秒),但恕我直言,它更容易阅读和扩展。
  • 我这里一定是做错了什么;不幸的是,您的函数在我的示例文档上慢了 很多 (我在答案中给出的需要 3201 毫秒,您需要 88921 毫秒;也许这应该归咎于 xsltproc,但无论如何,您的脚本没有产生与其他脚本相同的输出。它似乎并没有真正替换任何特殊字符。我确定这是我的错误。
  • @Dimitre,谢谢。 :-) 我已经将数据文件上传到huttar.net/lars-kathy/tmp/so3558407.zip@Frerich,哇,速度慢得可怕。我将尝试使用 xsltproc 运行此脚本,看看是否得到相同的结果。 Saxon 确实有一些巧妙的优化,也许这就是我们看到的性能差异的一部分。
  • 奇怪,今天早上当我尝试运行各种实现时,只有我的 XSLT 2.0 版本可以正常工作...@Dimitre 的最新 XSLT 1.0 版本和这个 EXSL 版本无法替换任何东西。我会努力的……
【解决方案7】:

在@Frerich-Raabe 发布了一个社区 wiki 答案后,该答案结合了迄今为止的建议并(根据他的数据)实现了 76 倍的加速——恭喜大家!!!

我忍不住不再走下去:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:variable name="specialLoutChars">"@|#\}&amp;^~/{</xsl:variable>

 <xsl:key name="kTextBySpecChars" match="text()"
  use="string-length(translate(., '&quot;@|#\}&amp;^~/', '') = string-length(.))"/>

 <xsl:template match="node()|@*">
  <xsl:copy>
   <xsl:apply-templates select="node()|@*"/>
  </xsl:copy>
 </xsl:template>

 <xsl:template match="text()[key('kTextBySpecChars', 'true')]" name="escape-text">
  <xsl:param name="s" select="."/>
  <xsl:param name="len" select="string-length($s)"/>

  <xsl:choose>
    <xsl:when test="$len >= 2">
        <xsl:variable name="halflen" select="round($len div 2)"/>
        <xsl:call-template name="escape-text">
            <xsl:with-param name="s" select="substring($s, 1, $halflen)"/>
            <xsl:with-param name="len" select="$halflen"/>
        </xsl:call-template>
        <xsl:call-template name="escape-text">
            <xsl:with-param name="s" select="substring($s, $halflen + 1)"/>
            <xsl:with-param name="len" select="$len - $halflen"/>
        </xsl:call-template>
    </xsl:when>
    <xsl:when test="$len = 1">
        <xsl:choose>
            <!-- Common case: the character at hand needs no escaping at all -->
            <xsl:when test="not(contains($specialLoutChars, $s))">
                <xsl:value-of select="$s"/>
            </xsl:when>
            <xsl:when test="$s = '&quot;' or $s = '\'">
                <xsl:text>&quot;\</xsl:text>
                <xsl:value-of select="$s"/>
                <xsl:text>&quot;</xsl:text>
            </xsl:when>
            <xsl:otherwise>
                <xsl:text>&quot;</xsl:text>
                <xsl:value-of select="$s"/>
                <xsl:text>&quot;</xsl:text>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:when>
  </xsl:choose>
 </xsl:template>
</xsl:stylesheet>

这种转换(根据我的数据)实现了 1.5 倍的进一步加速。所以总加速应该在100倍以上。

【讨论】:

  • 非常感谢您的努力!不幸的是,在这里使用xsl:key 并没有改善我的情况,相反的情况。除此之外,你不能在kTextBySpecChars 键中使用$specialLoutChars 吗?这样它也不会缺少“{”字符。 xsl:key 是否应该“编译”给定的 XPath,以便您可以更快地使用它?我很好奇这种变化背后的原因是什么。
  • @Frerich-Raabe:这意味着您的数据与我的非常不同。如果您有许多文本节点,您正在转义所有(或大部分)节点并且其中一些不包含特殊字符,则该键将有所帮助。在 XSLT 1.0 中,&lt;xsl:key&gt; 定义不允许包含变量引用:w3.org/TR/xslt#keyuse 属性或 match 属性的值包含 VariableReference 是错误的。”
  • @Frerich-Raabe,由于我们正在进行大量基准测试,您能否在某处发布示例数据集(也许 @Dimitre 也可以),以便我们将苹果与苹果进行比较?
  • @Alejandro:我的(对于 Saxon 9)是:-Xms1024M -Xmx1024M -jar C:\Xml\Parsers\Saxon\9.0.0.4\J\saxon9.jar -t -repeat:3 - o %out% %xml% %xsl% %param[ name="value"]%
  • @Alejandro:我的(对于 Saxon 6.5.5)是:%xml% %xsl% %out%%param[ name="value"]% 我正在启动 Saxon.bat:@echo off "C:\Program Files\Java\jre1.6.0_04\bin\java" com.icl.saxon.StyleSheet -t %1 %2 > %3 pause
【解决方案8】:

好的,我会加入。虽然没有优化 XSLT 1.0 版本那么有趣,但您确实说 XSLT 2.0 解决方案是受欢迎的,所以这是我的。

<xsl:stylesheet version="2.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

    <xsl:template name="escape-text" match="text()" priority="2">
        <xsl:variable name="regex1">[@|#}&amp;^~/{]</xsl:variable>
        <xsl:variable name="replace1">"$0"</xsl:variable>
        <xsl:variable name="regex2">["\\]</xsl:variable>
        <xsl:variable name="replace2">"\\$0"</xsl:variable>
        <xsl:value-of select='replace(replace(., $regex2, $replace2),
                              $regex1, $replace1)'/>
    </xsl:template>

    <xsl:template match="node()|@*">
        <xsl:copy>
            <xsl:apply-templates select="node()|@*"/>
        </xsl:copy>
    </xsl:template>

</xsl:stylesheet>

这只是使用正则表达式 replace() 将 \ 或 " 分别替换为 "\" 或 "\"";与另一个正则表达式 replace() 组成,用引号将任何其他可转义字符括起来。

在我的测试中,这比 Dimitre 最新的 XSLT 1.0 产品的性能多 2 倍。(但我自己编写了测试数据,其他条件可能是特殊的,所以我想知道其他人得到了什么结果。)

为什么性能变慢?我只能猜测是因为搜索正则表达式比搜索固定字符串要慢。

更新:使用分析字符串

根据@Alejandro 的建议,这里使用的是分析字符串:

<xsl:template name="escape-text" match="text()" priority="2">
    <xsl:analyze-string select="." regex='([@|#}}&amp;^~/{{])|(["\\])'>
        <xsl:matching-substring>
            <xsl:choose>
                <xsl:when test="regex-group(1)">"<xsl:value-of select="."/>"</xsl:when>
                <xsl:otherwise>"\<xsl:value-of select="."/>"</xsl:otherwise>
            </xsl:choose>
        </xsl:matching-substring>
        <xsl:non-matching-substring><xsl:value-of select="."/></xsl:non-matching-substring>
    </xsl:analyze-string>
</xsl:template>

虽然这似乎是个好主意,但不幸的是,它并没有给我们带来性能上的胜利:在我的设置中,它始终需要大约 14 秒才能完成,而上面的 replace() 模板需要 1 - 1.4 秒。称其为 10-14 倍 减速。 :-( 这表明在 XSLT 级别断开和连接大量大字符串比在内置函数中遍历大字符串两次要昂贵得多。

【讨论】:

  • @LarsH:你正在遍历字符串两次。
  • @LarsH: 你应该尝试analize-string([@|#}&amp;amp;^~/{])|(["\\]),然后在regexp-group(1) 引用时匹配子字符串choose,否则引用和反斜杠,因为不匹配子字符串只是输出。
  • @Alejandro,关于遍历字符串两次的好点。我仍然认为这比在高级 XSLT 中拆分和连接如此多的字符串要快。我会试试你的分析字符串建议。
  • @Alejandro 查看我对此答案的更新,关于分析字符串。
  • @LarsH:这太奇怪了!使用 Altova 进行测试(我无法设置严格的 Saxon 进行基准测试),analize-string 解决方案的运行速度比 replace 解决方案快 50%。
猜你喜欢
  • 2014-12-13
  • 2017-07-18
  • 2023-03-04
  • 1970-01-01
  • 2011-12-31
  • 2012-03-30
  • 1970-01-01
  • 2018-08-23
  • 2011-04-08
相关资源
最近更新 更多