如何用XSLT 3.0流式处理高度嵌套XML文档?(Saxon-EE 9.8.0.12)
我完全懂你的痛点——那些简单的XSLT流式示例都是给扁平重复结构准备的,碰到你这种高度嵌套、还要全节点遍历做JSON<->XML转换的场景,copy-of根本不管用,本质还是会把整个文档塞进内存。好在你用的是Saxon-EE 9.8,它的流式能力足够应付这种复杂场景,给你几个实操性强的方案:
1. 用多阶段流式拆解嵌套结构
把复杂的转换拆成两步,先把嵌套XML拆成扁平的中间流,再处理这个中间流转换格式:
- 第一阶段:流式遍历整个XML,提取每个节点的路径+值(比如
/ElectionReport/Election/Candidate/BallotName/Text对应文本值),输出成类似键值对的扁平结构,这一步完全不用加载整个文档 - 第二阶段:再流式处理这个扁平结构,把路径映射成JSON的层级结构,或者反过来从JSON的键值对组装成嵌套XML
给你个第一阶段提取路径的XSLT示例:
<xsl:mode streamable="yes" on-no-match="shallow-skip"/> <xsl:template match="*" streamable="yes"> <xsl:param name="path" select="''"/> <xsl:variable name="current-path" select="concat($path, '/', local-name())"/> <!-- 只处理有实际内容的文本节点 --> <xsl:if test="text()[normalize-space()]"> <entry> <path><xsl:value-of select="$current-path"/></path> <value><xsl:value-of select="normalize-space(text())"/></value> </entry> </xsl:if> <!-- 递归处理子节点,传递当前路径 --> <xsl:apply-templates select="child::*"> <xsl:with-param name="path" select="$current-path"/> </xsl:apply-templates> </xsl:template>
2. 自适应流式逐个处理嵌套节点
Saxon-EE的自适应流式允许你在模板里逐层处理节点,只要保证按文档顺序访问,不回头看已处理的内容就行。你可以给每个嵌套节点写对应的流式模板,直接构建JSON结构,不用加载整个子树。
比如处理<n1:Candidate>的示例:
<xsl:mode streamable="yes"/> <xsl:template match="n1:Candidate" streamable="yes"> <xsl:map> <xsl:map-entry key="'ballotName'" select="string(n1:BallotName/n1:Text)"/> <xsl:map-entry key="'contactInfo'" select="my:process-contact(n1:ContactInformation)"/> <!-- 其他属性和子节点照此处理 --> </xsl:map> </xsl:template> <xsl:function name="my:process-contact" streamable="yes"> <xsl:param name="contact" as="element(n1:ContactInformation)"/> <xsl:map> <xsl:map-entry key="'email'" select="string($contact/n1:Email)"/> <xsl:map-entry key="'phone'" select="string($contact/n1:Phone)"/> <xsl:map-entry key="'schedule'" select="my:process-schedule($contact/n1:Schedule)"/> </xsl:map> </xsl:function>
这里的关键是用string()这种流式安全的函数提取值,绝不使用copy-of整个子树(除非子树特别小)。Saxon会自动帮你管理内存,只保留当前正在处理的节点层级。
3. 拆分JSON<->XML转换的流式优化
如果用Saxon内置的xml-to-json/json-to-xml,默认会加载整个文档,你可以拆成多个小单元处理:
- XML转JSON:流式遍历XML,把每个独立的逻辑单元(比如每个
<n1:Candidate>、<n1:BallotCounts>)单独转成JSON对象,再把这些对象拼接成JSON数组/对象 - JSON转XML:把JSON拆成多个条目,流式组装成对应的XML嵌套结构,避免一次性加载整个JSON
给你个XML转JSON的流式示例:
<xsl:mode streamable="yes"/> <xsl:template match="/n1:ElectionReport" streamable="yes"> <xsl:text>{ "election": {</xsl:text> <xsl:text>"ballotCounts": [</xsl:text> <xsl:apply-templates select="n1:Election/n1:BallotCounts" mode="to-json"/> <xsl:text>],</xsl:text> <xsl:text>"candidates": [</xsl:text> <xsl:apply-templates select="n1:Election/n1:Candidate" mode="to-json"/> <xsl:text>]}}</xsl:text> </xsl:template> <xsl:template match="n1:BallotCounts" mode="to-json" streamable="yes"> <xsl:text>{</xsl:text> <xsl:text>"deviceClass": { "manufacturer": "</xsl:text><xsl:value-of select="n1:DeviceClass/n1:Manufacturer"/>"},</xsl:text> <xsl:text>"ballotsCast": "</xsl:text><xsl:value-of select="n1:BallotsCast"/>"</xsl:text> <xsl:text>}</xsl:text> <xsl:if test="position() != last()">,</xsl:if> </xsl:template>
这种方式逐个输出JSON片段,全程流式,内存占用极低。
最后注意Saxon-EE 9.8的流式限制
9.8的流式有几个坑要避开:
- 不能用逆向轴(比如
parent::、ancestor::),除非是在copy-of里,但尽量别用 - 不能对节点做排序、分组(除非是流式分组,但9.8支持有限)
- 所有
apply-templates和for-each必须按文档顺序处理
如果你的逻辑需要分组或排序,可以先在第一阶段把数据拆成扁平结构,再在第二阶段处理——此时数据量已经被压缩,内存压力小很多。
总的来说,核心就是把深度嵌套的处理拆成逐个节点/逐个逻辑单元的流式处理,别想着一次性搞定整个文档,利用Saxon-EE的流式能力就能实现低内存的JSON<->XML转换。
内容的提问来源于stack exchange,提问作者J. Nicholas

