首页 > 解决方案 > 将大型 XML 文件(超过 3G)转换为逗号分隔文件

问题描述

我需要将一个大的 XML 文件(超过 3G)转换成一个逗号分隔的文件。我创建了一个 XSL 文件来转换它。不幸的是,该文件太大而无法使用 XSLT 1.0 进行处理。我尝试使用 XSLT 3.0 (Saxon),但收到错误“XTSE3430:模板规则不可流式传输”。

脚本:

java -cp saxon9ee.jar net.sf.saxon.Transform -t -s:costing.xml -xsl:costing.xsl -o:costing.csv

错误信息:

Java version 1.8.0_191        
Using license serial number         
Stylesheet compilation time: 345.113654ms        
Processing file:costing.xml        
Streaming file:costing.xml        
Using parser com.sun.org.apache.xerces.internal.jaxp.SAXParserImpl$JAXPSAXParser        
URIResolver.resolve href="" base="file:costing.xsl"        
Using parser com.sun.org.apache.xerces.internal.jaxp.SAXParserImpl$JAXPSAXParser        
Building tree for file:costing.xsl using class net.sf.saxon.tree.tiny.TinyBuilder        
Tree built in 5.206935ms        
Tree size: 237 nodes, 104 characters, 25 attributes        
Error on line 71 of costing.xsl:        
  XTSE3430: Template rule is not streamable        
  * Operand {($currNode/element())/element()} of {let $vv:v0 := ...} selects streamed        
  nodes in a context that allows arbitrary navigation (line 86)        
Template rule is not streamable        
  * Operand {($currNode/element())/element()} of {let $vv:v0 := ...} selects streamed nodes in a context that allows arbitrary navigation (line 86)    

XML结构:

<?xml version="1.0" encoding="UTF-8"?>
<DATA_DS>
   <COSTREPORT>
   <DR>
      <PSU>ABC</PSU>
      <TRU>ABC</TRU>
      <CA>0</CA>
      <DA>0.00</DA>
      <UOM>ABC</UOM>
      <FN>0</FN>
      <RID>0</RID>
      <SD>2018-10-25</SD>
      <DN>ABC</DN>
      <ETD>2018-10-31</ETD>
      <DID>0</DID>
      <LN>ABC</LN>
      <LID>0</LID>
      <PN>ABC</PN>
      <EN>Jane Doe</EN>
      <EID>0</EID>
      <ELN>ABC</ELN>
      <ELV>ABC</ELV>
      <RELA>1234</RELA>
      <ETM>A0</ETM>
      <ASG>A0</ASG>
      <MN>ABC</MN>
      <CRY>ABC</CRY
      ><IVN>ABC</IVN>
      <AD>2018-10-31</AD>
      <CID>0</CID>
      <CCN>ABC</CCN
      ><BOC>ABC</BOC>
      <SG1>0</SG1>
      <SG2>0</SG2>
      <SG3>0</SG3>
      <SG4>0</SG4>
      <SG5>0</SG5>
      <SG9>0</SG9>
      <SG10>0</SG10>
      <TRUID>0</TRUID>
   </DR>
   <DR>
      [...]   
   </DR>
   [...]
   </COSTREPORT>
</DATA_DS>

XSL 文件:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
   <xsl:mode streamable="yes" />
   <xsl:output method="text" />

   <xsl:variable name="delimiter" select="','" />

   <!-- define an array containing the fields we are interested in -->
   <xsl:variable name="fieldArray">
      <field>PSU</field>        <!-- string -->
      <field>TRU</field>        <!-- string -->
      <field>CA</field>         <!-- number -->
      <field>DA</field>         <!-- number -->
      <field>UOM</field>        <!-- string -->
      <field>FN</field>         <!-- number -->
      <field>RID</field>        <!-- number -->
      <field>SD</field>         <!-- date -->
      <field>DN</field>         <!-- string -->
      <field>ETD</field>        <!-- date -->
      <field>DID</field>        <!-- number -->
      <field>LN</field>         <!-- string -->
      <field>LID</field>        <!-- number -->
      <field>PN</field>         <!-- string -->
      <field>EN</field>         <!-- string -->
      <field>EID</field>        <!-- number -->
      <field>ELN</field>        <!-- string -->
      <field>ELV</field>        <!-- string -->
      <field>RELA</field>       <!-- number -->
      <field>ETM</field>        <!-- string -->
      <field>ASG</field>        <!-- string -->
      <field>MN</field>         <!-- string -->
      <field>CRY</field>        <!-- string -->
      <field>IVN</field>        <!-- string -->
      <field>AD</field>         <!-- date -->
      <field>CID</field>        <!-- number -->
      <field>CCN</field>        <!-- string -->
      <field>BOC</field>        <!-- string -->
      <field>SG1</field>        <!-- number -->
      <field>SG2</field>        <!-- number -->
      <field>SG3</field>        <!-- number -->
      <field>SG4</field>        <!-- number -->
      <field>SG5</field>        <!-- number -->
      <field>SG9</field>        <!-- number -->
      <field>SG10</field>       <!-- number -->
      <field>TRUID</field>      <!-- number -->
   </xsl:variable>

   <xsl:param name="fields" select="document('')/*/xsl:variable[@name='fieldArray']/*" />

   <!-- HEADER -->

   <xsl:template match="/">

      <!-- output the header row -->
      <xsl:for-each select="$fields">
         <xsl:if test="position() != 1">
            <xsl:value-of select="$delimiter"/>
         </xsl:if>
         <xsl:value-of select="." />
      </xsl:for-each>

      <!-- output newline -->
      <xsl:text>
</xsl:text>

      <xsl:apply-templates select="DATA_DS/COSTREPORT/DR"/>
   </xsl:template>

   <!-- BODY -->

   <xsl:template match="DR">
    <xsl:variable name="currNode" select="." />

    <!-- output the data row -->
    <!-- loop over the field names and find the value of each one in the xml -->
    <xsl:for-each select="$fields">
      <xsl:if test="position() != 1">
        <xsl:value-of select="$delimiter"/>
      </xsl:if>

      <xsl:value-of select="$currNode/*/*[name() = current()]" />

    </xsl:for-each>

    <!-- output newline -->
    <xsl:text>
</xsl:text>
  </xsl:template>
</xsl:stylesheet>

标签: saxonxslt-3.0

解决方案


问题是变量:

<xsl:variable name="currNode" select="." />

这会将变量绑定到流式输入节点,这是不允许的,因为撒克逊人无法确保您从该输入节点进行的选择“以正确的顺序”完成;您按名称选择此节点的子/后代,并且流式分析无法确定这些后代是按照它们在输入中出现的顺序选择的。

答案其实很简单:把变量改成

<xsl:variable name="currNode" select="copy-of(.)" />

这样,每次点击 DR 元素时,Saxon 都会读取以该元素为根的子树,并将其作为树保存在内存中。因为该变量现在是一个常规的内存节点,而不是一个流式节点,所以它的使用方式没有限制。

请允许我对您的代码发表一些其他评论。

首先,document('')在 XSLT 1.0 中流行的构造现在已经彻底过时了。最好将查找数据放在全局变量中并直接访问它,使用

<xsl:param name="fields" select="$fieldArray/*"/>

document('')如果您尝试编译样式表并在原始源代码位置以外的位置执行它,调用实际上会失败。

其次,输出标题行的代码:

  <xsl:for-each select="$fields">
     <xsl:if test="position() != 1">
        <xsl:value-of select="$delimiter"/>
     </xsl:if>
     <xsl:value-of select="." />
  </xsl:for-each> 

可以简化为

<xsl:value-of select="$fields" separator="{$delimiter}"/>

同样,数据行的代码:

<xsl:for-each select="$fields">
  <xsl:if test="position() != 1">
    <xsl:value-of select="$delimiter"/>
  </xsl:if>
  <xsl:value-of select="$currNode/*/*[name() = current()]" />
</xsl:for-each>

简化为

<xsl:value-of select="for $f in $fields return $currNode/*/*[name()=$f]"
              separator="{$delimiter}"/>

推荐阅读