XSLT 通用解决方案,用于从 XML 中获取分层 html 表

Posted

技术标签:

【中文标题】XSLT 通用解决方案,用于从 XML 中获取分层 html 表【英文标题】:XSLT generic solution to get hierarchical html table out of XML 【发布时间】:2021-08-16 14:35:41 【问题描述】:

xml 格式是存储任何分层数据的好方法。作为一个例子,我们使用动物的分类

<?xml version='1.0' ?>
<classificationOfAnimals>
    <vertebrates>
        <warmBlooded>
            <mammals>
                <bear>
                    <individualNamedPeter/>
                    <individualNamedTed/>
                </bear>
                <tiger/>
                <whale/>
            </mammals>
            <birds>
                <ostrich/>
                <peacock/>
                <eagle/>
            </birds>
        </warmBlooded>
        <coldBlooded>
            <fish>
                <salmon/>
                <goldfish/>
                <guppy/>
            </fish>
            <reptiles>
                <turtle/>
                <crocodile/>
                <snake/>                
            </reptiles>
            <amphibians>
                <frog/>
                <toad/>
                <newt/>
            </amphibians>
        </coldBlooded>
    </vertebrates>
    <invertebrates>
        <withJoinedLegs>
            <with3PairsOfLegs>
                <ant/>
                <cockroch>
                    <fatherDan>
                        <sonBob>
                            <bruce/>
                            <lenny/>
                            <susan/>
                        </sonBob>
                    </fatherDan>
                </cockroch>
                <ladybug/>
            </with3PairsOfLegs>
            <withMoreThan3PairsOfLegs>
                <scorpion/>
                <spider/>
                <millipede/>
            </withMoreThan3PairsOfLegs>
        </withJoinedLegs>
        <withoutLegs>
            <wormLike>
                <earthworm/>
                <leech/>
            </wormLike>
            <notWormLike>
                <flukeWorm>
                    <individualNamedLance/>
                </flukeWorm>                
                <tapeWorm/>
            </notWormLike>
        </withoutLegs>
    </invertebrates>
</classificationOfAnimals>

我想从这个 xml 数据中创建一个代表动物分类层次结构的 html 表。

因此我想要一张这样的表格

转换应使用 XSLT 完成,它应该是一种通用方法,可以处理任何类型的层次结构,无论需要多少行和列,或者表必须是锯齿状的,或者层次结构有多深。 我们在这里遇到的一般问题是 html 表不会像 xml 那样具有层次结构。 html 表仅由行和列组成。要获得像屏幕截图中那样的层次结构,我们需要为表的td 元素使用属性rowspan。使用此属性,一个单元格跨越多行。 因此我们需要这样的表格

<table>
    <tr>
        <td rowspan="28">classificationOfAnimals</td>
        <td rowspan="16">vertebrates</td>
        <td rowspan="7">warmBlooded</td>
        <td rowspan="4">mammals</td>
        <td rowspan="2">bear</td>
        <td>individualNamedPeter</td>
    </tr>
    <tr>
        <td>individualNamedTed</td>
    </tr>
    <tr>
        <td>tiger</td>
    </tr>
    <tr>
        <td>whale</td>
    </tr>
    <tr>
        <td rowspan="3">birds</td>
        <td>ostrich</td>
    </tr>
    <tr>
        <td>peacock</td>
    </tr>
    <tr>
        <td>eagle</td>
    </tr>
    <tr>
        <td rowspan="9">coldBlooded</td>
        <td rowspan="3">fish</td>
        <td>salmon</td>
    </tr>
    <tr>
        <td>goldfish</td>
    </tr>
    <tr>
        <td>guppy</td>
    </tr>
    <tr>
        <td rowspan="3">reptiles</td>
        <td>turtle</td>
    </tr>
    <tr>
        <td>crocodile</td>
    </tr>
    <tr>
        <td>snake</td>
    </tr>
    <tr>
        <td rowspan="3">amphibians</td>
        <td>frog</td>
    </tr>
    <tr>
        <td>toad</td>
    </tr>
    <tr>
        <td>newt</td>
    </tr>
    <tr>
        <td rowspan="12">invertebrates</td>
        <td rowspan="8">withJoinedLegs</td>
        <td rowspan="5">with3PairsOfLegs</td>
        <td>ant</td>
    </tr>
    <tr>
        <td rowspan="3">cockroch</td>
        <td rowspan="3">fatherDan</td>
        <td rowspan="3">sonBob</td>
        <td>bruce</td>
    </tr>
    <tr>
        <td>lenny</td>
    </tr>
    <tr>
        <td>susan</td>
    </tr>
    <tr>
        <td>ladybug</td>
    </tr>
    <tr>
        <td rowspan="3">withMoreThan3PairsOfLegs</td>
        <td>scorpion</td>
    </tr>
    <tr>
        <td>spider</td>
    </tr>
    <tr>
        <td>millipede</td>
    </tr>
    <tr>
        <td rowspan="4">withoutLegs</td>
        <td rowspan="2">wormLike</td>
        <td>earthworm</td>
    </tr>
    <tr>
        <td>leech</td>
    </tr>
    <tr>
        <td rowspan="2">notWormLike</td>
        <td rowspan="1">flukeWorm</td>
        <td>individualNamedLance</td>
    </tr>
    <tr>
        <td>tapeWorm</td>
    </tr>
</table>

【问题讨论】:

您确定将单个物种作为元素名称是个好主意吗?通常我希望看到更像&lt;species name="cockroach"/&gt; 而不是&lt;cockroach&gt; 的东西——或者可能是门/王国/属或任何代替物种的东西。 是的,你是对的,在我用于特殊目的的原始实现中,我只有几个不同的元素名称,区别在于 name 属性。但这对 XSLT 没有影响,我只是将 @name 替换为 name(...)。我只想要一个简单的 xml 作为示例。 【参考方案1】:

我们可以在 html 表中看到,第一行包含一个用于表示每个层次结构级别的单元格,该单元格表示为一列。这意味着它必须生成一行,其中包含从最高到最深层次结构级别的所有元素。最深层次结构级别的元素是没有进一步后代的元素。为了得到这些,我们可以使用这个 xpath 表达式

<xsl:for-each select="./descendant::*[not(./descendant::*)]">

这意味着获取所有本身没有后代的后代。 然后我们处于最深的层次结构级别,我们可以从后面,从最深到***别建立行。所以现在我们需要得到这个最深元素的祖先。但前提是当前在作用域中的最深元素是其父元素的第一个子元素,在示例中为 individualNamedPeter,因为 tiger 和后续元素必须在一行中。

<!-- is the element the first child of its parent -->
<xsl:if test="generate-id() = generate-id(parent::*/*[1])">

然后用祖先从后面填充行以获得bearmammalswarmBloodedvertebratesclassificationOfAnimals,我们将使用递归调用的模板

 <!-- in the row with the first child of its parent we need as well the ancestors -->
 <xsl:call-template name="printTheAncestors">                        
     <xsl:with-param name="ancestors" select="./ancestor-or-self::*"/>
 </xsl:call-template>

传递的参数都是祖先或自身元素

<xsl:template name="printTheAncestors">
    <xsl:param name="ancestors"/>
    <xsl:param name="positionNumber" select="count($ancestors)"/>

    <!-- we need only the ancestor if the previous ancestor is the first child of this ancestor -->
    <xsl:if test="generate-id($ancestors[$positionNumber]) = generate-id($ancestors[$positionNumber - 1]/*[1])">

        <xsl:call-template name="printTheAncestors">
            <xsl:with-param name="ancestors" select="$ancestors"/>
            <xsl:with-param name="positionNumber" select="$positionNumber - 1"/>
        </xsl:call-template>            

        <!-- we are getting here the right order of the elements, because after the end of the recursive calling -->
        <!-- the nested called templates are returning one after another from the last to the first call  -->
        <td>
            <!-- the ancestor td cell has to span the count of rows which are holding descendants without further descendants -->
            <xsl:attribute name="rowspan">
                <xsl:value-of select="count($ancestors[$positionNumber - 1]/descendant::*[not(./descendant::*)])"/>
            </xsl:attribute>

            <xsl:value-of select="name($ancestors[$positionNumber - 1])"/>

        </td>  

    </xsl:if>

</xsl:template>

在这个模板中,我们只需要前一个祖先是当前祖先的第一个孩子,我们才需要再次创建祖先,因为我们想要创建例如td warmBlooded 就在我们有mammals 的行中,而不是在有birds 的行中。此外,当我们找到前一个祖先不是它的第一个孩子的第一个祖先时,我们需要停止递归。因为例如当我们迭代birds 时,我们不想为vertebrates 创建一个td,尽管warmBloodedvertebrates 的第一个孩子,所以我们需要在birds 的迭代过程中停在birds .

使用$positionNumber - 1,它从一个祖先到另一个祖先,从最深到最高。

另一个挑战是td 的打印顺序。因为对祖先的检查是从最深处开始的。但是要获得所需的表,我们需要 td classificationOfAnimals 作为第一个元素,依此类推。这是通过在模板的递归调用之后放置td 块来实现的。这导致递归调用的模板在条件不再满足时依次返回,因此最后打印第一个调用模板的td,反之亦然。

我们需要做的最后一件事是确定每个元素的行跨度。

<xsl:value-of select="count($ancestors[$positionNumber - 1]/descendant::*[not(./descendant::*)])"/>

完整样式表

<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet version="2.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="html" encoding="ISO-8859-1"/>


    <xsl:template match="/">

        <html>

            <head>
                <title>Hierarchical html table out of xml</title>                
                <link rel="stylesheet" type="text/css" href="../whereIsWhichAttributeUsed.css"/>
            </head>

            <body>

                <h1>Hierarchical html table out of xml</h1>

                <div>                
                    <table>
                        <!-- we need a row for each element which has no further descendants -->
                        <xsl:for-each select="./descendant::*[not(./descendant::*)]">
                            <tr>
                                <!-- is the element the first child of its parent -->
                                <xsl:if test="generate-id() = generate-id(parent::*/*[1])">
                                    <!-- in the row with the first child of its parent we need as well the ancestors -->
                                    <xsl:call-template name="printTheAncestors">                        
                                        <xsl:with-param name="ancestors" select="./ancestor-or-self::*"/>
                                    </xsl:call-template>
                                </xsl:if>

                                <!-- the lonely descendant without descendants -->
                                <td>
                                    <xsl:value-of select="name(.)"/>
                                </td>
                            </tr>

                        </xsl:for-each>
                    </table>
                </div>
            </body>
        </html>

    </xsl:template>



    <xsl:template name="printTheAncestors">
        <xsl:param name="ancestors"/>
        <xsl:param name="positionNumber" select="count($ancestors)"/>

        <!-- we need only the ancestor if the previous ancestor is the first child of this ancestor -->
        <xsl:if test="generate-id($ancestors[$positionNumber]) = generate-id($ancestors[$positionNumber - 1]/*[1])">

            <xsl:call-template name="printTheAncestors">
                <xsl:with-param name="ancestors" select="$ancestors"/>
                <xsl:with-param name="positionNumber" select="$positionNumber - 1"/>
            </xsl:call-template>            

            <!-- we are getting here the right order of the elements, because after the end of the recursive calling -->
            <!-- the nested called templates are returning one after another from the last to the first call  -->
            <td>
                <!-- the ancestor td cell has to span the count of rows which are holding descendants without further descendants -->
                <xsl:attribute name="rowspan">
                    <xsl:value-of select="count($ancestors[$positionNumber - 1]/descendant::*[not(./descendant::*)])"/>
                </xsl:attribute>

                <xsl:value-of select="name($ancestors[$positionNumber - 1])"/>

            </td>  

        </xsl:if>

    </xsl:template>


</xsl:stylesheet>

HTML 输出

<html>
   <head>
      <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
      <title>Hierarchical html table out of xml</title>
      <link rel="stylesheet" type="text/css" href="../whereIsWhichAttributeUsed.css">
   </head>
   <body>
      <h1>Hierarchical html table out of xml</h1>
      <div>
         <table>
            <tr>
               <td rowspan="28">classificationOfAnimals</td>
               <td rowspan="16">vertebrates</td>
               <td rowspan="7">warmBlooded</td>
               <td rowspan="4">mammals</td>
               <td rowspan="2">bear</td>
               <td>individualNamedPeter</td>
            </tr>
            <tr>
               <td>individualNamedTed</td>
            </tr>
            <tr>
               <td>tiger</td>
            </tr>
            <tr>
               <td>whale</td>
            </tr>
            <tr>
               <td rowspan="3">birds</td>
               <td>ostrich</td>
            </tr>
            <tr>
               <td>peacock</td>
            </tr>
            <tr>
               <td>eagle</td>
            </tr>
            <tr>
               <td rowspan="9">coldBlooded</td>
               <td rowspan="3">fish</td>
               <td>salmon</td>
            </tr>
            <tr>
               <td>goldfish</td>
            </tr>
            <tr>
               <td>guppy</td>
            </tr>
            <tr>
               <td rowspan="3">reptiles</td>
               <td>turtle</td>
            </tr>
            <tr>
               <td>crocodile</td>
            </tr>
            <tr>
               <td>snake</td>
            </tr>
            <tr>
               <td rowspan="3">amphibians</td>
               <td>frog</td>
            </tr>
            <tr>
               <td>toad</td>
            </tr>
            <tr>
               <td>newt</td>
            </tr>
            <tr>
               <td rowspan="12">invertebrates</td>
               <td rowspan="8">withJoinedLegs</td>
               <td rowspan="5">with3PairsOfLegs</td>
               <td>ant</td>
            </tr>
            <tr>
               <td rowspan="3">cockroch</td>
               <td rowspan="3">fatherDan</td>
               <td rowspan="3">sonBob</td>
               <td>bruce</td>
            </tr>
            <tr>
               <td>lenny</td>
            </tr>
            <tr>
               <td>susan</td>
            </tr>
            <tr>
               <td>ladybug</td>
            </tr>
            <tr>
               <td rowspan="3">withMoreThan3PairsOfLegs</td>
               <td>scorpion</td>
            </tr>
            <tr>
               <td>spider</td>
            </tr>
            <tr>
               <td>millipede</td>
            </tr>
            <tr>
               <td rowspan="4">withoutLegs</td>
               <td rowspan="2">wormLike</td>
               <td>earthworm</td>
            </tr>
            <tr>
               <td>leech</td>
            </tr>
            <tr>
               <td rowspan="2">notWormLike</td>
               <td rowspan="1">flukeWorm</td>
               <td>individualNamedLance</td>
            </tr>
            <tr>
               <td>tapeWorm</td>
            </tr>
         </table>
      </div>
   </body>
</html>

【讨论】:

该代码是否按照版本属性的建议使用 XSLT 2 处理器运行? generate-id 检查看起来像 XSLT 1。 我使用了 Saxon 处理器,但 XSLT 2 不是必需的。该属性只是另一个样式表的遗留物。【参考方案2】:

看来您可以根据自己的条件在父轴上行走,以确保您正在处理其父级的第一个子级:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  version="3.0"
  xmlns:xs="http://www.w3.org/2001/XMLSchema"
  exclude-result-prefixes="#all"
  expand-text="yes">

  <xsl:output method="html" indent="yes" html-version="5"/>

  <xsl:template match="/">
    <html>
      <head>
        <title>Test</title>
        <style xsl:expand-text="no">
        table, tr, td  border: 1px solid black; 
        </style>
      </head>
      <body>
        <h1>Test</h1>
        <table>
          <xsl:apply-templates select="descendant::*[not(*)]" mode="leaf"/>
        </table>
      </body>
    </html>
  </xsl:template>
  
  <xsl:template match="*" mode="leaf">
    <tr>
      <xsl:apply-templates select="parent::*[current() is current()/../*[1]]"/>
      <td>name()</td>
    </tr>
  </xsl:template>
  
  <xsl:template match="*">
    <xsl:apply-templates select="parent::*[current() is current()/../*[1]]"/>
    <td rowspan="count(descendant::*[not(*)])">name()</td>
  </xsl:template>

</xsl:stylesheet>

我使用了 XSLT 3,因为它可以生成更紧凑和可读的代码,但最终模板可以使用 generate-id() 比较而不是 isxsl:value-of 而不是文本为 XSLT 1 编写相同的值模板。

【讨论】:

是的,我们得到了相同的结果。我之前也有类似的解决方案。但是因为首先我没有通过在模板调用之后放置td元素来将元素的顺序从***别更改为最深级别,并且依赖于以相反顺序返回的被调用模板,我认为我需要将祖先存储在一个变量中以切换顺序。当我正确放置td 时,我仍然将它与祖先保持一致。

以上是关于XSLT 通用解决方案,用于从 XML 中获取分层 html 表的主要内容,如果未能解决你的问题,请参考以下文章

自动生成XSLT - 通用/默认XSLT

转换分层 DOM 树 XML 文件:XSLT 能否转换为“扁平化”XML 文件而不会丢失数据?

使用 XSLT 从 XML 中查询和提取数据

想要使用 XSLT 从 Xml 属性中获取价值

如何对适配器中的输入请求执行 xslt 转换

XSLT 3.0 - 无法在 XSLT 3.0 xml-to-json() 中获取对象数组