XSLT作为替换应用程序:实例化“XML宏”

时间:2017-06-16 14:47:57

标签: xml xslt xslt-1.0

我想使用XSLT根据不同XML文件中指定的键值映射从一个XML文件创建XML模式的多个实例,以供参考,参见https://en.wikipedia.org/wiki/Substitution_%28logic%29。 应将XSL应用于键值映射(替换),其中包含对模式的引用。 我猜这可以用一般的(也可能是优雅的)解决方案来完成,但我不知道如何继续。

以下是一个例子:

模式:(pattern.xml)

library(dplyr)

as.winner <- test_data %>% group_by(winner) %>% summarise(winner_sum = sum(winner_points))
as.loser <- test_data %>% group_by(loser) %>% summarise(loser_sum = sum(loser_points))
names(as.winner)[1] <- 'player'
names(as.loser)[1] <- 'player'
totals <- merge(as.winner, as.loser, by = 'player', all.x = T, all.y = T)
totals[is.na(totals)] <- 0
totals <- transform(totals, total_points = winner_sum + loser_sum)
totals

键值映射:(substitution.xml)更新:从input.xml重命名)

更新:替换应包含对模式的引用,并在常用浏览器中工作,我理解这需要使用XSLT 1.0。

<?xml version="1.0" encoding="UTF-8"?>
<pattern>
  <key1>a</key1>
  <element2>key2</element2>
  <element3 attribute="key2">key1</element3>
</pattern>

将地图“应用”到模式的结果应该是:

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="apply-substitution.xsl"?>
<root pattern="http://example.com/link-to-pattern.xml">
  <copy>
    <key1>VALUE1</key1>
    <key2>VALUE2</key2>
    <key3>VALUE3</key3>
  </copy>
    <copy>
    <key1>VALUE3</key1>
    <key2>VALUE4</key2>
    <key3>VALUE5</key3>
  </copy>
</root>

...创建模式的实例,其中所有元素名称,属性名称和值(匹配整个值,例如,不是子字符串)按键值映射的指定放置。

我的用例是模式是一个非常复杂的XML结构(不是我设计的),我想创建(可能非常大的一组)实例 - 保持模式的结构和大部分内容原样,但改变一些节点,这些节点可能出现在模式XML的任何位置,即元素名称,属性名称和值(匹配完整值)。模式实例的总和在模式XML语言中形成完整且合理的数据集。将模式视为一种由input.xml中的每个<?xml version="1.0" encoding="UTF-8"?> <some_root> <!-- instance 1 --> <pattern> <VALUE1>a</VALUE1> <element2>VALUE2</element2> <element3 attribute="VALUE2">VALUE1</element3> </pattern> <!-- instance 2 --> <pattern> <VALUE3>a</VALUE3> <element2>VALUE4</element2> <element3 attribute="VALUE4">VALUE3</element3> </pattern> </some_root> 调用或实例化的宏。

到目前为止,我的解决方案只复制模式而不替换值:

<copy>

更新
以下是一个开始(但是,包含@Eiríkr的回答所指出的错误,并使用对pattern.xml的硬编码引用)。它替换元素,属性名称和值以及文本节点---并递归应用。

[removed since irrelevant due to updates. New version below]

1 个答案:

答案 0 :(得分:1)

答案帖子中的XSL代码

你问,“是否有可能避免在任何地方传递$ map?”

如果您要以这种方式交叉应用不同的XML结构,那么您可能需要传入参数。

除此之外,我注意到以下内容:

  • 在答案的XSL代码中,您有两个文件:input.xmlpattern.xml。这些名称具有误导性 - 您需要将input.xml中的模式应用于pattern.xml中的XML结构。为了获得更流畅的流程和更自然的XSL-y处理方式,我们希望引用input.xml,并实际输入pattern.xml。这样做也可以使您的XSL样式表更加灵活 - 您可以将其传递给任何文件而不是pattern.xml,而无需编辑XSL本身来更改硬编码路径。

  • 简单地包含您在XSL本身的外部input.xml文件中定义的所有键值替换可能更有意义。

  • 作为进一步考虑,如果您使用match模板与<xsl:apply-templates/>一起使用,而不是使用name d模板,则XSL本质上是递归的。

  • 扩展<pattern> XML结构时,代码的输出不符合您声明的规范。例如,按如下方式扩展XML:

    <pattern>
        <key1>a</key1>
        <element2>key2</element2>
        <element3 attribute="key2">key1</element3>
        <element4>
            <key3>something <key1>and something else with <another key1="key1">key2</another> content.</key1></key3>
        </element4>
    </pattern>
    

    ...并且应用您的代码会产生太多输出:

    <root>
       <pattern>
          <VALUE1>a<VALUE3>something <VALUE1>and something else with <another VALUE1="VALUE1">VALUE2</another>
                </VALUE1>
             </VALUE3>
          </VALUE1>
          <element2>VALUE2<VALUE3>something <VALUE1>and something else with <another VALUE1="VALUE1">VALUE2</another>
                </VALUE1>
             </VALUE3>
          </element2>
          <element3 attribute="VALUE2">VALUE1<VALUE3>something <VALUE1>and something else with <another VALUE1="VALUE1">VALUE2</another>
                </VALUE1>
             </VALUE3>
          </element3>
          <element4>
            <VALUE3>something <VALUE1>and something else with <another VALUE1="VALUE1">VALUE2</another>
                </VALUE1>
             </VALUE3>
          </element4>
       </pattern>
       <pattern>
          <VALUE3>a<VALUE5>something <VALUE3>and something else with <another VALUE3="VALUE3">VALUE4</another>
                </VALUE3>
             </VALUE5>
          </VALUE3>
          <element2>VALUE4<VALUE5>something <VALUE3>and something else with <another VALUE3="VALUE3">VALUE4</another>
                </VALUE3>
             </VALUE5>
          </element2>
          <element3 attribute="VALUE4">VALUE3<VALUE5>something <VALUE3>and something else with <another VALUE3="VALUE3">VALUE4</another>
                </VALUE3>
             </VALUE5>
          </element3>
          <element4>
            <VALUE5>something <VALUE3>and something else with <another VALUE3="VALUE3">VALUE4</another>
                </VALUE3>
             </VALUE5>
          </element4>
       </pattern>
    </root>
    

    如果我已正确理解您的要求,那么您需要的是:

    <root>
       <pattern>
          <VALUE1>a</VALUE1>
          <element2>VALUE2</element2>
          <element3 attribute="VALUE2">VALUE1</element3>
          <element4>
            <VALUE3>something <VALUE1>and something else with <another VALUE1="VALUE1">VALUE2</another> content.</VALUE1>
             </VALUE3>
          </element4>
       </pattern>
       <pattern>
          <VALUE3>a</VALUE3>
          <element2>VALUE4</element2>
          <element3 attribute="VALUE4">VALUE3</element3>
          <element4>
            <VALUE5>something <VALUE3>and something else with <another VALUE3="VALUE3">VALUE4</another> content.</VALUE3>
             </VALUE5>
          </element4>
       </pattern>
    </root>
    

另一种方法

在咀嚼这一段时间以确保我理解你要做的事情之后,我意识到我最初在理解你的代码时遇到的一部分就是你开始使用input.xml文件并处理该上下文,然后您更改为处理pattern.xml文件并处理该上下文。

当您调用模板并希望该模板处理某些内容(而不仅仅是返回静态值或XML结构)时,您必须明确传入该内容,这会使上下文混乱。以下是使用<xsl:apply-templates>代替<xsl:call-template name="...">解决同一问题的另一种方法。

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

    <xsl:output omit-xml-declaration="yes" indent="yes"/>

    <!-- We need to create output based on processing the 
        `pattern.xml` file, so we use that as our input. -->

    <!-- Despite its name, the `input.xml` file actually contains
        the pattern we wish to apply to the incoming XML file. 
        We'll store this pattern in a variable here. -->
    <xsl:variable name="replacements" select="document('input.xml')">
        <!-- Rather than relying on external files, we could leave out the
            `select` attribute and just put all the replacement information 
            right here in the XSL file. --><!--
        <root>
            <copy>
                <key1>VALUE1</key1>
                <key2>VALUE2</key2>
                <key3>VALUE3</key3>
            </copy>
            <copy>
                <key1>VALUE3</key1>
                <key2>VALUE4</key2>
                <key3>VALUE5</key3>
            </copy>
        </root>-->
    </xsl:variable>

    <!-- We assume that we don't know the names of any of the elements
        or attributes in the file we're processing. 
        If you _do_ know any of those names, specify them: literal
        XPaths tend to be faster. -->
    <xsl:template match="/">
        <!-- We put the top-level XML structure into a variable so 
            we can refer to it from within the for-each. If we 
            don't do this, we can't get at it: within the for-each,
            both the `.` operator and the `current()` function
            refer to the context item for the `for-each` call,
            and _not_ to the context item for the `template match`
            statement.  The `child::` axis in the `select` statement
            is optional; we could just use `select="*"` instead.
            I added the `child::` to make it clear for humans. -->
        <xsl:variable name="top" select="child::*"/>
        <!-- At the root level, we want to create a copy of the 
            topmost element from the $replacements variable. -->
        <xsl:element name="{name($replacements/*[1])}">
            <!-- We want to apply the key-value pairs to the topmost
                XML structure from the input file, once for each 
                instance of <copy> in the $replacements variable. -->
            <xsl:for-each select="$replacements/*/copy">
                <!-- Now we apply our templates to the input
                    file structure, which we stored in $top. -->
                <xsl:apply-templates select="$top">
                    <!-- We do need to have some way of knowing which
                        `copy` we want to reference, so we pass this
                        along as a parameter. -->
                    <xsl:with-param name="this" select="."/>
                </xsl:apply-templates>
            </xsl:for-each>
        </xsl:element>
    </xsl:template>

    <!-- This matches all elements. -->
    <xsl:template match="*">
        <xsl:param name="this"/>
        <!-- We compare the current element name against all the 
            element names in $this, and use that value if found,
            or the current element name if not found. -->
        <xsl:element name="{if (name(.) = $this//*/name())
            then ($this//*[name() = name(current())])
            else (name(.))}">
            <xsl:apply-templates select="*|@*|text()">
                <xsl:with-param name="this" select="$this"/>
            </xsl:apply-templates>
        </xsl:element>
    </xsl:template>

    <!-- This matches all atributes.  We evaluate both attribute
        names and attribute values, allowing us to replace either
        or both, if they match the key-value pairs in $this.-->
    <xsl:template match="@*">
        <xsl:param name="this"/>
        <xsl:variable name="attname" select="if (name(.) = $this//*/name())
            then ($this//*[name() = name(current())])
            else (name(.))"/>
        <xsl:variable name="attval" select="if (. = $this//*/name())
            then ($this//*[name() = current()])
            else (.)"/>
        <xsl:attribute name="{$attname}" select="$attval"/>
    </xsl:template>

    <!-- This matches all text. -->
    <xsl:template match="text()">
        <xsl:param name="this"/>
        <xsl:value-of select="if (. = $this//*/name())
            then ($this//*[name() = current()])
            else (.)"/>
    </xsl:template>

</xsl:stylesheet>

从您的帖子来看,您是否仅限于XSL 1.0并不完全清楚。我使用上面的XSL 2.0,XPath表达式中的if () then () else ()逻辑仅适用于XSL 2.0+。在XSL 1.0中也不支持在XPath表达式的末尾使用*/name()。这两个问题都可以根据需要进行编码。

这个XSL通过添加<element4>从扩展输入XML生成(我相信预期?)更短的XML输出。

更新 - XSL 1.0版

以下内容大致相当于上面的XSL,但重新设计为与XSL 1.0兼容。主要区别:

  • $replacements变量中引用的文件名更改为substitution.xml
  • 创建了一个命名模板,用于替换XSL 2.0+ XPath表达式中内联的if () then () else ()逻辑。
  • 在模板中创建变量以调用该模板并存储值。

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

    <xsl:output omit-xml-declaration="yes" indent="yes"/>

    <!-- Filename changed to match the new requirements. -->
    <xsl:variable name="replacements" select="document('substitution.xml')"/>

    <xsl:template match="/">
        <xsl:variable name="top" select="*"/>
        <xsl:element name="{name($replacements/*[1])}">
            <xsl:for-each select="$replacements/*/copy">
                <xsl:apply-templates select="$top">
                    <xsl:with-param name="this" select="."/>
                </xsl:apply-templates>
            </xsl:for-each>
        </xsl:element>
    </xsl:template>

    <!-- This matches all elements. -->
    <xsl:template match="*">
        <xsl:param name="this"/>
        <xsl:variable name="elem_name">
            <!-- XSL 1.0 does not support `if / then / else` logic in
                XPath statements, so we use this variable instead to
                call a named template containing the logic, and then
                to store the resulting value. -->
            <xsl:call-template name="get_sub_value">
                <xsl:with-param name="this" select="$this"/>
                <xsl:with-param name="val" select="name()"/>
            </xsl:call-template>
        </xsl:variable>
        <xsl:element name="{$elem_name}">
            <xsl:apply-templates select="*|@*|text()">
                <xsl:with-param name="this" select="$this"/>
            </xsl:apply-templates>
        </xsl:element>
    </xsl:template>

    <!-- This matches all atributes. -->
    <xsl:template match="@*">
        <xsl:param name="this"/>
        <xsl:variable name="attname">
            <xsl:call-template name="get_sub_value">
                <xsl:with-param name="this" select="$this"/>
                <xsl:with-param name="val" select="name()"/>
            </xsl:call-template>
        </xsl:variable>

        <xsl:attribute name="{$attname}">
            <!-- For attribute alues, we just call `get_sub_value` directly. -->
            <xsl:call-template name="get_sub_value">
                <xsl:with-param name="this" select="$this"/>
                <xsl:with-param name="val" select="."/>
            </xsl:call-template>
        </xsl:attribute>
    </xsl:template>

    <!-- This matches all text. -->
    <xsl:template match="text()">
        <xsl:param name="this"/>
        <!-- For straight text, we just call `get_sub_value` directly. -->
        <xsl:call-template name="get_sub_value">
            <xsl:with-param name="this" select="$this"/>
            <xsl:with-param name="val" select="."/>
        </xsl:call-template>
    </xsl:template>

    <!-- Named template to handle the logic of looking for key-value pairs
        in the relevant `<copy>` element from the `substitution.xml` file. -->
    <xsl:template name="get_sub_value">
        <xsl:param name="this"/>
        <xsl:param name="val"/>
        <xsl:choose>
            <xsl:when test="$this//*[name() = $val]">
                <xsl:value-of select="$this//*[name() = $val]"/>
            </xsl:when>
            <xsl:otherwise>
                <xsl:value-of select="$val"/>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:template>

</xsl:stylesheet>

我对您在浏览器中执行此操作的用例感到有点困惑,但您知道自己需要什么。 :)

使用Xalan,Saxon 6.5和Saxon 9.6.0.7处理器在Oxygen XML v17.1中测试。唯一的区别是缩进。