XSLT将平面结构转换为数组

3
这是源XML代码:

<customers>
    <firstname1>Sean</firstname1>
    <lastname1>Killer</lastname1>
    <sex1>M</sex1>
    <firstname2>Frank</firstname2>
    <lastname2>Woods</lastname2>
    <sex2>M</sex2>
    <firstname3>Jennifer</firstname3>
    <lastname3>Lee</lastname3>
    <sex3>F</sex3>
</customers>

如何将它转换为这个格式?
<MyCustomers>
    <Customer>
        <Name> Sean Killer</Name>
        <Sex>M</Sex>
    </Customer>
    <Customer>
        <Name> Frank Woods</Name>
        <Sex>M</Sex>
    </Customer>
    <Customer>
        <Name>Jennifer Lee</Name>
        <Sex>F</Sex>
    </Customer>
</MyCustomers>

请使用 code 标记并仔细考虑您的帖子。 - Jaques le Fraque
好问题,+1。看看我的答案,它使用XSLT 1.0提供了最通用和灵活的解决方案。即使顶层元素的子元素以任意方式重新排列,它也能产生所需的结果。 :) - Dimitre Novatchev
那么你打算接受什么答案呢? - Emiliano Poggi
3个回答

4

根据评论:

如果元素不是按顺序排列的呢?

在这种情况下(假设使用XSLT 1.0),您可以使用translate()获取元素的id,然后使用concat()构建正确名称来搜索相应的元素。我会将following-sibling::轴更改为../(缩写为parent::),以确保最终也能捕获当前firstname之前的元素。

<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output omit-xml-declaration="yes" indent="yes"/>
    <xsl:strip-space elements="*"/>

    <xsl:template match="customers">
        <MyCustomers>
            <xsl:apply-templates select="*[starts-with(name(),'firstname')]"/>
        </MyCustomers>
    </xsl:template>

    <xsl:template match="*[starts-with(name(),'firstname')]">
        <xsl:variable name="id" select="translate(name(),'firstname','')"/>

        <Customer>
            <Name><xsl:value-of select="concat(.,' ',
                    ../*[name()=concat('lastname',$id)])"/></Name>
            <Sex><xsl:value-of select="../*[name()=concat('sex',$id)]"/></Sex>
        </Customer>
    </xsl:template>

</xsl:stylesheet>

已过时的答案

假设输入文档结构如问题所示,一个良好的工作XSLT 1.0转换是:

<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output omit-xml-declaration="yes" indent="yes"/>
    <xsl:strip-space elements="*"/>

    <xsl:template match="customers">
        <MyCustomers>
            <xsl:apply-templates select="*[starts-with(name(),'firstname')]"/>
        </MyCustomers>
    </xsl:template>

    <xsl:template match="*[starts-with(name(),'firstname')]">
        <Customer>
            <Name><xsl:value-of select="concat(.,' ',
                    following-sibling::*[1]
                    [starts-with(name(),'lastname')])"/></Name>
            <Sex><xsl:value-of select="following-sibling::*[2]
                    [starts-with(name(),'sex')]"/></Sex>
        </Customer>
    </xsl:template>

</xsl:stylesheet>

简要说明

由于您的XML输入中标签名字很奇怪,所以您需要使用XPath 1.0函数starts-with()。您可以使用following-sibling::轴来获取任何名称以firstname开头的元素的所需后续兄弟标签。


谢谢您的回答。但是如果元素不是按顺序排列的呢:<customers> <firstname1>Sean</firstname1> <firstname2>Frank</firstname2> <lastname1>Killer</lastname1> <sex1>M</sex1> <lastname2>Woods</lastname2> <firstname3>Jennifer</firstname3> <lastname3>Lee</lastname3> <sex3>F</sex3> <sex2>M</sex2> </customers> - sean
如果元素不是按顺序排列的,那么您的示例输入就不能正确反映您真实的XML。然而,如果元素没有按顺序排列,您如何知道某个“性别”标签属于某个特定的“名字”?能否请您更明确一些? - Emiliano Poggi
我现在明白了,你的意思是根据元素名称中附加的整数来确定。 - Emiliano Poggi
@sean - 请查看我的XSLT 2.0解决方案。希望你能使用2.0版本。 - Daniel Haley
@empo - 我喜欢你使用translate()而不是像substring()那样的方式。+1 - Daniel Haley
显示剩余3条评论

0
这是一个XSLT 2.0样式表,可以得到你想要的输出,即使它们不按顺序。它还按照"firstname"元素名称进行排序。 示例XML输入(混乱以展示不同的顺序):
<customers>
  <lastname1>Killer</lastname1>
  <sex3>F</sex3>
  <firstname2>Frank</firstname2>
  <firstname1>Sean</firstname1>
  <lastname2>Woods</lastname2>
  <sex2>M</sex2>
  <firstname3>Jennifer</firstname3>
  <sex1>M</sex1>
  <lastname3>Lee</lastname3>
</customers>

XSLT 2.0 样式表(已在 Saxon-HE 9.3 上测试):

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output indent="yes"/>
  <xsl:strip-space elements="*"/>

  <xsl:template match="node()|@*">
    <xsl:choose>
      <xsl:when test="name()[starts-with(.,'firstname')]">
        <xsl:variable name="suffix" select="substring(name(),10)"></xsl:variable>
        <xsl:message><xsl:value-of select="$suffix"/></xsl:message>
        <customer>
          <Name>
            <xsl:value-of select="concat(.,' ',/customers/*[starts-with(name(),'lastname')][ends-with(name(),$suffix)])"/>  
          </Name>
          <Sex>
            <xsl:value-of select="/customers/*[starts-with(name(),'sex')][ends-with(name(),$suffix)]"/>
          </Sex>
        </customer>
      </xsl:when>
      <xsl:when test="name()='customers'">
        <MyCustomers>
          <xsl:apply-templates>
            <xsl:sort select="name()[starts-with(.,'firstname')]"></xsl:sort>
          </xsl:apply-templates>
        </MyCustomers>
      </xsl:when>
      <xsl:otherwise>
        <xsl:apply-templates select="node()|@*"/>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>

</xsl:stylesheet>

输出:

<MyCustomers>
   <customer>
      <Name>Sean Killer</Name>
      <Sex>M</Sex>
   </customer>
   <customer>
      <Name>Frank Woods</Name>
      <Sex>M</Sex>
   </customer>
   <customer>
      <Name>Jennifer Lee</Name>
      <Sex>F</Sex>
   </customer>
</MyCustomers>

0
这个转换会产生想要的结果,即使顶级元素的子元素以任何随意的方式进行了洗牌。
<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:strip-space elements="*"/>

 <xsl:variable name="vNumCustomers"
      select="count(/*/*) div 3"/>

 <xsl:template match="/*">
     <MyCustomers>
       <xsl:for-each select=
           "*[not(position() > $vNumCustomers)]">
         <xsl:variable name="vNum" select="position()"/>

         <Customer>
          <Name>
            <xsl:value-of select=
             "concat(/*/*[name()=concat('firstname',$vNum)],
                     ' ',
                     /*/*[name()=concat('lastname',$vNum)]
                     )
             "/>
          </Name>
          <Sex>
            <xsl:value-of select=
             "/*/*[name()=concat('sex',$vNum)]
             "/>
          </Sex>
         </Customer>
       </xsl:for-each>
     </MyCustomers>
 </xsl:template>
</xsl:stylesheet>

当应用于此 XML 文档(提供的任意重新排列)时:

<customers>
    <sex1>M</sex1>
    <lastname2>Woods</lastname2>
    <lastname1>Killer</lastname1>
    <sex2>M</sex2>
    <firstname3>Jennifer</firstname3>
    <firstname2>Frank</firstname2>
    <lastname3>Lee</lastname3>
    <firstname1>Sean</firstname1>
    <sex3>F</sex3>
</customers>

得到了想要的、正确的结果

<MyCustomers>
   <Customer>
      <Name>Sean Killer</Name>
      <Sex>M</Sex>
   </Customer>
   <Customer>
      <Name>Frank Woods</Name>
      <Sex>M</Sex>
   </Customer>
   <Customer>
      <Name>Jennifer Lee</Name>
      <Sex>F</Sex>
   </Customer>
</MyCustomers>
说明:
  1. 我们计算出展示数据的客户数量。变量$vNumCustomers保存了这个数据。

  2. 对于每个顾客{i}(i = 1 to $vNumCustomers),我们创建相应的<Customer{i}>元素。为了避免使用递归,我们在这里使用the Piez method


我喜欢“count(//) div 3”这个想法,但如果源xml包含其他无关元素(如:<Customers><date>2011-07-11</date><firstname1/><lastname1/>......<Customers>),它可能无法正常工作。 - sean
@sean:当然。有一个更通用的解决方案,我只需要找一些空闲时间来完善我的答案。我现在的答案只是在闲暇的5分钟内完成的。 - Dimitre Novatchev
我很感激。顺便问一下,你更喜欢哪个解决方案?是使用for-each循环的解决方案还是模板匹配的解决方案? - sean
@Dimitre:我真的很喜欢你的 count(/*/*) div 3,但是当其中一个(三个)元素缺失时可能会出现问题。为了测试,请尝试从输入中删除 sex2,这个转换产生的输出将会错过目标。使用我的方法进行尝试,期望的输出将会被保留。 - Emiliano Poggi
@Sean:没有完美的答案,即使@Dimitre的答案通常是完美的。 - Emiliano Poggi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接