第一次编写xslt脚本。
2018年11月中旬起入职某医疗软件公司,担任接口开发程序员;基于自己以往技术实践所形成的偏好、也可能是某种路径依赖,总是倾向于使用存储过程实现接口需求。
公司软件产品基于MS SQLServer开发,而SQLServer要很晚的版本才能支持json,因此必须另寻途径来处理xml与json之间的相互转换。
花费不少时间、查找许多资料、作各种尝试,目前在用的方案是借助Newtonsoft.Json类库编写程序,部署成webservice供SQL代码远程调用、或clr函数供SQL代码本地调用。
另外,还借鉴stackoverflow网站问答帖子中提供的思路与代码样例,局部修改Newtonsoft.Json源代码,实现了目标json格式中要求非字符串节点值时的正确输出。
但是,单单为了转换xml与json数据格式转换就需要远程调用webservice未免显得小题大作,也有额外的耗时;而部署基于Newtonsoft.Json的clr函数,则需要额外多加载几个dll,这些dll还会因SQLServer所在服务器上.net框架版本差异而不同,从而有着潜在的兼容性隐患。
2019年末时开始了解到xslt(说来也惭愧,应用了二十几年的东西,这么晚才开始接触),感觉这是个解决xml转json的办法,只需要使用C#基于XslCompiledTransform类编写一个类似xml2any()这样的clr函数即可。
先在网上找了几个现成的xslt脚本,都不合用:
1、非重复元素无法转换为数组;
2、不支持非字符串类型的json节点值输出;
3、换行、TAB、反斜杠、双引号不转义;
大致了解一下xslt语法,凭着掌握得半生不熟的xslt自行编写了一个,能实现以下功能:
1、普通的节点结构/值的转换;
2、兼容Newtonsoft.Json中通过使用@json:Array="true"属性指定将xml元素输出为json数组;
3、也可用@json…Array属性指定将xml元素输出为json数组,以省却@json:Array属性所需的名称空间;
4、兼容此前局部修改Newtonsoft.Json源代码实现的通过@json:Type=“Integer"等指定将xml元素值输出为非字符串值;
5、也可用@json…Type属性指定将xml元素值输出为非字符串值,以省却@json:Type属性所需的名称空间;
6、换行、TAB、反斜杠、双引号几个符号能够转义输出;
7、当一个xml元素既有属性节点、同时其第一个text()节点值含有非空白字符内容时,将该第一个text()节点输出为xml元素转换的目标json对象下的"text”:“text()节点值” 字段;
逐层换行/缩进的功能暂未实现。。。
脚本编写完成后经测试,需要的功能都实现了,但效率不理想;以笔记本测试为例,使用同一份源xml数据进行转换,基于Newtonsoft.Json类库的clr函数耗时在1-1.5毫秒,使用基于xslt的clr函数则耗时15-18毫秒。这是可以理解的,毕竟除了解析源xml数据以外,另有加载xslt解析器的额外耗时、解析xslt的额外耗时,并且可以想见,xslt脚本的执行效率也是相对不高的。
程序员应当对效率敏感,甚至最好保持某种‘洁癖’;鉴于超过十倍的效率差异,这份xslt可能不会被投入实用。
绕了一大圈,其实最优办法是自已编写一个递归函数,既不用依赖额外的类库,执行效率也是最高的。可能是写多了面向业务的程序代码,退化得根本没有写基本算法的意识,结果反而事倍功半。
不过,绕的那一大圈,当然也不可能仅仅是无用功;而xslt作为一种所谓‘函数式’语言,具备某些面向数据(而不是面向过程)的特点,编写xslt脚本其实还算是个有些趣味的事情,代码如下:
<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" encoding="utf-8"/>
<xsl:template match="/*[1]">
<xsl:text>{</xsl:text>
<xsl:apply-templates select="*" mode="detect-element" />
<xsl:text>}</xsl:text>
</xsl:template>
<xsl:template match="*" mode="detect-element">
<xsl:choose>
<xsl:when test="name(preceding-sibling::*[1]) = name() or name(following-sibling::*[1]) = name() or @json..Array or @*[name()='json:Array'] ">
<xsl:if test="name(preceding-sibling::*[1]) != name()" >
<xsl:text>"</xsl:text><xsl:value-of select="name()"/><xsl:text>":[</xsl:text>
</xsl:if>
<xsl:apply-templates select="." mode="output-element" />
<xsl:if test="name(following-sibling::*[1]) != name()" ><xsl:text>]</xsl:text></xsl:if>
</xsl:when>
<xsl:otherwise>
<xsl:text>"</xsl:text><xsl:value-of select="name()"/><xsl:text>":</xsl:text>
<xsl:apply-templates select="." mode="output-element" />
</xsl:otherwise>
</xsl:choose>
<xsl:if test="following-sibling::*" ><xsl:text>,</xsl:text></xsl:if>
</xsl:template>
<xsl:template match="*" mode="output-element">
<xsl:choose>
<xsl:when test="* or @*[not(starts-with(name(), 'json..')) and not(starts-with(name(), 'json:'))]">
<xsl:text>{</xsl:text>
<xsl:if test="@*[not(starts-with(name(), 'json..')) and not(starts-with(name(), 'json:'))]">
<xsl:apply-templates select="@*[not(starts-with(name(), 'json..')) and not(starts-with(name(), 'json:'))]" mode="output-attribute" />
<xsl:if test="text() and normalize-space(text()) != '' or *">
<xsl:text>,</xsl:text>
</xsl:if>
</xsl:if>
<xsl:if test="text() and normalize-space(text()) != ''">
<xsl:text>"text":"</xsl:text>
<xsl:call-template name="output-string"><xsl:with-param name="string" select="text()"/></xsl:call-template>
<xsl:text>"</xsl:text>
<xsl:if test="*">
<xsl:text>,</xsl:text>
</xsl:if>
</xsl:if>
<xsl:apply-templates select="*" mode="detect-element" />
<xsl:text>}</xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:choose>
<xsl:when test="@json..Type or @*[name()='json:Type']">
<xsl:value-of select="normalize-space(text())"/>
<xsl:if test="normalize-space(text()) = ''"><xsl:text>null</xsl:text></xsl:if>
</xsl:when>
<xsl:otherwise>
<xsl:text>"</xsl:text>
<xsl:call-template name="output-string"><xsl:with-param name="string" select="text()"/></xsl:call-template>
<xsl:text>"</xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="@*" mode="output-attribute">
<xsl:text>"</xsl:text><xsl:value-of select="name()"/>
<xsl:text>":"</xsl:text>
<xsl:call-template name="output-string"><xsl:with-param name="string" select="."/></xsl:call-template>
<xsl:text>"</xsl:text>
<xsl:if test="position() < last()">,</xsl:if>
</xsl:template>
<xsl:template name="output-string">
<xsl:param name="string" />
<xsl:if test="$string != ''">
<xsl:choose>
<xsl:when test="contains($string, '	') or contains($string, '
') or contains($string, '
') or contains($string, '"') or contains($string, '\')">
<xsl:choose>
<xsl:when test="starts-with($string, '	')">
<xsl:text>\t</xsl:text>
</xsl:when>
<xsl:when test="starts-with($string, '
')">
<xsl:text>\n</xsl:text>
</xsl:when>
<xsl:when test="starts-with($string, '
')">
<xsl:text>\r</xsl:text>
</xsl:when>
<xsl:when test="starts-with($string, '"')">
<xsl:text>\"</xsl:text>
</xsl:when>
<xsl:when test="starts-with($string, '\')">
<xsl:text>\\</xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="substring($string, 1, 1)"/>
</xsl:otherwise>
</xsl:choose>
<xsl:call-template name="output-string">
<xsl:with-param name="string" select="substring($string, 2)"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$string"/>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
探讨使用XSLT转换XML到JSON的实践,包括解决非重复元素数组转换、非字符串类型值输出及特殊字符转义等问题,对比Newtonsoft.Json的效率。
1237

被折叠的 条评论
为什么被折叠?



