Scala - 修改xml中的嵌套元素

时间:2009-06-09 15:11:15

标签: xml scala

我正在学习scala,我正在寻找更新某个xml中的嵌套节点。我有一些工作,但我想知道它是否是最优雅的方式。

我有一些xml:

val InputXml : Node =
<root>
    <subnode>
        <version>1</version>
    </subnode>
    <contents>
        <version>1</version>
    </contents>
</root>

我想更新子节点中的版本节点,但不更新内容中的节点。

这是我的功能:

def updateVersion( node : Node ) : Node = 
 {
   def updateElements( seq : Seq[Node]) : Seq[Node] = 
   {
        var subElements = for( subNode <- seq ) yield
        {
            updateVersion( subNode )
        }   
        subElements
   }

   node match
   {
     case <root>{ ch @ _* }</root> =>
     {
        <root>{ updateElements( ch ) }</root>
     }
     case <subnode>{ ch @ _* }</subnode> =>
     {
         <subnode>{ updateElements( ch ) }</subnode> 
     }
     case <version>{ contents }</version> =>
     {
        <version>2</version>
     }
     case other @ _ => 
     {
         other
     }
   }
 }

有没有更简洁的方式来编写这个函数?

9 个答案:

答案 0 :(得分:55)

这一次,并没有人真正给出最合适的答案!不过,现在我已经了解了它,这是我对它的新看法:

import scala.xml._
import scala.xml.transform._

object t1 extends RewriteRule {
  override def transform(n: Node): Seq[Node] = n match {
    case Elem(prefix, "version", attribs, scope, _*)  =>
      Elem(prefix, "version", attribs, scope, Text("2"))
    case other => other
  }
}

object rt1 extends RuleTransformer(t1)

object t2 extends RewriteRule {
  override def transform(n: Node): Seq[Node] = n match {
    case sn @ Elem(_, "subnode", _, _, _*) => rt1(sn)
    case other => other
  }
}

object rt2 extends RuleTransformer(t2)

rt2(InputXml)

现在,做一些解释。类RewriteRule是抽象的。它定义了两种方法,都称为transform。其中一个只需NodeSequence,另一个Node transform。它是一个抽象类,所以我们不能直接实例化它。通过添加定义,在这种情况下覆盖其中一个RuleTransformer方法,我们正在创建它的匿名子类。每个RewriteRule都需要关注一个任务,尽管它可以做很多。

接下来,课程RewriteRule将可变数量的Node作为参数。它的变换方法需要Sequence并返回Node RewriteRule,通过应用用于实例化的每个BasicTransformer

这两个类派生自apply,它定义了一些不需要在更高层次上关注自己的方法。不过,transform方法调用RuleTransformer,因此RewriteRuleRuleTransformer都可以使用与之关联的语法糖。在这个例子中,前者和后者没有。

这里我们使用两个Elem级别,因为第一个级别将过滤器应用于更高级别的节点,第二个级别将更改应用于通过过滤器的任何过程。

还使用了提取器version,因此无需关注命名空间等细节或是否存在属性。并非元素2的内容被完全丢弃并替换为_*。如果需要,它也可以匹配。

另请注意,提取器的最后一个参数是_,而不是*。这意味着这些元素可以有多个孩子。如果您忘记了Text,则匹配可能会失败。在示例中,如果没有空格,匹配不会失败。由于空格被转换为subnode个元素,version下的单个空格会使匹配失败。

此代码比其他提出的建议更大,但它的优点是对XML结构的了解远远少于其他建议。它会更改任何名为subnode的元素 - 无论多少级别 - 一个名为RewriteRule的元素,无论名称空间,属性等等。

此外......好吧,如果你要进行很多转换,递归模式匹配就会变得很快。使用RuleTransformerxslt,您可以使用Scala代码有效地替换{{1}}个文件。

答案 1 :(得分:13)

您可以使用Lift的CSS Selector Transforms并编写:

"subnode" #> ("version *" #> 2)

请参阅http://stable.simply.liftweb.net/#sec:CSS-Selector-Transforms

答案 2 :(得分:11)

我认为最初的逻辑是好的。 这与(我敢说什么?)Scala-ish风味的代码相同:

def updateVersion( node : Node ) : Node = {
   def updateElements( seq : Seq[Node]) : Seq[Node] = 
     for( subNode <- seq ) yield updateVersion( subNode )  

   node match {
     case <root>{ ch @ _* }</root> => <root>{ updateElements( ch ) }</root>
     case <subnode>{ ch @ _* }</subnode> => <subnode>{ updateElements( ch ) }</subnode>
     case <version>{ contents }</version> => <version>2</version>
     case other @ _ => other
   }
 }

它看起来更紧凑(但实际上是相同的:))

  1. 我摆脱了所有不必要的东西 括号
  2. 如果需要支架,则从中开始 同一行
  3. updateElements只定义了一个var 并返回它,所以我摆脱了它 并直接返回结果
  4. 如果你愿意,你也可以摆脱updateElements。您希望将updateVersion应用于序列的所有元素。这是map method。有了它,你可以重写行

    case <subnode>{ ch @ _* }</subnode> => <subnode>{ updateElements( ch ) }</subnode>
    

    case <subnode>{ ch @ _* }</subnode> => <subnode>{ ch.map(updateVersion (_)) }</subnode>
    

    由于更新版本只需要1个参数,我99%肯定你可以省略它并写下:

    case <subnode>{ ch @ _* }</subnode> => <subnode>{ ch.map(updateVersion) }</subnode>
    

    结束于:

    def updateVersion( node : Node ) : Node = node match {
             case <root>{ ch @ _* }</root> => <root>{ ch.map(updateVersion )}</root>
             case <subnode>{ ch @ _* }</subnode> => <subnode>{ ch.map(updateVersion ) }</subnode>
             case <version>{ contents }</version> => <version>2</version>
             case other @ _ => other
           }
    

    您怎么看?

答案 3 :(得分:6)

我已经学到了更多,并在另一个答案中提出了我认为是一个优秀的解决方案。我已经解决了这个问题,因为我注意到我没有考虑subnode限制。

感谢您的提问!我刚刚在处理XML时学到了一些很酷的东西。这是你想要的:

def updateVersion(node: Node): Node = {
  def updateNodes(ns: Seq[Node], mayChange: Boolean): Seq[Node] =
    for(subnode <- ns) yield subnode match {
      case <version>{ _ }</version> if mayChange => <version>2</version>
      case Elem(prefix, "subnode", attribs, scope, children @ _*) =>
        Elem(prefix, "subnode", attribs, scope, updateNodes(children, true) : _*)
      case Elem(prefix, label, attribs, scope, children @ _*) =>
        Elem(prefix, label, attribs, scope, updateNodes(children, mayChange) : _*)
      case other => other  // preserve text
    }

  updateNodes(node.theSeq, false)(0)
}

现在,解释。第一个和最后一个案例陈述应该很明显。最后一个用于捕获非元素的XML部分。或者,换句话说,文本。但请注意,在第一个语句中,对标志进行测试以指示是否可以更改version

第二和第三个案例陈述将使用针对对象Elem的模式匹配器。这会将元素分解为所有其组成部分。最后一个参数“children @ _ *”将匹配子项列表中的任何内容。或者,更具体地说,Seq [Node]。然后我们用我们提取的部分重建元素,但是将Seq [Node]传递给updateNodes,进行递归步骤。如果我们匹配元素subnode,那么我们将标志mayChange更改为true,从而更改版本。

在最后一行中,我们使用node.theSeq从Node生成Seq [Node],并使用(0)获取作为结果返回的Seq [Node]的第一个元素。由于updateNodes本质上是一个map函数(对于... yield被转换为map),我们知道结果只有一个元素。我们传递false标记,以确保除非version元素是祖先,否则不会更改subnode

有一种略微不同的方式,它更强大,但更冗长和模糊:

def updateVersion(node: Node): Node = {
  def updateNodes(ns: Seq[Node], mayChange: Boolean): Seq[Node] =
    for(subnode <- ns) yield subnode match {
      case Elem(prefix, "version", attribs, scope, Text(_)) if mayChange => 
        Elem(prefix, "version", attribs, scope, Text("2"))
      case Elem(prefix, "subnode", attribs, scope, children @ _*) =>
        Elem(prefix, "subnode", attribs, scope, updateNodes(children, true) : _*)
      case Elem(prefix, label, attribs, scope, children @ _*) =>
        Elem(prefix, label, attribs, scope, updateNodes(children, mayChange) : _*)
      case other => other  // preserve text
    }

  updateNodes(node.theSeq, false)(0)
}

此版本允许您更改任何“版本”标记,无论其前缀,属性和范围如何。

答案 4 :(得分:3)

Scales Xml提供了“就地”编辑工具。当然这一切都是不可改变的,但这里是Scales的解决方案:

val subnodes = top(xml).\*("subnode"l).\*("version"l)
val folded = foldPositions( subnodes )( p => 
  Replace( p.tree ~> "2"))

XPath like语法是Scales签名功能,字符串后面的l指定它不应该没有命名空间(仅限本地名称)。

foldPositions遍历生成的元素并对其进行转换,将结果重新组合在一起。

答案 5 :(得分:1)

一种方法是镜片(例如scalaz's)。请参阅http://arosien.github.io/scalaz-base-talk-201208/#slide35以获得非常清晰的演示文稿。

答案 6 :(得分:0)

检查该项目:https://github.com/geirolz/advxml

这是一个基于RuleTransformer(标准scala xml库)和Cats的简单库,旨在简化XML转换和序列化。

答案 7 :(得分:0)

如果 2021 年有任何可怜的人仍然需要使用 Scala 处理 XML,那么这里还有一个我觉得特别好的基于库的解决方案:

import scala.xml._
import jstengel.ezxml.core.SimpleWrapper.ElemWrapper
import jstengel.ezxml.core.XmlPath.\~

val InputXml: Elem = <root>
    <subnode>
        <version>1</version>
    </subnode>
    <contents>
        <version>1</version>
    </contents>
</root>

(InputXml \~ "subnode" \~ "version").transformTarget(_ => <version>2</version>)

有问题的图书馆:https://github.com/JulienSt/ezXML(谢谢你,JulienSt 先生!)

https://scastie.scala-lang.org/EYJG5B91Q3KiVD7h9940JA 尝试

答案 8 :(得分:-2)

我真的不知道如何优雅地完成这项工作。 FWIW,我会采用不同的方法:为您正在处理的信息使用自定义模型类,并为其转换Xml。您可能会发现它是一种更好的处理数据的方式,而且它更加苛刻。

然而,有一种很好的方法可以直接使用Xml,我希望看到它。