使用XSLT/XPath查找有向无环图(DAG)的最小元素(顶点)?

8
我有一个XML文件,编码了一个表示偏序有向无环图(DAG)。这样的图对于指定依赖关系和查找关键路径非常有用。对于好奇的人来说,我目前的应用是为一个构建系统指定组件依赖关系,因此顶点是组件,边指定编译时依赖关系。这里是一个简单的例子:
<?xml version="1.0"?>
<dag>
    <vertex name="A">
        <directed-edge-to vertex="C"/>
    </vertex>
    <vertex name="B">
        <directed-edge-to vertex="C"/>
        <directed-edge-to vertex="D"/>
    </vertex>
    <vertex name="C">
        <directed-edge-to vertex="E"/>
    </vertex>
    <vertex name="D">
        <directed-edge-to vertex="E"/>
    </vertex>
    <vertex name="E">
        <directed-edge-to vertex="G"/>
    </vertex>
    <vertex name="F">
        <directed-edge-to vertex="G"/>
    </vertex>
    <vertex name="G"/>
</dag>

这个DAG可以像这样绘制:


(来源:iparelan.com)

我想应用一个XSLT 样式表,生成另一个XML文档,仅包含对应于偏序的最小元素的顶点。也就是说,那些没有入边的顶点。对于示例图形的最小顶点集是{A,B,F}。对于我的构建依赖关系应用程序,找到这个集合很有价值,因为我知道如果我构建这个集合的成员,那么我的项目中的所有内容都将被构建。

这是我目前的样式表解决方案(我正在使用Apache Ant的任务在Java上运行Xalan)。一个关键的观察是,最小的顶点不会被任何元素引用:
<?xml version="1.0"?>
<xsl:stylesheet version="1.0"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:xalan="http://xml.apache.org/xslt"
                exclude-result-prefixes="xalan">
    <xsl:output method="xml" indent="yes" xalan:indent-amount="4"/>

    <xsl:template match="dag">
        <minimal-vertices>
            <xsl:for-each select="//vertex">
                <xsl:if test="not(//vertex/directed-edge-to[@vertex=current()/@name])">
                    <minimal-vertex name="{@name}"/>
                </xsl:if>
            </xsl:for-each>
        </minimal-vertices>
    </xsl:template>
</xsl:stylesheet>

应用这个样式表会产生以下输出(我认为是正确的):
<?xml version="1.0" encoding="UTF-8"?>
<minimal-vertices>
    <minimal-vertex name="A"/>
    <minimal-vertex name="B"/>
    <minimal-vertex name="F"/>
</minimal-vertices>

事实是,我对这个解决方案并不完全满意。我想知道是否有一种方法可以使用XPath语法将for-each的select和if的test结合起来。
我想写出类似下面的内容:
<xsl:for-each select="//vertex[not(//vertex/directed-edge-to[@vertex=current()/@name])]">

但这并不能满足我的需求,因为current()函数没有引用外部//vertex表达式所选的节点。
到目前为止,我的解决方案使用XPath 1.0XSLT 1.0语法,但我也可以接受XPath 2.0XSLT 2.0语法。
如果您愿意,这是Ant构建脚本:
<?xml version="1.0"?>
<project name="minimal-dag" default="default">
    <target name="default">
        <xslt in="dag.xml" out="minimal-vertices.xml" style="find-minimal-vertices.xsl"/>
    </target>
    <target name="dot">
        <xslt in="dag.xml" out="dag.dot" style="xml-to-dot.xsl"/>
    </target>
</project>

“dot”目标生成Graphviz Dot 语言代码,用于渲染图形。这里是xml-to-dot.xsl:
<?xml version="1.0"?>
<xsl:stylesheet version="1.0"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:xalan="http://xml.apache.org/xslt"
                exclude-result-prefixes="xalan">
    <xsl:output method="text"/>

    <xsl:template match="dag">
        digraph {
        rankdir="BT";
        node [style="filled", fillcolor="cyan", fontname="Helvetica"];
        <xsl:apply-templates select="//directed-edge-to"/>
        }
    </xsl:template>

    <xsl:template match="directed-edge-to">
        <xsl:value-of select="concat(ancestor::vertex/@name, '->', @vertex, ';')"/>
    </xsl:template>
</xsl:stylesheet>

1
“//”缩写应尽可能避免使用,因为它非常昂贵,会导致搜索以上下文节点为根的整个子树。“//”在顶层会导致搜索整个XML文档。当编写XPath表达式时,只要XML文档的结构已知,就非常重要使用“//”。 - Dimitre Novatchev
2个回答

8
您可以利用XPath中的隐式存在量词来使用=运算符:
<xsl:for-each select="//vertex[not(@name = //vertex/directed-edge-to/@vertex)]">

当您使用任何六个比较运算符(=!=<<=>>=)来比较节点集时,如果节点集中的任何节点满足条件,则表达式将返回true。当将一个节点集与另一个节点集进行比较时,如果第一个节点集中的任何节点与第二个节点集中的任何节点进行比较时都满足条件,则表达式返回true。XPath 2.0引入了六个新运算符,它们不执行这种存在量词(eqneltlegtge)。但在您的情况下,您需要使用“=”来获取这种存在量词。
当然,请注意,您仍然需要使用not()函数,就像您之前所做的那样。大多数情况下,最好避免使用!=运算符。如果您在此处使用它而不是not(),则它将返回true,如果有任何@vertex属性不等于@name值,这不是您的意图。(如果任一节点集为空,则会返回false,因为与空节点集的比较总是返回false。)
如果您想使用eq,那么您需要像之前做的那样将条件与迭代分离,以便绑定current()。但在XPath 2.0中,您可以在表达式中完成这个操作。
<xsl:for-each select="for $v in //vertex
                      return $v[not(//directed-edge-to[@vertex eq $v/@name])]">

当您的条件不是简单的相等比较时(因此无法使用“=”进行存在量化),这将非常有用。例如:starts-with(@vertex, $v/@name)

XPath 2.0还具有明确执行存在量化的方法。我们可以像下面这样编写,而不是上面的for表达式:

<xsl:for-each select="//vertex[not(some $e in //directed-edge-to
                                   satisfies @name eq $e/@vertex)]">

除了XPath 2.0中提供的“some”语法外,还提供了相应的“every”语法来执行普遍量化。
您可以使用模板规则而不是使用for-each,这更加模块化(和强大):
<xsl:stylesheet version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

  <xsl:template match="/">
    <minimal-vertices>
      <xsl:apply-templates/>
    </minimal-vertices>
  </xsl:template>

  <!-- Copy vertex elements that have no arrows pointing to them -->
  <xsl:template match="vertex[not(@name = //directed-edge-to/@vertex)]">
    <minimal-vertex name="{@name}"/>
  </xsl:template>

</xsl:stylesheet>

在这种情况下,我们依赖于 = 的存在量词。
XSLT 1.0禁止在模式中使用current()函数,即在match属性中使用,但是XSLT 2.0允许使用。在这种情况下,current()指的是当前正在匹配的节点。因此,在XSLT 2.0中,我们也可以这样写(而不必使用for表达式):
<xsl:template match="vertex[not(//directed-edge-to[@vertex eq current()/@name])]">

请注意,这个模式本质上与您尝试在for-each中使用的表达式相同,但是它在for-each中不起作用,而在模式中却能实现您想要的效果(因为current()绑定的内容不同)。
最后,我将添加另一种变化,以某种方式简化逻辑(删除not())。这也回到了使用XSLT 1.0的方式:
<xsl:stylesheet version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

  <xsl:template match="/">
    <minimal-vertices>
      <xsl:apply-templates/>
    </minimal-vertices>
  </xsl:template>

  <!-- By default, copy vertex elements -->
  <xsl:template match="vertex">
    <minimal-vertex name="{@name}"/>
  </xsl:template>

  <!-- But strip out vertices with incoming arrows -->
  <xsl:template match="vertex[@name = //directed-edge-to/@vertex]"/>

</xsl:stylesheet>

如果您不喜欢输出空格,请添加一个空规则来处理文本节点,这样它们就会被去除(覆盖默认的文本节点规则,即复制它们)。
<xsl:template match="text()"/>

或者你可以更加选择性地应用模板到节点中:

<xsl:apply-templates select="/dag/vertex"/>

你采取哪种方法部分取决于口味,部分取决于样式表和预期数据的更广泛背景(输入结构可能变化的程度等)。

我知道我超出了你的要求范围,但我希望你至少觉得这很有趣。:-)


非常棒的回答!感谢您提供的各种变体和清晰的解释。希望这个答案可以帮助许多人在未来解决问题!(这可能可以分成几个答案) - Greg Mattes
很高兴你觉得有帮助。感谢你的投票。我还在学习如何使用这个网站。我应该提供单独的回复吗? - Evan Lenz
提供单独的答案或一个带有多个变体的答案是一种品味问题。独立的答案允许独立投票。例如,也许我会接受使用apply-templates作为最佳响应的答案,但社区可能更喜欢使用for-each的答案。其他替代方案可能会被投反对票。当按投票排序时,我的被接受的答案将首先显示,社区答案将排在第二位。评论可以针对特定的解决方案进行。 - Greg Mattes
非常有道理。谢谢你的建议! - Evan Lenz

5

其中一个XPath 1.0表达式为::

        /*/vertex[not(@name = /*/vertex/directed-edge-to/@vertex)]

然后将其放入XSLT样式表中,就像这样:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

    <xsl:template match="/">
      <minimal-vertices>
          <xsl:for-each select=
          "/*/vertex[not(@name = /*/vertex/directed-edge-to/@vertex)]"
          >
           <minimal-vertex name="{@name}"/>
          </xsl:for-each>
      </minimal-vertices>
    </xsl:template>
</xsl:stylesheet>

当这个样式表应用于原始的XML文档时:

<dag>
    <vertex name="A">
        <directed-edge-to vertex="C"/>
    </vertex>
    <vertex name="B">
        <directed-edge-to vertex="C"/>
        <directed-edge-to vertex="D"/>
    </vertex>
    <vertex name="C">
        <directed-edge-to vertex="E"/>
    </vertex>
    <vertex name="D">
        <directed-edge-to vertex="E"/>
    </vertex>
    <vertex name="E">
        <directed-edge-to vertex="G"/>
    </vertex>
    <vertex name="F">
        <directed-edge-to vertex="G"/>
    </vertex>
    <vertex name="G"/>
</dag>

所期望的结果已经生成:

<minimal-vertices>
  <minimal-vertex name="A" />
  <minimal-vertex name="B" />
  <minimal-vertex name="F" />
</minimal-vertices>

请注意XSLT中提供了遍历完整(可能是循环的)图形的解决方案这里提供了更多信息。


谢谢!这也是一个很好的答案,非常专注于我提出的问题。这是一个艰难的决定,但我接受了Evan的答案,因为他的回答涵盖了更广泛的内容。我很好奇为什么你更喜欢使用 /*/ 语法而不是 //,是否有额外字符的优势? - Greg Mattes
1
@greg-mattes 应尽可能避免使用“//”缩写,因为它非常耗费资源,会导致搜索以上下文节点为根的整个子树。在顶层使用“//”会导致搜索整个XML文档。当编写XPath表达式时已知XML文档的结构时,非常重要不要使用“//”。 - Dimitre Novatchev
那么,//通常更好,因为它将搜索限制在单个级别上,因为表示“选择上下文节点的所有元素子代”(http://www.w3.org/TR/xpath#path-abbrev),而不是所有后代,这可能是一个大的搜索?在这个特定的例子中,这并不会有什么区别,但这是一个需要记住的好观点。再次感谢。 - Greg Mattes
1
我同意Dimitre关于使用“//”的建议。你说得对,对于这个特定的数据来说性能并不是太重要的考虑因素。然而,使用/* /vertex,甚至更好的/dag/vertex,还有另一个原因,那就是它使您的意图更明确。 “*”表示文档元素的名称可能会改变,“//”表示<vertex>元素可能会出现为更深层次的后代。通过使您的意图更加明确,您可以帮助阅读代码的人避免猜测这些事情。当真正需要时,“//”仍然很有用,即当它实际上是您的意图时。 - Evan Lenz

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