ScalaでXMLを加工する
Scala で XML を扱う機会ってまだまだあるのかなと思ってます。
例えば、既存システムの WebAPI が XML 形式しか対応してなかった場合とか。そうしたときに、Scala の XML リテラルで直接 XML を書くとか、XML を探索して特定要素の値を抽出するとかだったら情報が比較的手に入れやすいと思います。コップ本にも書いてありますしね。
ところが、既にある XML に対して何らかの加工をしたい場合って情報が少ないんじゃないでしょうか。ということで、今回は Scala で XML を加工する方法について書きたいと思います。(ちなみに私は去年の開発でこの辺りけっこう悩んだ記憶があります。そのときは ここ に実装のヒントが書かれていることを知らなかったので...)
実行環境
実行した環境は以下になります。
準備
build.sbt
の libraryDependencies
に scala-xml
を追加します。
libraryDependencies ++= Seq( "org.scala-lang.modules" %% "scala-xml" % "1.1.0" )
実装概要
今回紹介するのは下記の実装についてです。
- 要素に属性を追加する
- 要素の内容を置換する
- 子要素を置換する
- 子要素を追加する
- 指定した要素を削除する
- 指定した属性の値が条件に合致する場合に要素を削除する
- 子要素が空要素のみの親要素を削除する
- XMLをトリムする
実装方法
要素に属性を追加する
特定の要素に属性を追加する方法です。名前空間なしの場合は UnprefixedAttribute
、名前空間ありの場合は PrefixedAttribute
を使用します。
import scala.xml._ val xml = <root id="hoge"> <a>aaa</a> <b>bbb</b> </root> // 名前空間なし属性の追加 val result1 = xml % new UnprefixedAttribute("key", "value", xml.attributes) println("名前空間なし:") println(result1) println // 名前空間あり属性の追加 val result2 = xml % new PrefixedAttribute("x", "key", "value", xml.attributes) println("名前空間あり:") println(result2)
実行結果
名前空間なし: <root id="hoge" key="value"> <a>aaa</a> <b>bbb</b> </root> 名前空間あり: <root id="hoge" x:key="value"> <a>aaa</a> <b>bbb</b> </root>
要素の内容を置換する
要素の内容の置換には、scala.xml.transform.RuleTransformer
を使用します。
RuleTransformer
のコンストラクタに変換ルールを定義した scala.xml.RewriteRule
を設定するのですが、具体的には transform
メソッドの引数である Node
のパターンマッチで、Elem
クラスで label
が指定した要素の名前だったら、copy
メソッドで置換したい文字列を設定した新しい Elem
を返すようにします。こうすることで、要素の内容が置換されます。
なお、scala.xml.RewriteRule
は複数設定可能なので、変換ルールが分割されている場合にも対応できるようです。
import scala.xml._ import scala.xml.transform._ val xml = <root> <a>aaa</a> <b>bbb</b> </root> val rule = new RuleTransformer(new RewriteRule { override def transform(n: Node): Seq[Node] = n match { case elem: Elem if elem.label == "b" => elem.copy( child = elem.child collect { case Text(_) => Text("ccc") } ) case _ => n } }) val result = rule.transform(xml) println(result)
実行結果
<root> <a>aaa</a> <b>ccc</b> </root>
子要素を置換する
要素の全ての子要素を置換したい場合は、copy
メソッドで指定します。
val xml = <root> <a>aaa</a> <b>bbb</b> </root> val result = xml.copy(child = <c>ccc</c>) println(result)
実行結果
<root><c>ccc</c></root>
子要素を追加する
要素の指定した子要素を追加する場合は、既存の child
に List
と同じ要領で新しい要素を追加します。
val xml = <root> <a>aaa</a> <b>bbb</b> </root> val result = xml.copy(child = <c>ccc</c> ++ xml.child) println(result)
実行結果
<root><c>ccc</c> <a>aaa</a> <b>bbb</b> </root>
指定した要素を削除する
指定した要素を削除する場合はここでも scala.xml.transform.RuleTransformer
を使用します。要素名が合致していたら NodeSeq.Empty
に置き換えています。
import scala.xml._ import scala.xml.transform._ val xml = <root> <a>aaa</a> <b>bbb</b> <b>ccc</b> </root> val rule = new RuleTransformer(new RewriteRule { override def transform(n: Node): NodeSeq = n match { case e: Elem if (e \\ "b") != NodeSeq.Empty => NodeSeq.Empty case _ => n } }) val result = rule.transform(xml) println(result)
実行結果
<root> <a>aaa</a> </root>
NodeSeq.Empty
に置き換えただけなので中身は歯抜けのようになります。この場合は後述しますがトリムをかけると良いです。
指定した属性の値が条件に合致する場合に要素を削除する
以下は、ある要素の配下で属性が flag="true"
の要素を全て削除する例です。
import scala.xml._ import scala.xml.transform._ val xml = <root> <a>aaa</a> <b flag="true">bbb</b> <c flag="false">ccc</c> <d flag="true">ddd</d> </root> val rule = new RuleTransformer(new RewriteRule { override def transform(n: Node): NodeSeq = n match { case e: Elem if (e \ "@flag").text == "true" => NodeSeq.Empty case _ => n } }) val result = rule.transform(xml) println(result)
実行結果
<root> <a>aaa</a> <c flag="false">ccc</c> </root>
子要素が空要素のみの親要素を削除する
以下は、子要素が空要素、または存在しない場合は、親要素もろとも削除する例です。
import scala.xml._ import scala.xml.transform._ val xml = <root> <a> <aa /> </a> <b> <ba>ba</ba> <bb></bb> </b> <c /> </root> val rule = new RuleTransformer(new RewriteRule { override def transform(n: Node): NodeSeq = n match { case elem: Elem if elem.child.count(_.toString.trim != "") == 0 => NodeSeq.Empty case _ => n } }) val result = rule.transform(xml) println(result)
実行結果
<root> <b> <ba>ba</ba> </b> </root>
XMLをトリムする
先ほどまでの実行結果で、XML が歯抜けの場合はトリムをかけることできれいに整形されます。
val xml = <root> <a>aaa</a> <b>bbb</b> </root> val trimXml = scala.xml.Utility.trim(xml) println(trimXml)
実行結果
<root><a>aaa</a><b>bbb</b></root>