通过XSLT从另一个XML结构中获取XML信息

3
我正在使用XSLT 1.0,并尝试完成以下操作:
我有一个名为1.xml的文件:
<root>
    <elem1>value1</elem1>
    <elem2>
        <elem3>
            <param1>value2</param1>
            <param2>value3</param2>
        </elem3>
    </elem2>
    <elem4>
        <param3>value4</param3>
    </elem4>
</root>

现在客户端传递给我另一个XML文件,告诉我他想要我返回什么元素(可能会因客户而异),即:
<root>
  <RequiredElements>
    <elementName>elem1</elementName>
    <elementName>elem2/elem3/param1</elementName>
  </RequiredElements>
</root>

这意味着在这种情况下,我应该创建另一个具有以下结构的XML文件:
<root>
  <elem1>value1</elem1>
  <elem2>
    <elem3>
      <param1>value2</param1>
    </elem3>
  </elem2>
</root>

我尝试使用XSLT(除此之外没有其他编程语言)来获得所需的结构,但无法做到。
有任何想法或指针应该怎么做?
感谢您的帮助。
2个回答

3
这个简单的转换(如果参数文档没有被内联,则少于30行):
<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="*"/>

 <my:paramDoc>
        <root>
          <RequiredElements>
            <elementName>elem1</elementName>
            <elementName>elem2/elem3/param1</elementName>
          </RequiredElements>
        </root>
 </my:paramDoc>

 <xsl:variable name="vPaths" select=
   "document('')/*/my:paramDoc/*/*/*"/>

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

 <xsl:template match="*/*">
  <xsl:variable name="vPath">
    <xsl:for-each select=
      "ancestor-or-self::*[not(position()=last())]">
      <xsl:value-of select="concat(name(), '/')"/>
    </xsl:for-each>
  </xsl:variable>

   <xsl:if test=
   "$vPaths[starts-with(concat(.,'/'), $vPath)]">
    <xsl:call-template name="identity"/>
   </xsl:if>
 </xsl:template>
</xsl:stylesheet>

当应用于提供的XML文档时:
<root>
    <elem1>value1</elem1>
    <elem2>
        <elem3>
            <param1>value2</param1>
            <param2>value3</param2>
        </elem3>
    </elem2>
    <elem4>
        <param3>value4</param3>
    </elem4>
</root>

产生所需的、正确的结果

<root>
   <elem1>value1</elem1>
   <elem2>
      <elem3>
         <param1>value2</param1>
      </elem3>
   </elem2>
</root>

解释:

  1. 身份规则会完全复制每个节点。

  2. 有一个单一的模板匹配任何父元素为元素的元素。在这个模板中执行以下操作:

  3. 为当前(匹配的)节点生成相对XPath表达式字符串,其上下文节点为文档的顶层元素。

  4. 如果此相对路径是参数传递的文档中指定的某个表达式的前缀,则对此元素执行身份变换。


太棒了!我甚至不知道你可以使用嵌入在xsl:variable中的元素构建变量。在正确的人手中,XSLT可以如此优雅。 - G_H
@G_H:是的,XSLT可以非常强大和优雅。只需看看FXSL(2.0适用于XSLT 2.0)就知道了。 - Dimitre Novatchev

1

到目前为止,这是我能想到的最好方案:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

    <xsl:output method="xml" encoding="UTF-8" indent="yes" version="1.0"/>

    <xsl:template match="/root">
        <xsl:copy>
            <xsl:for-each select="*">
                <xsl:call-template name="check-if-allowed">
                    <xsl:with-param name="path" select="local-name(.)"/>
                </xsl:call-template>
            </xsl:for-each>
        </xsl:copy>
    </xsl:template>

    <xsl:template name="check-if-allowed">

        <xsl:param name="path"/>

        <xsl:copy>

            <xsl:if test="$path = document('filter.xml')//RequiredElements/elementName/text()">
                <xsl:attribute name="flagged-by-filter">true</xsl:attribute>
            </xsl:if>

            <xsl:choose>
                <xsl:when test="*">
                    <xsl:for-each select="*">
                        <xsl:call-template name="check-if-allowed">
                            <xsl:with-param name="path" select="concat($path, '/', local-name(.))"/>
                        </xsl:call-template>
                    </xsl:for-each>
                </xsl:when>
                <xsl:otherwise>
                    <xsl:copy-of select="text()"/>
                </xsl:otherwise>
            </xsl:choose>

        </xsl:copy>

    </xsl:template>

</xsl:stylesheet>

让我们来看一下:第一个模板匹配您的/root元素。它将创建一个浅拷贝,然后为每个子元素调用模板check-if-allowed,并将该子元素的本地名称作为参数path传递。

check-if-allowed模板接受名为path的参数。它会创建其节点的浅拷贝,然后测试path参数是否包含在从文档filter.xml中选择的内容中。这必须是指向您的第二个文档的路径,其中包含允许的路径列表。如果测试成功(即如果path参数出现为filter.xmlelementName的文本内容),则还会添加一个名为flagged-by-filter且值为true的属性。

接下来,xsl:choose 将执行两件事情中的一件。如果当前元素存在任何子元素,则会在每次调用相同的 check-if-allowed 模板上使用一个 path 参数,该参数是添加到该元素的局部名称。如果没有子元素,则它将简单地复制当前元素中可能存在的任何文本。

请注意,这只是一个非常不完整的解决方案。它忽略了属性,并且对于混合内容(即文本与元素混合)无效。

第二个样式表可以应用于第一个样式表的结果以进行实际过滤:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

    <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>

    <xsl:template match="/">
        <xsl:copy><xsl:apply-templates select="*"/></xsl:copy>
    </xsl:template>

    <xsl:template match="*[//*[@flagged-by-filter='true']]">
        <xsl:copy><xsl:apply-templates select="*"/></xsl:copy>
    </xsl:template>

    <xsl:template match="*[* and not(//*[@flagged-by-filter='true']) and @flagged-by-filter='true']">
        <xsl:copy></xsl:copy>
    </xsl:template>

    <xsl:template match="*[not(*) and @flagged-by-filter='true']">
        <xsl:copy-of select="."/>
    </xsl:template>

    <xsl:template match="*[not(*) and not(@flagged-by-filter='true')]"/>

</xsl:stylesheet>

这里的工作非常基础。它不处理属性,并且仍然存在一个问题,因为elem4总是以某种原因通过。不确定为什么,调试器显示它始终匹配第二个模板,但我无法想象如何做到这一点。

这是更常见的声明性XSLT样式。第一个模板匹配根。它只是复制它,然后将模板应用于其子元素。第二个模板匹配具有带有flagged-by-filter="true"属性的后代元素的任何元素。第三个模板匹配至少有一个子元素,具有标记过滤器属性但没有带有该属性的后代元素的任何元素。第四个模板匹配没有子元素但本身被标记的任何元素。最后一个模板匹配没有子元素也没有被标记的任何元素。

虽然这不是您问题的完整解决方案,但我希望它足以让您上路。如果您不能自由地应用两个连续的XSLT转换,则需要找到一种方法来按照您的要求应用第一个XSLT中的内容。我想不出如何做到这一点,但也许其他人有好主意。

说了这么多,对于这样的问题,要么不使用XSLT,要么根据您的过滤器XML以编程方式生成样式表。由于我们不断地在另一个文档上应用XPath表达式,因此以上方法在性能方面非常糟糕。不仅如此,每次都必须完全解析它。我曾经有过类似的设置,发现性能非常差。因此,我将对第二个文档的访问更改为使用预加载数据调用Java方法的扩展函数。
XSLT在某些方面非常好,但是当您涉及到这样的复杂性时,我认为最好与另一种语言结合使用。

@_G_H:你的回答似乎在暗示每次调用document()函数都会导致XML文档被重新解析。这是不正确的,事实上如果给定的XSLT处理器正在重新解析,则它是不符合规范的。因为正如W3C XSLT规范所定义的那样,document()函数必须是稳定的,也就是说,每当使用相同参数调用时,它应该始终产生完全相同的节点。 - Dimitre Novatchev
@DimitreNovatchev 有趣...我观察到使用document()时性能下降了,虽然当你将第二个文档拖入XPath表达式所评估的过程中时,这可能是可以预料的。第二个文档基本上是键值对,意味着搜索属性值而不是元素名称。这不是一个很好的方法。我将其更改为将属性文档读入Properties对象,并通过扩展函数简单地访问该对象。性能有了很大提升。 - G_H
如果键值对文档不小,搜索它将会导致性能瓶颈。在这里正确的解决方案是使用<xsl:key>key()函数--如果执行多个搜索,这将导致显着的性能提升。 - Dimitre Novatchev
@DimitreNovatchev 那也可能行得通。不过,这并不是很重要。它用于在用户门户中呈现EDI-XML消息,但我很快就会重新设计它。新版本使用了更易理解的XML格式。 - G_H

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接