LL脳がscalaの勉強を始めたよ その91


Scalaコップ本の26章残りをやってしまいますよー。26章はXML操作についてですね(`・ω・´)

前回作成したXML操作用のサンプルオブジェクトはこんな感じですね

abstract class CCTherm {
  // 温度計の説明
  val description: String
  // 製作年
  val yearMade:int
  // 購入日
  val dateObtained:String
  // 流通平均価格:単位はセント
  val bookPrice:Int
  // 購入価格:単位はセント
  val purchasePrice:Int
  // 状態:1~10で表現
  val condition:Int

  override def toString = description
  
  // 各データをXMLに変換するメソッド
  def toXML = 
    <cctherm>
      <description>{description}</description>
      <yearMade>{yearMade}</yearMade>
      <dateObtained>{dateObtained}</dateObtained>
      <bookPrice>{bookPrice}</bookPrice>
      <purchasePrice>{purchasePrice}</purchasePrice>
      <condition>{condition}</condition>
    </cctherm>
}

上記オブジェクトは下記手順でXMLノードに変換しました

// インスタンスを生成
val therm = new CCTherm {
  val description = "hot dog #5"
  val yearMade = 1952
  val dateObtained = "March 14, 2006"
  val bookPrice = 2199
  val purchasePrice = 500
  val condition = 9
}

// XMLに変換
val node = therm.toXML

今回も上記サンプルをベースに進めていきますよ

ロードと保存

XMLから文字列への変換はtoStringメソッドでOKみたいですが、XMLとバイトストリームの変換には文字符号化方式を指定できるなどのメリットがあるのでラブラリールーチンを使った方がヨロシイデスとのことです。

実際にXMLをファイルに変換するにはXML.saveFullコマンドを使いますね。XML.saveFileコマンドではファイル名、保存ノード、文字符号化方式を指定してオプションとして文字符号化方式XML宣言の出力可否、XMLファイルのドキュメントタイプを指定するです。サンプルとしてはこんな感じですね。

// 文字符号化XML宣言ありでドキュメントタイプは指定なしです
scala.xml.XML.saveFull("therm1.xml", node, "UTF-8", true, null)

上記処理で生成したXMLファイルは次のようになりますね

<?xml version='1.0' encoding='UTF-8'?>
<cctherm>
             <description>hot dog #5</description>
             <yearMade>1952</yearMade>
             <dateObtained>March 14, 2006</dateObtained>
             <bookPrice>2199</bookPrice>
             <purchasePrice>500</purchasePrice>
             <condition>9</condition>
           </cctherm>

ちなみに上記をロードするときはXML.loadFileを使えば一発みたいです。XML.loadFileはこんな感じで使いますね(´・ω・`)

scala> val loadnode = scala.xml.XML.loadFile("therm1.xml")
loadnode: scala.xml.Elem = 
<cctherm>
             <description>hot dog #5</description>
             <yearMade>1952</yearMade>
             <dateObtained>March 14, 2006</dateObtained>
             <bookPrice>2199</bookPrice>
             <purchasePrice>500</purchasePrice>
             <condition>9</condition>
           </cctherm>

ScalaでのXMLの保存・ロードはかなり楽ちんなので、かなり使い勝手がいいですな(`・ω・´)

XMLを対象とするパターンマッチ

前回はtextやXPath的な\や\\でのXMLの分解についてやってみたのですが、パターンマッチを使ったXMLの分解についてやってみたいと思います。パターンマッチを使うことで複雑なXMLの制御が可能になるみたいですね。

XMLのパターンは次のようにXMLリテラルに似た形式になります。ただし{}で括られた部分がパターンとして利用できるようになりますね。サンプルとしてはこんな感じになりますです(`・ω・´)

def proc(node:scala.xml.Node):String =
  node match {
    case <a>{ contents }</a> => "これはaの" + contents
    case <b>{ contents }</b> => "これはbの"  + contents
    case _ => "なんでもないデス(`・ω・´)"
  }
  
// 実際に使ってみますよ
scala> proc(<a>apple</a>)
res4: String = これはaのapple

scala> proc(<b>apple</b>)
res5: String = これはbのapple

scala> proc(<c>apple</c>) 
res6: String = なんでもないデス(`・ω・´)

上記サンプルでは単一ノードのみのマッチングですが、一連のノードに対してマッチングさせるためには次のように任意のシーケンスを表す_*を利用するといいみたいですね。例えばこんなふうに書けるみたいです。

// サブ要素のシーケンスに対応したパターンマッチです
def proc2(node:scala.xml.Node):String =
  node match {
    // @パターンを利用して_*を使ってみますデス(´・ω・`)
    case <a>{ contents @ _* }</a> => "これはaの" + contents
    case <b>{ contents @ _* }</b> => "これはbの"  + contents
    case _ => "なんでもないデス(`・ω・´)"
  }
  
//// 実行してみますよ
// @パターンで_*の内容もcontentsに束縛しておりますね
scala> proc2(<a>a <em>red</em> apple</a>)
res10: String = これはaのArrayBuffer(a , <em>red</em>,  apple)

//シーケンス要素を追加してみます
scala> proc2(<b>a <em>blue</em> <span><em>big</em></span> apple</b>)  
res14: String = これはbのArrayBuffer(a , <em>blue</em>,  , <span><em>big</em></span>,  apple)

こんな感じで一つ一つのXMLノードをパターンマッチで処理させることが出来るわけですね(`・ω・´)

for式でXMLを処理

上でやったパターンマッチはあくまでもXMLノード単体を対象としたものですが、XML全体を処理する場合はfor式と組み合わせるのが良いみたいですね。例えば次のようなXMLを処理することを例としてみますね(´・ω・`)

val catalog =
  <catalog>
    <cctherm>
      <description>hot dog #5</description>
      <yearMade>1952</yearMade>
      <dateObtained>March 14, 2006</dateObtained>
      <bookPrice>2199</bookPrice>
      <purchasePrice>500</purchasePrice>
      <condition>9</condition>
    </cctherm>
    <cctherm>
      <description>Sprite Boy</description>
      <yearMade>1964</yearMade>
      <dateObtained>April 28, 2003</dateObtained>
      <bookPrice>1695</bookPrice>
      <purchasePrice>595</purchasePrice>
      <condition>5</condition>
    </cctherm>
  </catalog>

上記XML要素の間には2個の要素とその前後の空白要素のあわせて5つの要素から構成されているので、特に3つの空白要素を考慮しないと次のような失敗を擦る可能性があるとのことです。

// 空白処理を考慮せずにマッチングしてみます
scala> catalog match {
     |   case <catalog>{ therms @ _* }</catalog> =>
     |   for(therm <- therms)
     |     println("processing: " + (therm \ "description").text)
     | }
// 空白もノード要素として処理されてしまいました(´・ω・`)
processing: 
processing: hot dog #5
processing: 
processing: Sprite Boy
processing: 

上記はのサブノードを全て対象にしたため空白ノードが含まれてしまったので、空白要素を無視して対象となる要素のみを処理するためには次のようにfor式内部でパターンマッチする必要がありますね。

scala> catalog match {
     |   case <catalog>{ therms @ _* }</catalog> =>
     |   // for式内で<cctherm>要素かどうかを判定して反復処理します
     |   for(therm  @ <cctherm>{ _* }</cctherm> <- therms)
     |     println("processing: " + (therm \ "description").text)
     | }
processing: hot dog #5
processing: Sprite Boy
スーパー蛇足で

ちなみにXML空白要素は改行部分に発生するみたいなので、次のようにタグ近辺の改行を無くしてしまえば空白要素を考慮せずとも良いみたいですね(´・ω・`)

val catalog =
  <catalog><cctherm>
      <description>hot dog #5</description>
      <yearMade>1952</yearMade>
      <dateObtained>March 14, 2006</dateObtained>
      <bookPrice>2199</bookPrice>
      <purchasePrice>500</purchasePrice>
      <condition>9</condition>
    </cctherm><cctherm>
      <description>Sprite Boy</description>
      <yearMade>1964</yearMade>
      <dateObtained>April 28, 2003</dateObtained>
      <bookPrice>1695</bookPrice>
      <purchasePrice>595</purchasePrice>
      <condition>5</condition>
    </cctherm></catalog>

上記XMLであれば空白考慮なしバージョンを実行しても普通に処理できます

scala> catalog match {
     |   case <catalog>{ therms @ _* }</catalog> =>
     |   for(therm <- therms)
     |     println("processing: " + (therm \ "description").text)
     | }
processing: hot dog #5
processing: Sprite Boy

いじょー

とりあえずScalaではXMLを気軽に扱えまっせ(`・ω・´)という内容でした。XML処理は個人的によく使うので今後しっかりと調べていきたいところですねー

次回はモジュラープログラミングとやらに進みますね