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


Scalaコップ本の26章にはいっていきますよー、26章はXML操作にかんするものですね(´・ω・`)。ScalaではXMLリテラルとして扱えたりするので、Web関係を主に扱う身としてはしっかりと読んでいきたいところですねー

準構造化データ

XML木構造という形式で構築されているけれども、型システム(XMLスキーマはあるけどもここでは扱いまセン(´・ω・`))もなくタグや属性は自由に設定できることから準構造化データと呼バレるそうですね。完全に構造化されたデータに比べて扱いが楽なために、プログラムデータを保存したりネットワーク越しに送信したりと大活躍なXMLですが、当然ScalaでもXMLをサポートしてますYO!というのがコップ本の弁です。

それでは具体的な内容を読んでいきましょうかねー

XMLの概要

とりあえずXMLに対するScalaのサポートを見ていく前にXMLについて軽く復習をば。XMLは次のように開始タグと終了タグによって構成されますね

<hoge>Hogeデータ</hoge>

// タグ内にデータを持たない空要素も指定できます
<hoge></hoge>

また各タグには属性も付与できますね

<hoge maxlength="12" display="block">Hoge</hoge>

また上記タグの入れ子によって構成されますな(´・ω・`)

<hoge>Hogeですけども<moge>MogeMoge</moge>Hogeでした</hoge>

とりあえずこんな感じのXML(の基本だけども)をベースにScalaXMLサポートを見ていきますよ

XMLリテラル

ScalaではXMLリテラルとして扱えるみたいです。これは例えば他言語でXMLをま使う際には文字列として(" とかで括って)扱っていたのがXMLをそのまま扱うことが出来るという素敵状態なわけです。とりあえずサンプルを見てみますかね

// 直接XMLを記述できます
scala> <hoge>
     | This is XML
     | hoge Tag
     | </hoge>
res0: scala.xml.Elem = 
<hoge>
       This is XML
       hoge Tag
       </hoge>

//// またXML構造もきっちりチェックしてくれますデス
scala> <hoge>
     | hugahuga
     | </moge>
<console>:3: error: in XML literal: expected closing tag of hoge
       </moge>
            ^
<console>:1: error: start tag was here: hoge>
       <hoge>
        ^

上記の結果のとおりXMLXMLクラスのscala.xml.Elem型になるみたいです。このXMLクラスについては次のような定義になっているみたいですね

  • Nodeクラスは全てのXMLノードクラスのスーパークラス
  • Textクラスはテキストだけを保持するノード
    • 例えばstuffのstuffはTextクラス
  • NodeSeqクラスはノードのシーケンスを保持
    • NodeはNodeSeqを拡張したもの(1個要素のNodeSeq)
    • XMLライブラリの多くのメソッドは個々のNodeを処理すれば良いと思われる部分でNodeSeqを処理

またXMLリテラル内でScalaコードを評価したい場合は{}で括ればいいみたいデス(´・ω・`)

scala> <scala>{"Hello" + "World"}</scala>
res3: scala.xml.Elem = <scala>HelloWorld</scala>

// Scalaコードの中にさらにXMLリテラルを追加したりも出来るです
scala> <test>{ if(0 < moge) <moge>over 1</moge> else xml.NodeSeq.Empty}</test>
res4: scala.xml.Elem = <test><moge>over 1</moge></test>

ちなみにxml.NodeSeq.Emptyは無ノードのことみたいです(´・ω・`)またScalaコードの実行結果としてノードの中にXML以外の要素が入った場合は文字列に変換されてテキストノードとして扱われるみたいです

scala> <test>{2 + 3}</test>
// 数値に見えるけどもこの5は文字列です
res5: scala.xml.Elem = <test>5</test>

それとテキスト中の<, >, &等の文字はエスケープされるみたいです

scala> <test>{"</>&"}</test>
res7: scala.xml.Elem = <test>&lt;/&gt;&amp;</test>

ちなみに中カッコ{ }をXMLテキストとして利用する場合は2個の中カッコに挟めばいいみたいです

scala> <test>{{ escape }}</test>
// エスケープ文字のエスケープみたいなイメージですかね(´・ω・`)
res16: scala.xml.Elem = <test>{ escape }</test>

うん、Webやってる身からするとHTMLをとても便利に扱えるから嬉しいですな(`・ω・´)

リアライゼーション

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>
}

アメリカだとコカコーラ時計の収集って結構メジャーな趣味なのかしら…とりあえず実行してみますかね(´・ω・`)

scala> 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
     | }
therm: CCTherm = hot dog #5

scala> therm.toXML
res8: 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> 

上記実行結果では抽象クラスをnew しているけれども、生成されるインスタンスは無名サブクラスのインスタンスになりますね(´・ω・`)

XMLの分解

XMLクラスのメソッドを幾つか見ていきますかね。ScalaXMLクラスのメソッドはXPath言語をベースにしているみたいです。

テキストの抽出

textメソッドを利用するとノードに含まれるテキスト要素を抽出出来るみたいです

scala> <test>Text</test>.text
res17: String = Text

// エスケープされた文字列もデコードされますね(´・ω・`)
scala> <test>Text&amp;</test>.text
res18: String = Text&
サブ要素の抽出

\ メソッドを利用するとノードのサブ要素を取得できるみたいです。また\\メソッドを使うとディープサーチができるみたいですね。なお、引数としてタグ名を渡す必要がありますです。

// サブ要素の探索をします
scala> <a><b><c>Hoge</c></b></a> \ "b" 
res25: scala.xml.NodeSeq = <b><c>Hoge</c></b>

// 対象はサブ要素なのでサブサブ要素はヒットしません
scala> <a><b><c>Hoge</c></b></a> \ "c" 
res26: scala.xml.NodeSeq = 

// サブサブ要素の場合はディープサーチします
scala> <a><b><c>Hoge</c></b></a> \\ "c"
res27: scala.xml.NodeSeq = <c>Hoge</c>

// ディープサーチなのでサブ要素も引っかかります
scala> <a><b><c>Hoge</c></b></a> \\ "b"
res28: scala.xml.NodeSeq = <b><c>Hoge</c></b>

// トップ要素もヒットしますね
scala> <a><b><c>Hoge</c></b></a> \\ "a"
res29: scala.xml.NodeSeq = <a><b><c>Hoge</c></b></a>
属性の抽出

\や\\メソッドを使うとタグ属性の抽出も出来るみたいです。各タグに対して@属性名での呼び出しをする使い方ですね

// XML定義です
scala> val test = <test a="a" b="b" c="c">Test</test>
test: scala.xml.Elem = <test a="a" c="c" b="b">Test</test>

// ノードの属性を取得します
scala> test \ "@a"                                   
res34: scala.xml.NodeSeq = a

// 該当ノードの属性のみみたいです
scala> test \ "@b"
res35: scala.xml.NodeSeq = b

scala> val test1 = <test a="a" b="b" c="c"><hoge e="e" f="f">Test</hoge></test>
test1: scala.xml.Elem = <test a="a" c="c" b="b"><hoge f="f" e="e">Test</hoge></test>

// サブノードの属性は取れないです(´・ω・`)
scala> test1 \ "@e"
res36: scala.xml.NodeSeq = 

// ディープサーチを使えばサブノードの属性も取得できますね
scala> test1 \\ "@e"
res37: scala.xml.NodeSeq = e

// 当然該当ノードの属性も取れますです
scala> test1 \\ "@c"
res38: scala.xml.NodeSeq = c

デシリアライゼーション

上記XML分解メソッドを組み合わせることで、XMLノードをデータ構造に変換するデシリアライザーを作成できますね。XMLを分解してさっきやったCCThremオブジェクトを返すメソッドをサンプルとしては書いてみますね(´・ω・`)

def fromXML(node:scala.xml.Node):CCTherm = 
  new CCTherm {
    val description = (node \ "description").text
    val yearMade = (node \ "yearMade").text.toInt
    val dateObtained = (node \ "dateObtained").text
    val bookPrice = (node \ "bookPrice").text.toInt
    val purchasePrice = (node \ "purchasePrice").text.toInt
    val condition = (node \ "condition").text.toInt
  }

んじゃ先程生成したCCThremオブジェクトのインスタンスthermインスタンスを利用して実行してみますよ(`・ω・´)

// 先程生成したCCThremオブジェクトのXMLです
scala> therm.toXML                                                
res42: 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>

// XMLからオブジェクトに逆変換しますよ
scala> fromXML(therm.toXML)
res43: CCTherm = hot dog #5

おお、出来ました(`・ω・´)

いじょー

時間切れのため以上です。次回はXMLのロードと保存からやりますよ