ScalaでXMLを加工する

ScalaXML を扱う機会ってまだまだあるのかなと思ってます。

例えば、既存システムの WebAPI が XML 形式しか対応してなかった場合とか。そうしたときに、ScalaXML リテラルで直接 XML を書くとか、XML を探索して特定要素の値を抽出するとかだったら情報が比較的手に入れやすいと思います。コップ本にも書いてありますしね。

ところが、既にある XML に対して何らかの加工をしたい場合って情報が少ないんじゃないでしょうか。ということで、今回は ScalaXML を加工する方法について書きたいと思います。(ちなみに私は去年の開発でこの辺りけっこう悩んだ記憶があります。そのときは ここ に実装のヒントが書かれていることを知らなかったので...)

実行環境

実行した環境は以下になります。

準備

build.sbtlibraryDependenciesscala-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>

子要素を追加する

要素の指定した子要素を追加する場合は、既存の childList と同じ要領で新しい要素を追加します。

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>

参考資料