如何对XML元素与属性序列进行规范化以适配版本控制场景?
问题背景
我正在对一批第三方应用生成的XML文件做版本控制,但这类文件的存储方式经常给版本控制带来不必要的麻烦,常见的无意义差异包括以下几种:
- 元素顺序调换:
<root> - <b>bar</b> <a>foo</a> + <b>bar</b> </root>
- 属性顺序重排:
-<root a="foo" b="bar"/> +<root b="bar" a="foo"/>
- 缩进格式改动:
-<root a="foo" b="bar"/> +<root + a="foo" + b="bar"/>
需要说明的是,这批文件不存在文本与元素节点混合的情况(比如<a>foo <b>bar</b></a>),不同排序、格式的文件没有语义差异,因此可以安全地随意重排序和调整格式。
我已经通过xsltproc搭配如下样式表部分解决了元素排序的问题:
<stylesheet version="1.0" xmlns="http://www.w3.org/1999/XSL/Transform"> <output method="xml" indent="yes" encoding="UTF-8"/> <strip-space elements="*"/> <template match="processing-instruction()|@*"> <copy> <apply-templates select="node()|@*"/> </copy> </template> <template match="*"> <copy> <apply-templates select="@*"/> <apply-templates> <sort select="name()"/> <sort select="@*[1]"/> <sort select="@*[2]"/> <sort select="@*[3]"/> <sort select="@*[4]"/> <sort select="@*[5]"/> <sort select="@*[6]"/> </apply-templates> </copy> </template> </stylesheet>
但我最近了解到XSLT中属性的处理顺序没有规范定义,因此按“前6个属性”排序的方案通用度不足,而且该方案也不能对属性本身进行排序。
(我在标题中使用“规范化”一词,是因为我不一定非要按特定规则排序元素,只是希望让两个语义完全相同的文件不存在文本差异。)
请问有没有办法实现这样的排序需求?
虽然名称类似,但本问题和“XSLT sort by tag name and attribute value”的问题不同,那个问题仅涉及单个属性,采纳的解决方案通用度不足。
解决方案
你有两种成熟的实现路径,都可以满足你的XML规范化需求:
方案一:直接使用现成命令行工具(最简便)
你可以直接用xmllint的规范化功能,它原生处理属性排序、空格、缩进等无意义差异,命令非常简单:
xmllint --format --c14n your.xml > normalized.xml
其中--format负责统一缩进格式,--c14n是W3C标准的XML规范化实现,会自动按名称排序所有属性,处理空值等差异,完全符合你要求的"语义相同则文本完全一致"的需求。
方案二:自定义XSLT实现
如果你需要自定义排序规则,可以使用以下通用XSLT样式表,支持任意数量属性的排序,兼容XSLT 1.0版本:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="xml" indent="yes" encoding="UTF-8"/> <xsl:strip-space elements="*"/> <!-- 复制处理指令、注释等非元素节点 --> <xsl:template match="processing-instruction()|comment()"> <xsl:copy/> </xsl:template> <!-- 元素节点处理逻辑 --> <xsl:template match="*"> <xsl:copy> <!-- 按属性名称排序所有属性 --> <xsl:apply-templates select="@*"> <xsl:sort select="name()"/> </xsl:apply-templates> <!-- 子元素先按标签名排序,再按属性值排序 --> <xsl:apply-templates select="*"> <xsl:sort select="name()"/> <xsl:sort select="string(@*)"/> </xsl:apply-templates> </xsl:copy> </xsl:template> <!-- 直接复制属性节点 --> <xsl:template match="@*"> <xsl:copy/> </xsl:template> </xsl:stylesheet>
用xsltproc调用的命令示例:
xsltproc normalize.xslt your.xml > normalized.xml
你可以根据自己的需求调整排序规则,比如需要按子元素的文本内容排序,只需要在子元素排序的位置新增一行<xsl:sort select="text()"/>即可。
内容的提问来源于stack exchange,提问作者l0b0

