XSLT(2.0 或 3.0)方法将存储在 xml 中的每个逗号分隔值的整个 xml 复制到单独的 xml 文件中

Posted

技术标签:

【中文标题】XSLT(2.0 或 3.0)方法将存储在 xml 中的每个逗号分隔值的整个 xml 复制到单独的 xml 文件中【英文标题】:XSLT (2.0 or 3.0) way of duplicating whole xml for each comma separated value stored in xml into separate xml files 【发布时间】:2021-11-27 02:38:57 【问题描述】:

我有一个 XML 文件,它是来自网络订单系统的 workOrder。它包含大量订单数据,其中一个值是多个文件路径的分隔字符串。 我想复制完整的 XML 并为每个 fileURL 输出一个 xml,并用每个 fileURL 换出值(每个 xml 中的单个文件路径)。原因是稍后使用的工作流系统读取文件的路径并将其拾取并将 xml 关联为元数据以进行进一步处理,但每个文件需要一个 xml。

输入XML(包含存储路径的部分):

<rootNode> 
... 
<properties>
<property>
<name label='fileURL'>fileurl</name>
<value>\\nas02\Order\O10346_OP176786_X1.pdf, \\nas02\Order\Weborder\O10346_OP176789_X2.pdf, \\nas02\Order\Weborder\O10346_OP176795_X3.pdf, \\nas02\Order\Weborder\O10346_OP176796_X1.pdf,
</value>   
</property>   
</properties> 
</technicalSpec> 
... 
</rootNode>

预期的输出将是每个包含相同数据的 fileURL 的一个 xml,除了属性值应该是每个副本的单个 fileURL:

<rootNode> 
    ... 
    <properties>
    <property>
    <name label='fileURL'>fileurl</name>
    <value>\\nas02\Order\O10346_OP176786_X1.pdf
    </value>   
    </property>   
    </properties> 
    </technicalSpec> 
    ... 
    </rootNode>

我知道如何将 csv 字符串放入变量中:

<xsl:variable name="csv" select="//property[name='fileurl']/value"></xsl:variable>

我发现我可以为值做一个 for-each 循环:

<xsl:for-each select="tokenize($csv, ',')">

我还发现了如何复制整个 xml 内容:

<xsl:template match="@*|node()">
            <xsl:copy>
                <xsl:apply-templates select="@*|node()"/>
            </xsl:copy>
    </xsl:template>

而且我知道我可以在 for-each 循环中使用“结果文档”来创建单独的输出文件。

但我无法弄清楚如何将所有内容组合成一个有效的 xslt(如果可能的话)以为每个 csv 值创建一个 xml。

【问题讨论】:

每个结果文档的名称和文件路径应该是什么?我想您不想保存扩展名为 .pdf 的 XML 文档? 【参考方案1】:

这个问题在结构上与Split google XML items by param value 中的问题相同,但我不会将其标记为重复,因为对于初学者来说,如何将问题的答案转换为您的需要可能并不明显。

这种方法的本质是:

<xsl:mode on-no-match="shallow-copy"/>

<xsl:template match="/">
  <xsl:variable name="root" select="/*"/>
  <xsl:for-each select="tokenize(//value, ',')!normalize-space()">
     <xsl:result-document href="position().xml">
       <xsl:apply-templates select="$root">
         <xsl:with-param name="current-file" select="."/>
       </xsl:apply-templates>
     </xsl:result-document>
  </xsl:for-each>
</xsl:template>

<xsl:template match="value">
  <xsl:param name="current-file"/>
  <value>$current-file</value>
</xsl:template>

请注意,这取决于内置模板规则通过未更改的方式复制参数值(它们的行为实际上类似于隧道参数)。当然也可以显式声明为隧道参数。

【讨论】:

谢谢。正如您所说,这与给出的第一个答案非常相似。您建议的两个答案都对我有用。感谢您的帮助。【参考方案2】:

在给定 XSLT 3 的情况下,我会使用隧道参数和 xsl:mode

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  version="3.0"
  xmlns:xs="http://www.w3.org/2001/XMLSchema"
  exclude-result-prefixes="#all"
  expand-text="yes">

  <xsl:output method="xml" indent="yes"/>
  
  <xsl:template match="/">
    <xsl:variable name="main-root" select="/"/>
    <xsl:for-each select="tokenize(rootNode/technicalSpec/properties/property[name[@label = 'fileURL' and . = 'fileurl']]/value, ',s*')[normalize-space()]">
      <xsl:result-document href="substring-before(., '.pdf').xml">
        <xsl:apply-templates select="$main-root/*">
          <xsl:with-param name="url" select="." tunnel="yes"/>
        </xsl:apply-templates>        
      </xsl:result-document>
    </xsl:for-each>
  </xsl:template>
  
  <xsl:template match="property[name[@label = 'fileURL' and . = 'fileurl']]/value">
    <xsl:param name="url" tunnel="yes"/>
    <xsl:copy>$url</xsl:copy>
  </xsl:template>

  <xsl:mode on-no-match="shallow-copy"/>

</xsl:stylesheet>

【讨论】:

与我的回答几乎相同,只是您对输入 XML 中的省略号的解释与我理解的方式不同。 谢谢。这个工作正常。我做了一些小改动,以更具体地适合我需要作为磁盘输出的文件名。【参考方案3】:

这个问题已经有三个答案,它们都做同样的事情:标记value 元素,为每个标记创建一个result-document 并应用具有当前标记的模板作为用于模板匹配的参数value.

我会建议一种不同的方法,我认为它更简单:

XSLT 3.0

<xsl:stylesheet version="3.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
expand-text="yes">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>

<xsl:mode on-no-match="shallow-copy"/>

<xsl:template match="/">
  <xsl:variable name="root" select="."/>
    <xsl:analyze-string select="//value" regex="(.+?)($|, )">
        <xsl:matching-substring>
            <xsl:result-document href="position().xml">
                <xsl:apply-templates select="$root/*"/>
            </xsl:result-document>
        </xsl:matching-substring>
    </xsl:analyze-string>
</xsl:template>

<xsl:template match="value">
    <xsl:copy>regex-group(1)</xsl:copy>
</xsl:template>

</xsl:stylesheet>

请注意,我假设了一个正确分隔的字符串,格式为:

<value>\\nas02\Order\O10346_OP176786_X1.pdf, \\nas02\Order\Weborder\O10346_OP176789_X2.pdf, \\nas02\Order\Weborder\O10346_OP176795_X3.pdf, \\nas02\Order\Weborder\O10346_OP176796_X1.pdf</value>   

演示(模拟):https://xsltfiddle.liberty-development.net/3MP42Pb

【讨论】:

【参考方案4】:

实现此目的的一种方法是使用由xsl:apply-templates 填充的变量,并带有mode 属性。第一步正如您所怀疑的那样,但更改结果文档中的一个元素有点棘手。

在这种方法中,首先,我用行链接到输入文档

<xsl:variable name="doc" select="/" />

制作了带有路径的文件名的副本 - 正如您已经建议的那样:

<xsl:variable name="csv" select="//property[name='fileurl']/value" />

这里应用了xsl:for-each。 作为输出文件名,我简单地选择了$csv 字符串的当前部分(本次迭代)的最后一部分:

<xsl:variable name="result-name" select="string-join(tokenize(., '\\')[position() = last()], '')" />

然后我使用一个变量,其值由apply-templates 填充,并带有提到的mode="new" 属性;使用此模式应用于模板;其中之一更改了xsl:param 给出的参数中设置的值:

<xsl:variable name="new-doc">
    <xsl:apply-templates select="$doc" mode="new">
        <xsl:with-param name="nam" select="normalize-space(.)" />
    </xsl:apply-templates>
</xsl:variable>

现在,具有mode="new" 属性的两个模板被执行。 最后,将变量用xsl:result-document写入对应的文档:

<xsl:result-document encoding="UTF-8" href="$result-name">
    <xsl:copy-of select="$new-doc" />
</xsl:result-document>

整个样式表可能如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" omit-xml-declaration="yes" indent="yes"/>

    <xsl:variable name="doc" select="/" />
    <xsl:variable name="csv" select="//property[name='fileurl']/value" />

    <xsl:template match="/">
        <xsl:for-each select="tokenize($csv, ',')">
            <xsl:variable name="result-name" select="string-join(tokenize(., '\\')[position() = last()], '')" />            
            <xsl:variable name="new-doc">
                <xsl:apply-templates select="$doc" mode="new">
                    <xsl:with-param name="nam" select="normalize-space(.)" />
                </xsl:apply-templates>
            </xsl:variable>
            <xsl:result-document encoding="UTF-8" href="$result-name">
                <xsl:copy-of select="$new-doc" />
            </xsl:result-document>
        </xsl:for-each>
    </xsl:template>

    <xsl:template match="value" mode="new">
        <xsl:param name="nam" />
        <value><xsl:value-of select="$nam" /></value>
    </xsl:template>

    <!-- identity template -->
    <xsl:template match="node()|@*" mode="new">
        <xsl:param name="nam" />
        <xsl:copy>
            <xsl:apply-templates select="node()|@*" mode="new">
                <xsl:with-param name="nam" select="$nam" />
            </xsl:apply-templates>
        </xsl:copy>
    </xsl:template> 

</xsl:stylesheet>

【讨论】:

&lt;xsl:variable name="doc" select="/" /&gt; 不会“制作输入文档的副本”。创建$new-doc 变量只是为了将其内容复制到输出的目的是什么?为什么不直接写入输出?可能还有更多,但这两个跳出来了。 是的。这不是一个副本,而是一个链接。我改变了那个。从this answer at SO 获得灵感。是的,这个变量可以被优化掉。

以上是关于XSLT(2.0 或 3.0)方法将存储在 xml 中的每个逗号分隔值的整个 xml 复制到单独的 xml 文件中的主要内容,如果未能解决你的问题,请参考以下文章

BizTalk Server 2016 映射中是不是支持 XSLT 2.0 或 3.0?

XSLT 3.0 - 无法在 XSLT 3.0 xml-to-json() 中获取对象数组

XSLT 3.0 - 在 XSLT 3.0 xml-to-json() 中出现错误“重复键值”

将 XML 作为参数传递和解析到 XSLT 2.0

XSLT 使用 xslt 2.0 或更高版本将纯文本文件处理为 XML

SSIS - XML 任务未使用我创建的 XSLT-2.0 转换我的 XML 文件