如何使用Microsoft xslt 1.0有效地修改大型xml文档

时间:2013-10-09 16:17:44

标签: xml xslt-1.0

我想转换这个xml:

<Root>
  <Result>
    <Message>
      <Header>
        <!-- Hundreds of child nodes -->        
      </Header>
      <Body>
        <Node1>
          <!-- Hundreds of child nodes -->
          <Node2>
            <!-- Hundreds of child nodes -->
            <Node3>
              <!-- Hundreds of child nodes -->
              <NodeX>value 1 to be changed</NodeX>
              <!-- Hundreds of child nodes -->
              <Node4>
                <!-- Hundreds of child nodes -->
                <NodeY>value 2 to be changed</NodeY>
              </Node4>
            </Node3>
          </Node2>          
        </Node1>        
      </Body>
      <RealValuesRoot>
        <!-- These two nodes -->
        <Value ID="1">this value must replace the value of Node X</Value>
        <Value ID="2">this value must replace the value of Node Y</Value>
      </RealValuesRoot>
    </Message>
  </Result>
  <!-- Hundreds to thousands of similar MessageRoot nodes -->
</Root>

进入这个xml:

<Root>
  <Result>
    <Message>
      <Header>
        <!-- Hundreds of child nodes -->
      </Header>
      <Body>
        <Node1>
          <!-- Hundreds of child nodes -->
          <Node2>
            <!-- Hundreds of child nodes -->
            <Node3>
              <!-- Hundreds of child nodes -->
              <NodeX>this value must replace the value of Node X</NodeX>
              <!-- Hundreds of child nodes -->
              <Node4>
                <!-- Hundreds of child nodes -->
                <NodeY>this value must replace the value of Node Y</NodeY>
              </Node4>
            </Node3>
          </Node2>
        </Node1>
      </Body>
    </Message>
  </Result>
  <!-- Hundreds to thousands of similar MessageRoot nodes -->
</Root>

输出几乎与输入相同,但以下更改除外:

  1. 必须使用/ RealValuesRoot / Value节点的值替换X和Y节点值。
  2. 必须从输出中删除/ RealValuesRoot节点。
  3. xml的其余部分必须在输出中保持不变。
  4. “值”节点具有唯一ID,表示消息正文中的唯一x路径,例如, ID 1刷新到xpath / Message / Body / Node1 / Node2 / Node3 / NodeX。

    我必须使用微软的xslt版本1.0 !!

    我已经有了一个可以正常运行的xslt并且做了我想要的一切,但我对性能不满意!

    我的xslt的工作原理如下:

    1. 我创建了一个全局字符串变量,其作用类似于键值对,类似于:1:xpath1_2:xpath2_ ... _N:xpathN。此变量将“Value”节点的ID与消息正文中需要替换的节点相关联。

    2. xslt从根节点开始递归迭代输入xml。

    3. 我计算当前节点的xpath,然后执行以下操作之一:

      1. 如果当前xpath完全匹配全局列表中的一个xpath,那么我将其值替换为相应“Value”节点的值并继续迭代。
      2. 如果当前的xpath指向“RealValuesRoot”节点,那么我省略该节点(不要将其复制到输出中)并继续以递归方式迭代。
      3. 如果当前的xpath在全局ID-xpath字符串中不存在,那么我将完整节点复制到输出并继续迭代。 (这发生在例如/ Message / Header节点中,它永远不会包含任何需要替换的节点)
      4. 如果当前的xpath部分匹配全局列表中的一个xpath,那么我只是继续递归迭代,直到我达到上述3个案例中的一个。
    4. 如上所述,我的xslt工作正常,但我想尽可能提高性能,请随时提出一个全新的xslt逻辑!欢迎您的想法和建议!

2 个答案:

答案 0 :(得分:2)

  

我计算当前节点的xpath,然后执行以下操作之一......

这可能是您的低效率 - 如果您每次可能正在查看O(N 2 )算法时重新计算回到根的路径。在没有看到你的XSLT的情况下,这是相当推测的,但你可以通过使用参数将当前路径传递给递归来调整这一点 - 如果你的主算法基于标准身份模板

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

然后将其更改为

<xsl:template match="@*|node()">
  <xsl:param name="curPath" />
  <xsl:copy>
    <xsl:apply-templates select="@*|node()">
      <xsl:with-param name="curPath" select="concat($curPath, '/', name())" />
    </xsl:apply-templates>
  </xsl:copy>
</xsl:template>

或用于构建所需路径的逻辑。现在,在您要按摩的节点的特定模板中,您已经拥有了其父节点的路径,并且您不必每次都一直走到根目录。

您可能需要添加

<xsl:template match="/">
  <xsl:apply-templates />
</xsl:template>

所以你不会在$curPath

的前面得到双斜杠
  

xpath在xslt中作为全局变量

中的字符串进行硬编码

如果您在XML结构中表示此映射而不是字符串,那么您可以使用 key 机制来加速查找:

<xsl:variable name="rtfLookupTable">
  <lookuptable>
    <val xpath="/first/xpath/expression" id="1" />
    <val xpath="/second/xpath/expression" id="2" />
    <!-- ... -->
  </lookuptable>
</xsl:variable>

<xsl:variable name="lookupTable" select="msxsl:node-set($rtfLookupTable)" />

<xsl:key name="valByXpath" match="val" use="@xpath" />

(将xmlns:msxsl="urn:schemas-microsoft-com:xslt"添加到您的xsl:stylesheet)。或者,如果您想避免使用node-set扩展功能,则可以使用替代定义

<xsl:variable name="lookupTable" select="document('')//xsl:variable[name='rtfLookupTable']" />

通过将样式表本身视为纯XML文档来工作。

多个文档中的键在XSLT 1.0中有点繁琐但可以完成,基本上你必须在调用键函数之前将当前上下文切换到指向$lookupTable,所以你需要保存当前变量中的上下文让你稍后引用它:

<xsl:template match="text()">
  <xsl:param name="curPath" />

  <xsl:variable name="dot" select="." />
  <xsl:variable name="slash" select="/" />

  <xsl:for-each select="$lookupTable">
    <xsl:variable name="valId" select="key('valByXpath', $curPath)/@id" />
    <xsl:choose>
      <xsl:when test="$valId">
        <xsl:value-of select="$slash//Value[@id = $valId]" />
        <!-- or however you extract the right Value -->
      </xsl:when>
      <xsl:otherwise>
        <xsl:value-of select="$dot" />
      </xsl:otherwise>
    </xsl:choose>
  </xsl:for-each>
</xsl:template>

或者实际上,为什么不让XSLT引擎为您完成艰苦的工作。而不是将您的映射表示为字符串

/path/to/node1_1:/path/to/node2_2

直接将其表示为模板

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <!-- copy everything as-is apart from exceptions below -->
  <xsl:template match="@*|node()">
    <xsl:copy><xsl:apply-templates select="@*|node()" /></xsl:copy>
  </xsl:template>

  <!-- delete the RealValuesRoot -->
  <xsl:template match="RealValuesRoot" />

  <xsl:template match="/path/to/node1">
    <xsl:copy><xsl:value-of select="//Value[id='1']" /></xsl:copy>
  </xsl:template>
  <xsl:template match="/path/to/node2">
    <xsl:copy><xsl:value-of select="//Value[id='2']" /></xsl:copy>
  </xsl:template>
</xsl:stylesheet>

我相信您可以看到如何使用某种模板机制(甚至可能是另一种XSLT)从现有映射中轻松自动生成特定模板。

答案 1 :(得分:1)

最有效的方法是使用XSL密钥。

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

  <!-- a key that indexes real values by their IDs -->
  <xsl:key name="kRealVal" match="RealValuesRoot/Value" use="@ID" />

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

  <!-- ...except elements named <NodeX> -->
  <xsl:template match="*[starts-with(name(), 'Node')]">
    <xsl:variable name="myID" select="substring-after(name(), 'Node')" />
    <xsl:variable name="myRealVal" select="key('kRealVal', $myID)" />

    <xsl:copy>
      <xsl:copy-of select="@*" />
      <xsl:choose>
        <xsl:when test="$myRealVal">
          <xsl:value-of select="$myRealVal" />
        </xsl:when>
        <xsl:otherwise>
          <xsl:apply-templates select="node()" />
        </xsl:otherwise>
      </xsl:choose>
    </xsl:copy>
  </xsl:template>

  <!-- the <RealValuesRoot> element can be trashed -->
  <xsl:template match="RealValuesRoot" />
</xsl:stylesheet>

以下是此解决方案的实时预览:http://www.xmlplayground.com/R78v0n


这是一个概念验证解决方案,它使用Microsoft脚本扩展来完成繁重的任务:

<xsl:stylesheet
  version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:msxsl="urn:schemas-microsoft-com:xslt"
  xmlns:script="http://tempuri.org/script"
>  
  <msxsl:script language="JScript" implements-prefix="script">
    var index = {};

    function getNode(context, xpath) {
      var theContext = context[0],
          theXpath = xpath[0].text,
          result;

      try {
        result = theContext.selectSingleNode(theXpath)
      } catch (ex) {
        // xpath is invalid. we could also just throw here
        // but lets return the empty node set.
        result = theContext.selectSingleNode("*[false()]");
      }
      return result;
    }
    function buildIndex(id, node) {
      var theNode = node[0];

      if (id) index[id] = theNode;
      return "";
    }
    function getValue(id) {
      return (id in index) ? index[id] : '';
    }
  </msxsl:script>


  <!-- this is the boilerplate to evaluate all the XPaths -->
  <xsl:variable name="temp">
    <xsl:for-each select="/root/source/map">
      <xsl:value-of select="script:buildIndex(generate-id(script:getNode(/, @xpath)), .)" />
    </xsl:for-each>
  </xsl:variable>

  <!-- the identity template to get things rolling -->
  <xsl:template match="node() | @*">
    <xsl:copy>
      <xsl:apply-templates select="node() | @*" />
    </xsl:copy>
  </xsl:template>

  <xsl:template match="/">
    <!-- actually evaluate $temp once, so the variable is being calculated -->
    <xsl:value-of select="$temp" />
    <xsl:apply-templates select="node() | @*" />
  </xsl:template> 

  <!-- all <value> nodes do either have a related "actual value" or they are copied as they are -->
  <xsl:template match="value">
    <xsl:copy>
      <xsl:copy-of select="@*" />

      <xsl:variable name="newValue" select="script:getValue(generate-id())" />
      <xsl:choose>
        <xsl:when test="$newValue">
          <xsl:value-of select="$newValue" />
        </xsl:when>
        <xsl:otherwise>
          <xsl:apply-templates select="node() | @*" />
        </xsl:otherwise>
      </xsl:choose>
    </xsl:copy>
  </xsl:template>

  <!-- the <source> element can be dropped -->
  <xsl:template match="source" />

</xsl:stylesheet>

它转换

<root>
  <value id="foo">this is to be replaced</value>

  <source>
    <map xpath="/root/value[@id = 'foo']">this is the new value</map>
  </source>
</root>

<root>
  <value id="foo">this is the new value</value>
</root>

也许你可以在你的设置中走这条路。

思路是:

  • 迭代您拥有的所有XPath,并使用.selectSingleNode()评估它们。
  • 将每个评估结果(理想情况下为一个节点)及其唯一ID作为键值对存储在对象中。这使用XSLT的generate-id()从节点获取ID。
  • 现在正常转换输入。对于有问题的每个节点,获取其ID并检查该节点是否实际存在“新值”。
  • 如果是,请插入新值,如果没有,则继续转换。

使用msxsl.exe成功测试。

当然,这假设输入具有那些<map xpath="...">元素,但该部分并非真正必要且易于适应您的实际情况。例如,您可以从JavaScript中index的长字符串中构建split()对象。