递归地对任意XML文档的元素进行排序

时间:2013-09-09 21:26:44

标签: xml xslt groovy

我正在尝试对一些XML文档进行排序和规范化。期望的最终结果是:

  1. 每个元素的子元素按字母顺序排列
  2. 每个元素属性都按字母顺序排列
  3. 评论已删除
  4. 所有元素都间隔适当(即“漂亮印刷”)。
  5. 除了#1,我已经实现了所有这些目标。

    我一直使用this回答作为我的模板。以下是我到目前为止的情况:

    import javax.xml.transform.stream.StreamResult
    import javax.xml.transform.stream.StreamSource
    import javax.xml.transform.TransformerFactory
    import org.apache.xml.security.c14n.Canonicalizer
    
    // Initialize the security library
    org.apache.xml.security.Init.init()
    
    // Create some variables
    
    // Get arguments
    
    // Make sure required arguments have been provided
    
    if(!error) {
        // Create some variables
        def ext = fileInName.tokenize('.').last()
        fileOutName = fileOutName ?: "${fileInName.lastIndexOf('.').with {it != -1 ? fileInName[0..<it] : fileInName}}_CANONICALIZED_AND_SORTED.${ext}"
        def fileIn = new File(fileInName)
        def fileOut = new File(fileOutName)
        def xsltFile = new File(xsltName)
        def temp1 = new File("./temp1")
        def temp2 = new File("./temp2")
        def os
        def is
    
        // Sort the XML attributes, remove comments, and remove extra whitespace
        println "Canonicalizing..."
        Canonicalizer c = Canonicalizer.getInstance(Canonicalizer.ALGO_ID_C14N_OMIT_COMMENTS)
        os = temp1.newOutputStream()
        c.setWriter(os)
        c.canonicalize(fileIn.getBytes())
        os.close()
    
        // Sort the XML elements
        println "Sorting..."
        def factory = TransformerFactory.newInstance()
        is = xsltFile.newInputStream()
        def transformer = factory.newTransformer(new StreamSource(is))
        is.close()
        is = temp1.newInputStream()
        os = temp2.newOutputStream()
        transformer.transform(new StreamSource(is), new StreamResult(os))
        is.close()
        os.close()
    
        // Write the XML output in "pretty print"
        println "Beautifying..."
        def parser = new XmlParser()
        def printer = new XmlNodePrinter(new IndentPrinter(fileOut.newPrintWriter(), "    ", true))
        printer.print parser.parseText(temp2.getText())
    
        // Cleanup
        temp1.delete()
        temp2.delete()
    
        println "Done!"
    }
    

    完整脚本为here

    XSLT:

    <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
      <xsl:output indent="yes"/>
      <xsl:strip-space elements="*"/>
      <xsl:template match="node()|@*">
        <xsl:copy>
          <xsl:apply-templates select="node()|@*"/>
        </xsl:copy>
      </xsl:template>
      <xsl:template match="foo">
        <foo>
          <xsl:apply-templates>
            <xsl:sort select="name()"/>
          </xsl:apply-templates>
        </foo>
      </xsl:template>
    </xsl:stylesheet>
    

    示例输入XML:

    <foo b="b" a="a" c="c">
        <qwer>
        <zxcv c="c" b="b"/>
        <vcxz c="c" b="b"/>
        </qwer>
        <baz e="e" d="d"/>
        <bar>
        <fdsa g="g" f="f"/>
        <asdf g="g" f="f"/>
        </bar>
    </foo>
    

    所需的输出XML:

    <foo a="a" b="b" c="c">
        <bar>
            <asdf f="f" g="g"/>
            <fdsa f="f" g="g"/>
        </bar>
        <baz d="d" e="e"/>
        <qwer>
            <vcxz b="b" c="c"/>
            <zxcv b="b" c="c"/>
        </qwer>
    </foo>
    

    如何将变换应用于所有元素,以便所有元素的子元素按字母顺序排列?

2 个答案:

答案 0 :(得分:8)

如果要将变换应​​用于所有元素,则需要一个模板来匹配所有元素,而不是使模板与特定的“foo”元素匹配

<xsl:template match="*">

请注意,您必须更改与“node()”匹配的当前模板以排除元素:

 <xsl:template match="node()[not(self::*)]|@*">

在此模板中,您还需要使用代码来选择属性,因为此时“foo”模板会忽略它们(<xsl:apply-templates />不会选择属性)。

实际上,根据您的要求,第1至第3项都可以使用单个XSLT完成。例如,要删除注释,您可以从当前与node()

匹配的模板中忽略它
<xsl:template match="node()[not(self::comment())][not(self::*)]|@*">

尝试以下XSLT,将实现点1到3

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="xml" indent="yes"/>
  <xsl:strip-space elements="*"/>

  <xsl:template match="node()[not(self::comment())][not(self::*)]|@*">
    <xsl:copy>
      <xsl:apply-templates select="node()|@*"/>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="*">
    <xsl:copy>
      <xsl:apply-templates select="@*">
        <xsl:sort select="name()"/>
      </xsl:apply-templates>
      <xsl:apply-templates>
        <xsl:sort select="name()"/>
      </xsl:apply-templates>
    </xsl:copy>
  </xsl:template>
</xsl:stylesheet>

编辑:模板<xsl:template match="node()[not(self::comment())][not(self::*)]|@*">实际上可以仅用<xsl:template match="processing-instruction()|@*">替换,这可能会提高可读性。这是因为“node()”匹配元素,文本节点,注释和处理指令。在您的XSLT中,元素由其他模板拾取,文本节点由内置模板拾取,您想要忽略的注释,只留下处理指令。

答案 1 :(得分:3)

为了好玩,您也可以以编程方式执行此操作:

def x = '''<foo b="b" a="a" c="c">
    <qwer>
    <!-- A comment -->
    <zxcv c="c" b="b"/>
    <vcxz c="c" b="b"/>
    </qwer>
    <baz e="e" d="d"/>
    <bar>
    <fdsa g="g" f="f"/>
    <asdf g="g" f="f"/>
    </bar>
</foo>'''

def order( node ) {
    [ *:node.attributes() ].sort().with { attr ->
        node.attributes().clear()
        attr.each { node.attributes() << it }
    }
    node.children().sort { it.name() }
                   .each { order( it ) }
    node
}

def doc = new XmlParser().parseText( x )

println groovy.xml.XmlUtil.serialize( order( doc ) )

如果您的节点有内容,则需要更改为:

def x = '''<foo b="b" a="a" c="c">
    <qwer>
    <!-- A comment -->
    <zxcv c="c" b="b">Some Text</zxcv>
    <vcxz c="c" b="b"/>
    </qwer>
    <baz e="e" d="d">Woo</baz>
    <bar>
    <fdsa g="g" f="f"/>
    <asdf g="g" f="f"/>
    </bar>
</foo>'''

def order( node ) {
    [ *:node.attributes() ].sort().with { attr ->
        node.attributes().clear()
        attr.each { node.attributes() << it }
    }
    node.children().sort()
                   .grep( Node )
                   .each { order( it ) }
    node
}

def doc = new XmlParser().parseText( x )

println groovy.xml.XmlUtil.serialize( order( doc ) )

然后给出:

<?xml version="1.0" encoding="UTF-8"?><foo a="a" b="b" c="c">
  <baz d="d" e="e">Woo</baz>
  <bar>
    <fdsa f="f" g="g"/>
    <asdf f="f" g="g"/>
  </bar>
  <qwer>
    <zxcv b="b" c="c">Some Text</zxcv>
    <vcxz b="b" c="c"/>
  </qwer>
</foo>