seratch's weblog in Japanese

About Scala, Java and Ruby programming in Japaense. If you need English information, go to http://blog.seratch.net/

Scala の XML リテラル入門

Scalaのバージョン

この記事が対象とする Scala のバージョンは「2.9.1.final」です。

リテラルとして書ける

Scala では XML や HTML を直接コードの中にリテラルとして書く事ができ、そのままオブジェクトとして解釈されます。

XML を読む

val xml = 
  <root>
    <items>
      <item isTarget="false" >
        <name>foo</name>
        <age>19</age>
      </item>
      <item isTarget="true" >
        <name>bar</name>
        <age>24</age>
      </item>
    </items>
  </root>

XML オブジェクトにはエレメントを探索できる「\」「\\」といったメソッドがあります。

「\」は直下のエレメントの指定、「\\」は間のエレメントを飛ばして探索ができるメソッドです。

なお、はてなの仕様でバックスラッシュはコード例の中では円マークになっています。

指定するとき、ルートエレメントは省略します。

// val items = xml \ "root" \ "items" \ "item" // 見つからない(空のNodeSeq)
val items = xml \ "items" \ "item"

「\\」の場合は欲しいエレメントのタグ名を指定するだけです。

val items = xml \\ "item"

items は scala.xml.NodeSeq 型です。コレクション型として処理ができます。

items foreach { 
  item => {
    val name = item \ "name"
    val age = item \ "age"
    val isTarget = item \ "@isTarget"
    println("name:" + name.text + ",age:" + age.text + ",isTarget:" + isTarget)
  }
}

XML を書く

{}で囲んだ部分には Scala のコードを埋め込む事ができます。

case class Item(val name: String, val age: Int, val isTarget: Boolean)
val item1 = Item("foo", 19, false)
val item2 = Item("bar", 24, true)
val xml =
  <root>
    <items>
      <item isTarget={item1.isTarget.toString} >
        <name>{item1.name}</name>
        <age>{item1.age}</age>
      </item>
      <item isTarget={item2.isTarget.toString} >
        <name>{item2.name}</name>
        <age>{item2.age}</age>
      </item>
    </items>
  </root>

以下のように for 式などの処理を埋め込む事もできます。REPL で動作するためには以下のように {} 部分は一行で書く必要があります。

case class Item(val name: String, val age: Int, val isTarget: Boolean)
val items = List(Item("foo", 19, false), Item("bar", 24, true))
val xml =
  <root>
    <items>
    { items map { i => <item isTarget={i.isTarget.toString} ><name>{i.name}</name><age>{i.age}</age></item> } }
    </items>
  </root>
val xml =
  <root>
    <items>
    { for( i <- items) yield <item isTarget={i.isTarget.toString} ><name>{i.name}</name><age>{i.age}</age></item> }
    </items>
  </root>

シリアライズ/デシリアライズ

abstract class Item {
  val name: String
  val age: Int
  val isTarget: Boolean
  def toXML = <item isTarget={isTarget.toString} ><name>{name}</name><age>{age}</age></item>
}

// シリアライズ
val item = new Item { 
  val name = "foo" 
  val age = 19 
  val isTarget = false 
}
val xml = item.toXML // <item isTarget="false"><name>foo</name><age>19</age></item>

object Item {
  def fromXML(node: scala.xml.Node): Item = new Item {
    val name = (node \ "name").text
    val age = (node \ "age").text.toInt
    val isTarget = (node \ "@isTarget").text.toBoolean
  }
}

// デシリアライズ
val item = Item.fromXML(<item isTarget="false"><name>foo</name><age>19</age></item>)

ファイル出力する

import scala.xml.XML
val xml = <root><items><item isTarget="false" ><name>foo</name><age>19</age></item><item isTarget="true" ><name>bar</name><age>24</age></item></items></root>
XML.saveFull("items.xml", xml, "UTF-8", true, null)
// <?xml version='1.0' encoding='UTF-8'?>
// <root><items><item isTarget="false"><name>foo</name><age>19</age></item><item isTarget="true"><name>bar</name><age>24</age></item></items></root>

ロードする

文字列をパースして XML オブジェクトをロードします。

import scala.xml.XML
XML.loadString("""<root><items><item isTarget="false" ><name>foo</name><age>19</age></item><item isTarget="true" ><name>bar</name><age>24</age></item></items></root>""")

ファイルから XML オブジェクトをロードします。

import scala.xml.XML
XML.loadFile("items.xml")

パターンマッチ

val item = <item isTarget="false" ><name>foo</name><age>19</age></item>
item match {
  case <item><name>{name}</name><age>{age}</age></item> => println(name) // foo
  case _ =>
}

任意の文字列として一部を扱ってより簡潔に書く事ができます。「_*」は任意の文字列を示し、以下の例ではマッチした部分を"unused"に代入しています。

item match {
  case <item><name>{name}</name>{unused @ _*}</item> => println(name) // foo
  case _ =>
}

もちろん使わない部分をまとめるためだけではなく、以下のように「_*」を使って抽出処理を書く事ができます。

item match {
  case <item>{elements @ _*}</item> => {
    elements foreach {
      el => println(el.text) // "foo" "19"
    }
  }
  case _ =>
}
val items = <items>
  <item isTarget="false" ><name>foo</name><age>19</age></item>
  <item isTarget="true" ><name>bar</name><age>24</age></item>
</items>
for ( <item>{elements @ _*}</item> <- items \ "item" ) {
  elements foreach {
    el => println(el.text) // "foo" "19" "bar" "24"
  }
}
// for ( elements @ <item>{_*}</item> <- items \ "item" ) {
// と書いた場合はelementsとして<item isTarget="false" ><name>foo</name><age>19</age></item>がとれます