Groovy, Scala, Ruby, PHP による XML の加工 - 要素や属性の追加・更新・削除

今回は、各種スクリプト言語による XML の加工方法をまとめてみました。
Groovy, Scala, Ruby, PHP を使って以下のような XML

<root>
  <data id="1">sample1</data>
  <data id="2" ext="none">
    <details />
    <details />
  </data>
</root>

次のような加工を施して

  • (1) id="3" の data 要素を追加
  • (2) id="2" の data 要素に type 属性を追加
  • (3) id="1" の data 要素を削除
  • (4) 一番目の details 要素を text 要素に置換
  • (5) 二番目の details 要素の内容を変更
  • (6) id="2" の data 要素の ext 属性の内容を変更

以下のように変更してみます。

<root>
  <!-- (3) data 要素削除 -->
  <!-- (2) type 属性追加、(6) ext 属性の内容変更 -->
  <data id="2" ext="updated" type="node">
    <!-- (4) text 要素に置換 -->
    <text>update test</text>
    <!-- (5) 要素の内容を変更 -->
    <details>after</details>
  </data>
  <!-- (1) 要素の追加 -->
  <data id="3">
    <details>added</details>
  </data>
</root>

Groovy の場合

XmlSlurper を使いました。find や配列によるアクセスは parse 時の構成に基づいて実施する必要があり、変更を加えた箇所には適用できないようなので注意。

import groovy.xml.StreamingMarkupBuilder

def doc = new XmlSlurper().parse(new File(args[0]))

// (1) id="3" の data 要素を追加(details 子要素含む)
doc.appendNode {
    data(id: "3") {
        details("added")
    }
}
// (2) type 属性を追加
doc.data.find {it.@id == "2"}.@type = "node"
//以下でも可
//doc.data[1].@type = "node"
//以下は不可。appendNode した要素は find や配列でアクセスできない模様
//doc.data.find {it.@id == "3"}.@type = "node1"
//doc.data[2].@type = "node2"

// (3) id="1" の data 要素を削除
doc.data.find {it.@id == "1"}.replaceNode {}

// (4) details 要素を text 要素に置換
doc.data.find {it.@id == "2"}.children()[0].replaceNode {
    text("update test")
}
//以下でも可。id="1" を削除する前の構成でアクセスする必要あり
/*
doc.data[1].details[0].replaceNode {
    text("update test")
}
*/

// (5) details 要素の内容を変更
doc.data.find {it.@id == "2"}.details[1] = "after"

// (6) ext 属性の内容を変更
doc.data.find {it.@id == "2"}.@ext = "updated"

//文字列化して出力
println new StreamingMarkupBuilder().bind{
    mkp.yield doc
}

Scala の場合

RuleTransformer と RewriteRule を使いました。
Scala の場合、他のものに比べると処理内容がかなり変わっています。
要素や属性の値を変更するというよりは、変更点を反映した新しい要素を作っていくようなイメージです。

ただし、普段 XML の更新に Scala を使っていない事もあり、知らないだけでもっと良い方法があるかもしれません。特に details 要素の処理の部分はイマイチかもしれません。
ちなみに、スクリプトは Scala 2.8.0 用です。

import scala.xml._
import scala.xml.transform.{RewriteRule, RuleTransformer}

val rule = new RewriteRule {
    override def transform(n: Node): NodeSeq = n match {
        // (1) 要素を追加
        case <root>{ ch @ _* }</root> => <root>{ ch }<data id="3"><details>added</details></data></root>
        // (3) 要素を削除
        case e: Elem if (e \ "@id").text == "1" => NodeSeq.Empty
        case e: Elem if (e \ "@id").text == "2" =>
            // (2) 属性の追加と (6) 変更
            val ne = e % Attribute("", "type", "node", Null) % Attribute("", "ext", "updated", Null)
            //以下でも可
        //  val ne = e % new UnprefixedAttribute("type", "node", Null) % new UnprefixedAttribute("ext", "updated", Null)

            //子要素(details)を処理
            val ch: NodeSeq = ne.child.zipWithIndex.map {l =>
                l._2 match {
                    // (4) 要素を置換
                    case 0 => <text>update test</text>
                    // (5) 要素の値を変更
                    case 1 => Elem(l._1.prefix, l._1.label, l._1.attributes, l._1.scope, Text("after"))
                    case _ => l._1
                }
            }
            //子要素だけを変更した data 要素のコピーを作成
            ne.copy(child = ch)
        case n => n
    }
}
val doc = XML.loadFile(args(0))
val newDoc = new RuleTransformer(rule).transform(Utility.trim(doc))

println(newDoc.mkString)

Ruby の場合

REXML を使いました。
elements[index] を使う場合、index は 1 からになるようなので注意。

require "rexml/document"

xml = REXML::Document.new File.new(ARGV[0])
doc = xml.root

# (1) data 要素を追加
doc.add_element("data", {"id" => "3"}).add_element("details").add_text("added")

n = doc.get_elements("//data[@id='2']")[0]
# (2) type 属性を追加
n.add_attribute("type", "node")

# (3) 要素を削除
doc.elements["//data[@id='1']"].remove
#以下でも可
#doc.get_elements("//data[@id='1']")[0].remove
#doc.elements["data"].remove
#doc.delete_element("data")

# (4) 要素を置換、elements[index] を使用する場合 index は 1 からになる
n.replace_child(n.elements[1], REXML::Element.new("text").add_text("update test"))

# (5) 要素の値を変更
n.elements[2].text = "after"

# (6) ext 属性の値を変更
n.attributes["ext"] = "updated"

puts doc

PHP の場合

SimpleXML を使いました。
SimpleXML で要素を置換する方法が無さそうだったので、DOMDocument の API を使って、要素を置換しています。dom_import_simplexml や simplexml_import_dom で DOM と SimpleXML との相互変換を行っています。

<?php
$doc = simplexml_load_file($argv[1]);

// (1) 要素を追加
$d = $doc->addChild("data");
$d["id"] = "3";
$d->addChild("details", "added");

// (2) type 属性を追加
$doc->data[1]["type"] = "node";

// (3) 要素を削除
unset($doc->data[0]);

//SimpleXML を DOM に変換
$dom = dom_import_simplexml($doc);
//1番目の details 要素取得
$target = $dom->getElementsByTagName("details")->item(0);
// (4) 要素を置換(DOMDocument の API を使用)
$target->parentNode->replaceChild(new DOMElement("text", "update test"), $target);
//DOM を SimpleXML に変換
$doc = simplexml_import_dom($dom);

// (5) 要素の値を変更
$doc->data[0]->details[0] = "after";

// (6) ext 属性の値を変更
$doc->data[0]["ext"] = "updated";

echo $doc->asXML();