如何用XSLT合并XML中job属性与login值相同的ban元素并取最新reason
没问题,我来帮你搞定这个XSLT合并需求!核心思路是按job属性和login元素的组合值分组,然后在每组里提取最新的reason值。下面分情况给出实现方案:
先看示例输入XML
假设你的原始XML结构是这样的(对应你描述的5个ban元素场景):
<root> <ban job="gh:test/test#1"> <login>user1</login> <reason>First ban reason for user1</reason> <date>2023-01-01</date> </ban> <ban job="gh:test/test#1"> <login>user1</login> <reason>Updated ban reason for user1</reason> <date>2023-02-01</date> </ban> <ban job="gh:test/test#2"> <login>user2</login> <reason>Initial ban for user2</reason> <date>2023-01-15</date> </ban> <ban job="gh:test/test#3"> <login>user3</login> <reason>One-time ban for user3</reason> <date>2023-03-01</date> </ban> <ban job="gh:test/test#2"> <login>user2</login> <reason>Latest ban reason for user2</reason> <date>2023-03-15</date> </ban> </root>
方案1:XSLT 2.0/3.0(推荐,语法更简洁)
利用xsl:for-each-group直接分组,同时支持按日期取最新(如果有日期字段)或按节点顺序取最后一个:
<?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="xml" indent="yes"/> <!-- 定义分组键:job属性 + login值,用|分隔避免冲突 --> <xsl:key name="ban-group" match="ban" use="concat(@job, '|', login)"/> <xsl:template match="/root"> <root> <!-- 按组合键分组遍历 --> <xsl:for-each-group select="ban" group-by="concat(@job, '|', login)"> <ban job="{@job}"> <!-- 每组的login值都相同,直接取第一个即可 --> <login><xsl:value-of select="login"/></login> <!-- 方式1:按日期取最新的reason(推荐,更精准) --> <xsl:variable name="latest-by-date" select="current-group()[date = max(current-group()/date)]"/> <reason><xsl:value-of select="$latest-by-date/reason"/></reason> <date><xsl:value-of select="$latest-by-date/date"/></date> <!-- 方式2:如果没有日期,按节点顺序取最后一个的reason --> <!-- <reason><xsl:value-of select="current-group()[last()]/reason"/></reason> --> </ban> </xsl:for-each-group> </root> </xsl:template> </xsl:stylesheet>
方案2:XSLT 1.0(兼容旧环境)
如果只能用XSLT 1.0,就用Muenchian分组实现,代码稍复杂但逻辑一致:
<?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" indent="yes"/> <xsl:key name="ban-group" match="ban" use="concat(@job, '|', login)"/> <xsl:template match="/root"> <root> <!-- 选择每组的第一个节点作为分组入口 --> <xsl:for-each select="ban[generate-id() = generate-id(key('ban-group', concat(@job, '|', login))[1])]"> <xsl:variable name="current-group" select="key('ban-group', concat(@job, '|', login))"/> <ban job="{@job}"> <login><xsl:value-of select="login"/></login> <!-- 按节点顺序取最后一个的reason --> <reason><xsl:value-of select="$current-group[last()]/reason"/></reason> <!-- 如果要按日期取最新,需要先排序再取第一个 --> <!-- <xsl:for-each select="$current-group"> <xsl:sort select="date" order="descending"/> <xsl:if test="position() = 1"> <reason><xsl:value-of select="reason"/></reason> <date><xsl:value-of select="date"/></date> </xsl:if> </xsl:for-each> --> </ban> </xsl:for-each> </root> </xsl:template> </xsl:stylesheet>
预期输出
无论用哪种方案,最终都会得到合并后的XML,比如:
<root> <ban job="gh:test/test#1"> <login>user1</login> <reason>Updated ban reason for user1</reason> <date>2023-02-01</date> </ban> <ban job="gh:test/test#2"> <login>user2</login> <reason>Latest ban reason for user2</reason> <date>2023-03-15</date> </ban> <ban job="gh:test/test#3"> <login>user3</login> <reason>One-time ban for user3</reason> <date>2023-03-01</date> </ban> </root>
关键说明
- 分组键用
concat(@job, '|', login)是为了避免不同job和login的组合产生冲突(比如job="a|b"+login="c"和job="a"+login="b|c"的情况) - 如果你没有
date字段,直接用节点顺序取最后一个元素的reason即可,也就是代码里注释掉的方式2
内容的提问来源于stack exchange,提问作者Kirill
相关产品推荐
相关产品推荐

