You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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="'&#10;'"/>
        <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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 22:45:46