XSLT 1.0实现CSV转XML 按行首0值分组单据明细
XSLT 1.0 按起始标记分组CSV转XML实现方案
需求说明
需要使用XSLT 1.0引擎将存储客户单据数据的CSV文件转换为XML文档,规则如下:
- 每笔新单据以首字段值为
0的行作为起始标记,该行存储单据日期、客户名称的头信息 - 单据头之后首字段值大于0的行属于当前单据的明细,直到遇到下一个首字段为0的新单据头行为止
- 每个单据包含的明细行数量不固定
输入CSV示例
<root> 0;15-01-2022;Customer1 1;Dual monitors;50 2;Laser mouse;10.50 0;21-1-2022;Customer5 1;Multi-jet printer;100 0;30-1-2022;Customer8 1;Goods returned;-200 2;Basic keyboard;300 </root>
目标XML结构
<Documents> <Document> <Header> <Customer>Customer1</Customer> <Date>15-01-2022</Date> </Header> <Lines> <Line> <LineNumber>1</LineNumber> <Price>50</Price> <Description>Dual monitors</Description> </Line> <Line> <LineNumber>2</LineNumber> <Price>10.50</Price> <Description>Laser mouse</Description> </Line> </Lines> </Document> <Document> <Header> <Customer>Customer5</Customer> <Date>21-1-2022</Date> </Header> <Lines> <Line> <LineNumber>1</LineNumber> <Price>100</Price> <Description>Multi-jet printer</Description> </Line> </Lines> </Document> <Document> <Header> <Customer>Customer8</Customer> <Date>30-1-2022</Date> </Header> <Lines> <Line> <LineNumber>1</LineNumber> <Price>-200</Price> <Description>Goods returned</Description> </Line> <Line> <LineNumber>2</LineNumber> <Price>300</Price> <Description>Basic keyboard</Description> </Line> </Lines> </Document> </Documents>
原有代码问题
原有实现的明细遍历逻辑直接选取了所有首字段大于0的行,没有做单据边界判断,导致所有明细行被重复归入每一笔单据下。
修正后实现代码
核心逻辑是通过兄弟节点位置判断,筛选当前单据头和下一个单据头之间的行作为当前单据的明细:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:exsl="http://exslt.org/common" extension-element-prefixes="exsl"> <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/> <xsl:template match="/"> <!-- 按换行拆分CSV行,过滤空行 --> <xsl:variable name="rows"> <xsl:call-template name="tokenize"> <xsl:with-param name="text" select="root"/> </xsl:call-template> </xsl:variable> <!-- 按分号拆分每行的单元格 --> <xsl:variable name="data"> <xsl:for-each select="exsl:node-set($rows)/row[normalize-space(.)]"> <row> <xsl:call-template name="tokenize"> <xsl:with-param name="text" select="normalize-space(.)"/> <xsl:with-param name="delimiter" select="';'"/> <xsl:with-param name="name" select="'cell'"/> </xsl:call-template> </row> </xsl:for-each> </xsl:variable> <xsl:variable name="allRows" select="exsl:node-set($data)/row"/> <!-- 输出最终XML --> <Documents> <xsl:for-each select="$allRows[cell[1] = 0]"> <xsl:variable name="currentHeaderPos" select="count(preceding-sibling::row) + 1"/> <!-- 查找下一个单据头的位置,最后一个单据没有下一个头 --> <xsl:variable name="nextHeaderPos" select="count(following-sibling::row[cell[1]=0][1]/preceding-sibling::row) + 1"/> <Document> <Header> <Customer> <xsl:value-of select="cell[3]"/> </Customer> <Date> <xsl:value-of select="cell[2]"/> </Date> </Header> <Lines> <!-- 只选取当前头之后、下一个头之前的明细行 --> <xsl:for-each select="../row[(count(preceding-sibling::row) + 1) > $currentHeaderPos][((count(preceding-sibling::row) + 1) < $nextHeaderPos) or not($nextHeaderPos)][cell[1] > 0]"> <Line> <LineNumber> <xsl:value-of select="cell[1]"/> </LineNumber> <Price> <xsl:value-of select="cell[3]"/> </Price> <Description> <xsl:value-of select="cell[2]"/> </Description> </Line> </xsl:for-each> </Lines> </Document> </xsl:for-each> </Documents> </xsl:template> <!-- 通用字符串拆分模板 --> <xsl:template name="tokenize"> <xsl:param name="text"/> <xsl:param name="delimiter" select="' '"/> <xsl:param name="name" select="'row'"/> <xsl:variable name="token" select="substring-before(concat($text, $delimiter), $delimiter)"/> <xsl:if test="$token"> <xsl:element name="{$name}"> <xsl:value-of select="$token"/> </xsl:element> </xsl:if> <xsl:if test="contains($text, $delimiter)"> <xsl:call-template name="tokenize"> <xsl:with-param name="text" select="substring-after($text, $delimiter)"/> <xsl:with-param name="delimiter" select="$delimiter"/> <xsl:with-param name="name" select="$name"/> </xsl:call-template> </xsl:if> </xsl:template> </xsl:stylesheet>
关键改动说明
- 移除了原代码中无效的
k1键定义,该键按首字段值匹配无法实现相邻分组逻辑 - 拆分CSV行时增加空行过滤,避免换行符、首尾空格导致的行位置计算错误
- 遍历单据头时通过计算前置兄弟节点数量获取当前行的全局位置,同时查找下一个同类型单据头的全局位置,通过位置区间筛选归属当前单据的明细行,解决明细跨单据重复归属的问题
- 对最后一个单据做边界兼容,不存在下一个单据头时直接取当前头之后的所有明细行
内容的提问来源于stack exchange,提问作者b100
相关产品推荐
相关产品推荐

