【问题标题】:Finding Directed Acyclic Graph (DAG) Minimal Elements (Vertices) with XSLT/XPath?使用 XSLT/XPath 查找有向无环图 (DAG) 最小元素(顶点)?
【发布时间】:2010-10-25 00:56:05
【问题描述】:

我有一个 XML 文件,它编码一个 directed acyclic graph (DAG) 表示 partial order。这样的图表对于指定依赖项和查找critical paths 等事情很有用。出于好奇,我当前的应用程序是为 build system 指定组件依赖项,因此顶点是组件,边指定编译时依赖项。这是一个简单的例子:

<?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 stylesheet 来生成另一个 XML 仅包含与偏序的minimal elements 对应的顶点的文档。也就是说,那些没有传入边的顶点。示例图的最小顶点集是{A, B, F}。对于我的构建依赖应用程序,找到这个集合很有价值,因为我知道如果我构建这个集合的成员,那么我的项目中的所有内容都将被构建。

这是我当前的样式表解决方案(我使用 Apache Ant 的 xslt 任务在 Java 上使用 Xalan 运行它)。一个关键的观察是在任何directed-edge-to 元素中都不会引用最小顶点:

<?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>

问题是,我对这个解决方案并不完全满意。 我想知道是否有办法将for-eachselectiftest 与XPath 语法结合起来。

我想写这样的东西:

<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 language 代码用于渲染图形。这里是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>

【问题讨论】:

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

标签: xslt xpath graph-theory directed-acyclic-graphs build-system


【解决方案1】:

您可以在 = 运算符上利用 XPath 的隐式存在量化:

<xsl:for-each select="//vertex[not(@name = //vertex/directed-edge-to/@vertex)]">

当您使用六个比较运算符(=!=&lt;&lt;=&gt;&gt;=)中的任何一个比较节点集时,表达式将返回 true如果节点集中的任何节点满足条件。在将一个节点集与另一个节点集进行比较时,如果第一个节点集中的任何节点在与第二个节点集中的任何节点进行比较时满足条件,则表达式返回 true。 XPath 2.0 引入了六个不执行这种存在量化的新运算符(eqneltlegtge)。但在您的情况下,您需要使用“=”来获得存在量化。

当然请注意,您仍然希望像以前一样使用not() 函数。大多数时候,最好避免使用!= 运算符。如果您在这里使用它而不是not(),那么如果有任何@vertex 属性不等于@name 值,它将返回true,这不是您的意图。 (如果任一节点集为空,则返回 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)]">

除了“some”语法之外,XPath 2.0 还提供了相应的“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"/>

您采用哪种方法部分取决于品味,部分取决于样式表的更广泛上下文和预期数据(输入结构可能有多少变化等)。

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

【讨论】:

  • 很好的答案!感谢所有的变化和清晰的解释。希望这个答案将来能帮助很多人! (这可以分为几个答案)
  • 很高兴您发现它有帮助。感谢您的投票。我还在学习如何使用这个网站。我应该提供单独的答复吗?
  • 提供单独的答案或提供具有多种变体的答案是个人喜好问题。独立答案允许独立投票。例如,也许我会接受使用 apply-templates 作为最佳响应的答案,但社区可能更喜欢使用 for-each 的答案。其他替代方案可能被否决。在按投票排序时,我接受的答案将首先显示,而社区答案将排在第二位。可以针对特定的解决方案发表评论。
  • 非常有意义。感谢您的提示!
【解决方案2】:

一个这样的 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 中提供了遍历完整(可能是循环)图的解决方案here

【讨论】:

  • 谢谢!这也是一个很好的答案,它非常专注于我提出的问题。这是一个艰难的决定,但我接受了埃文的回答,因为他回答的范围很广。我很好奇为什么你更喜欢 /*/ 语法而不是 //,额外的字符有什么好处吗?
  • @greg-mattes 应尽可能避免使用“//”缩写,因为它非常昂贵,会导致搜索以上下文节点为根的整个子树。顶层的“//”导致搜索整个 XML 文档。在编写 XPath 表达式时,当 XML 文档的结构已知时,不要使用“//”是非常重要的。
  • 所以 /*/ 通常更好,因为它将搜索限制在单个级别,因为 * 表示“选择上下文节点的所有元素子元素”(w3.org/TR/xpath#path-abbrev)而不是所有可能的后代大搜索?在这个特定的示例中,它不应该有所作为,但请牢记这一点。再次感谢。
  • 我同意 Dimitre 关于“//”的使用。你说得对,性能并不是这个特定数据的重要考虑因素。但是,使用 /*/vertex 或者更好的是 /dag/vertex 的另一个原因是它使您的意图更加明确。 “*”表示文档元素的名称可能不同,“//”表示 元素可能显示为更深的后代。通过使您的意图更加明确,您可以使阅读您的代码的人不必怀疑这些事情。当然,“//”在必要时仍然有用,即当它实际上是您的意图时。
猜你喜欢
  • 1970-01-01
  • 2015-11-15
  • 1970-01-01
  • 2020-04-01
  • 1970-01-01
  • 2022-01-24
  • 1970-01-01
  • 2020-05-19
  • 1970-01-01
相关资源
最近更新 更多