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>
【问题讨论】:
您确定将单个物种作为元素名称是个好主意吗?通常我希望看到更像<species name="cockroach"/>
而不是<cockroach>
的东西——或者可能是门/王国/属或任何代替物种的东西。
是的,你是对的,在我用于特殊目的的原始实现中,我只有几个不同的元素名称,区别在于 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])">
然后用祖先从后面填充行以获得bear
、mammals
、warmBlooded
、vertebrates
和classificationOfAnimals
,我们将使用递归调用的模板
<!-- 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
,尽管warmBlooded
是vertebrates
的第一个孩子,所以我们需要在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()
比较而不是 is
和 xsl:value-of
而不是文本为 XSLT 1 编写相同的值模板。
【讨论】:
是的,我们得到了相同的结果。我之前也有类似的解决方案。但是因为首先我没有通过在模板调用之后放置td
元素来将元素的顺序从***别更改为最深级别,并且依赖于以相反顺序返回的被调用模板,我认为我需要将祖先存储在一个变量中以切换顺序。当我正确放置td
时,我仍然将它与祖先保持一致。以上是关于XSLT 通用解决方案,用于从 XML 中获取分层 html 表的主要内容,如果未能解决你的问题,请参考以下文章