Scala XML Building:将子节点添加到现有节点

时间:2010-02-04 10:56:15

标签: xml scala

我有一个XML节点,我希望随着时间的推移添加子节点:

val root: Node = <model></model>

但我看不到像 addChild()这样的方法,因为我想写下以下内容:

def addToModel() = {
    root.addChild(<subsection>content</subsection>)
}

因此,在对此方法进行一次调用后,根xml将为:

<model><subsection>content</subsection></model>

我能看到的唯一可以附加节点的类是NodeBuffer。我在这里遗漏了一些基本的东西吗?

9 个答案:

答案 0 :(得分:29)

从这开始吧:

def addChild(n: Node, newChild: Node) = n match {
  case Elem(prefix, label, attribs, scope, child @ _*) =>
    Elem(prefix, label, attribs, scope, child ++ newChild : _*)
  case _ => error("Can only add children to elements!")
}

方法++在此处有效,因为childSeq[Node],而newChildNode,其扩展为NodeSeqSeq[Node]

现在,这并没有改变任何东西,因为Scala中的XML是不可变的。它将生成一个具有所需更改的新节点。唯一的成本是创建新的Elem对象,以及创建新的Seq个孩子。子节点本身不会被复制,只是被引用,这不会导致问题,因为它们是不可变的。

但是,如果要将子节点添加到XML层次结构的节点上,事情会变得复杂。一种方法是使用拉链,如this blog中所述。

但是,您可以使用scala.xml.transform,其规则将更改特定节点以添加新子节点。首先,编写一个新的变压器类:

class AddChildrenTo(label: String, newChild: Node) extends RewriteRule {
  override def transform(n: Node) = n match {
    case n @ Elem(_, `label`, _, _, _*) => addChild(n, newChild)
    case other => other
  }
}

然后,像这样使用它:

val newXML = new RuleTransformer(new AddChildrenTo(parentName, newChild)).transform(oldXML).head

在Scala 2.7上,将head替换为first

Scala 2.7上的示例:

scala> val oldXML = <root><parent/></root>
oldXML: scala.xml.Elem = <root><parent></parent></root>

scala> val parentName = "parent"
parentName: java.lang.String = parent

scala> val newChild = <child/>
newChild: scala.xml.Elem = <child></child>

scala>     val newXML = new RuleTransformer(new AddChildrenTo(parentName, newChild)).transform(oldXML).first
newXML: scala.xml.Node = <root><parent><child></child></parent></root>

如果只是父元素不够,你可以让它变得更复杂。但是,如果您需要将子项添加到具有特定索引的公用名的父项,那么您可能需要采用拉链方式。

例如,如果您有<books><book/><book/></books>,并且想要将<author/>添加到第二个,那么使用规则转换器很难做到。你需要一个针对books的RewriteRule,然后得到它的child(它本应该被命名为children),找到n th {{ 1}}在其中,将新子项添加到其中,然后重构子项并构建新节点。可行,但如果你必须做太多,拉链可能会更容易。

答案 1 :(得分:8)

在Scala中,xml节点是不可变的,但可以这样做:

var root = <model/>

def addToModel(child:Node) = {
  root = root match {
    case <model>{children@ _*}</model> => <model>{children ++ child}</model>
    case other => other
  }
}

addToModel(<subsection>content</subsection>)

它重写了一个新的xml,复制了旧的xml并将你的节点添加为子节点。

编辑:Brian提供了更多信息,我想要一个不同的匹配。

要将子项添加到2.8中的任意节点,您可以执行以下操作:

def add(n:Node,c:Node):Node = n match { case e:Elem => e.copy(child=e.child++c) }

这将返回添加了子节点的父节点的新副本。假设你的子节点在可用时已经堆叠了:

scala> val stack = new Stack[Node]()
stack: scala.collection.mutable.Stack[scala.xml.Node] = Stack()

一旦你认为你已经完成了检索孩子的工作,你就可以在父母上打电话来添加堆栈中的所有孩子,如下所示:

stack.foldRight(<parent/>:Node){(c:Node,n:Node) => add(n,c)}

我不知道使用StackfoldRight的性能影响因此,根据您堆积的孩子数量,您可能需要修补...然后您可能需要致电{ {1}}也是。希望这可以照顾stack.clear的不变性,也可以根据需要处理你的过程。

答案 2 :(得分:6)

由于scala 2.10.0 Elem的实例构造函数已经改变,如果你想使用@Daniel C. Sobral编写的朴素解决方案,它应该是:

xmlSrc match {
  case xml.Elem(prefix, label, attribs, scope, child @ _*) =>
       xml.Elem(prefix, label, attribs, scope, child.isEmpty, child ++ ballot : _*)
  case _ => throw new RuntimeException
}

对我而言,它的效果非常好。

答案 3 :(得分:2)

以通常的Scala方式,所有Node,Elem等实例都是不可变的。 你可以反过来使用它:

  scala> val child = <child>foo</child>
  child: scala.xml.Elem = <child>foo</child>

  scala> val root = <root>{child}</root>
  root: scala.xml.Elem = <root><child>foo</child></root>

有关详细信息,请参阅http://sites.google.com/site/burakemir/scalaxbook.docbk.html

答案 4 :(得分:2)

由于XMLimmutable,因此每次要添加节点时都必须创建一个新节点,您可以使用Pattern matching添加新节点:

    var root: Node = <model></model>
    def addToModel(newNode: Node) = root match {
       //match all the node from your model
       // and make a new one, appending old nodes and the new one
        case <model>{oldNodes@_*}</model> => root = <model>{oldNodes}{newNode}</model>
    }
    addToModel(<subsection>content</subsection>)

答案 5 :(得分:2)

我同意你必须“反过来”使用XML。请记住,当信息可用时,您不必拥有整个XML文档,只需在应用程序需要读取XML时编写XML。

保持子部分状态,但是当您需要XML时,将其全部包装在一起。

  val subsections : List[Elem]

  def wrapInModel(f : => Elem) = {
    <model>{f}</model>
  }

  wrapInModel(subsections)

  def wrapInModel(f : => Elem) = {
    <model>{f}</model>
  }
  wrapInModel(<subsection>content</subsection>)

答案 6 :(得分:1)

Scales Xml允许通过折叠XPath进行简单的就地更改,将子节点添加到特定的子节点就适合这种方法。

有关详细信息,请参阅In-Place Transformations

答案 7 :(得分:1)

你的根定义实际上是一个Elem对象,它是节点的子类,所以如果丢弃不必要的Node类型(隐藏它的实现),你实际上可以在它上面做一个++,因为Elem类有这个方法。

val root = <model/>
val myChild = <myChild/>
root.copy(child = root.child ++ myChild)

scala ev:

root: scala.xml.Elem = <model/>
myChild: scala.xml.Elem = <mychild/>
res2: scala.xml.Elem = <model><mychild/></model>

由于每个Elem和每个Node都是NodeSeq,即使你附加的是一个未知序列,你也可以非常有效地添加它们:

val root = <model/>
//some node sequence of unknown subtype or structure
val children: scala.xml.NodeSeq = <node1><node2/></node1><node3/> 
root.copy(child = root.child ++ children)

scala ev:

root: scala.xml.Elem = <model/>
children: scala.xml.NodeSeq = NodeSeq(<node1><node2/></node1>, <node3/>)
res6: scala.xml.Elem = <model><node1><node2/></node1><node3/></model>

答案 8 :(得分:0)

我通过以下方式实现我的'appendChild'方法:

  def appendChild(elem: Node, child: Node, names: String) = {
    appendChild(elem, child, names.split("/"))
  }

  private def appendChild(elem: Node, child: Node, names: Array[String]) = {
    var seq = elem.child.diff(elem \ names.head)
    if (names.length == 1)
      for (re <- elem \ names.head)
        seq = seq ++ re.asInstanceOf[Elem].copy(child = re.child ++ child)
    else
      for (subElem <- elem \ names.head)
        seq = seq ++ appendChild(subElem, child, names.tail)
    elem.asInstanceOf[Elem].copy(child = seq)
  }

该方法以递归方式将子节点附加到节点。在'if'语句中,它只是调用Elem类的'copy'方法来产生受影响儿童的新实例(可能是复数)。然后在'else'语句中递归调用'appendChild'方法验证生成的XML将被重建。在'if-else'之前,有一些序列是由不受影响的孩子建立的。 最后,我们需要将此序列复制到origin元素。

val baz = <a><z x="1"/><b><z x="2"/><c><z x="3"/></c><z x="4"/></b></a>
println("Before: \n" + XmlPrettyPrinter.format(baz.toString()))

val res = appendChild(baz, <y x="5"/>, "b/c/z")
println("After: \n" + XmlPrettyPrinter.format(res.toString()))

结果:

Before: 
<a>
  <z x="1"/>
  <b>
    <z x="2"/>
    <c>
      <z x="3"/>
    </c>
    <z x="4"/>
  </b>
</a>

After: 
<a>
  <z x="1"/>
  <b>
    <z x="2"/>
    <z x="4"/>
    <c>
      <z x="3">
        <y x="5"/>
      </z>
    </c>
  </b>
</a>