S-JIS[2011-02-11/2013-09-18] 変更履歴

Scala XML

ScalaでXMLを操作する例。


XMLリテラル

ScalaではXMLをプログラム中に直接書くことが出来る(XMLリテラル)。
また、波括弧でくくったブロックを作ることにより、XMLリテラルの外の変数や演算を記述することも出来る。

Scala 内容 備考
val x1 = <sample>hoge</sample>
val x2 =
<sample>
  <zzz>test</zzz>
  <foo/>
</sample>
x1: scala.xml.Elem = <sample>hoge</sample>

x2: scala.xml.Elem =
<sample>
  <zzz>test</zzz>
  <foo></foo>
</sample>
「<タグ/>」という形式で指定したタグは
「<タグ></タグ>」という形で保持されるようだ。
val x3 = <sample>
  <!-- comment -->
</sample>
x3: scala.xml.Elem =
<sample>
  <!-- comment -->
</sample>
コメントも指定可能。
val name = "world"

val x = <s>hello,{name}</s>
val n = <s>{ 123 + 456 }</s>
x: scala.xml.Elem = <s>hello,world</s>

n: scala.xml.Elem = <s>579</s>
波括弧で囲んだブロックはScalaの式として評価(演算)され、
その位置に埋め込まれる。
Stringのformat代わりに使えるかも?)
このブロック内では改行できない。
val xl = <list>
{ List(1,2,3) }
</list>
val xm = <map>
{ Map("a"->123, "b"->456) }
</map>
xl: scala.xml.Elem =
<list>
123
</list>

xm: scala.xml.Elem =
<map>
(a,123)(b,456)
</map>
ListやMapを渡すとその内容の文字列になるようだが
toString()の戻り値ともmkString()とも異なるので、
どういうルールで変換されるのか謎。
val xx = <t1>
{ <t2>zzz</t2> }
</t1>
val xf = <t1>
{ for(i <- 1 to 3) yield <t>{i}</t> }
</t1>
xx: scala.xml.Elem =
<t1>
<t2>zzz</t2>
</t1>

xf: scala.xml.Elem =
<t1>
<t>1</t><t>2</t><t>3</t>
</t1>
ブロック内でさらにXMLリテラルを指定する事も出来るようだ。
(ループ内で改行させるにはどうしたらいいんだろう?)
val e = <s>
&quot;foo&amp;bar&quot;
</s>
× <s>"foo&bar"</s>
e: scala.xml.Elem =
<s>
&quot;foo&amp;bar&quot;
</s>
大小記号や「&」はエスケープしたもの(実体参照)を指定する必要がある。
val e = <escape>
{ """<>&"'""" }
</escape>
e: scala.xml.Elem =
<escape>
&lt;&gt;&amp;&quot;'
</escape>
波括弧のブロック内の文字列に大小記号等を入れると
自動的にエスケープされる。
val c1 = <s><![CDATA[<p>]]></s>
val c2 = <s><![CDATA[{"<p>"}]]></s>
c1: scala.xml.Elem = <s>&lt;p&gt;</s>

c: scala.xml.Elem = <s>{&quot;&lt;p&gt;&quot;}</s>
CDATAセクションもちゃんと使える。
波括弧ブロックはCDATAセクションの一部とみなされ、Scalaの式は書けない。
val a1 = <tag attr="123" />
val a2 = <tag attr='456' />
a1: scala.xml.Elem = <tag attr="123"></tag>

a2: scala.xml.Elem = <tag attr="456"></tag>
属性値はダブルクォーテーションでもシングルクォーテーションでも指定可能。
(囲まないのはXMLの規約違反なのでコンパイルエラー)
val a = <tag attr={ "123" } />
× <tag attr={ 123 } />
× <tag { "attr" } = "123" />
× <tag { "attr=123" } />
a: scala.xml.Elem = <tag attr="123"></tag> 属性値にも波括弧ブロックを指定可能。
ただしその内容はStringでないと駄目なようだ。
(例えば数値を指定するとコンパイルエラーになる)
また、属性名をブロックにする事は出来ない。
(属性全体を条件によって出したり出さなかったりということは出来ない)

文字列→XML

文字列(String)からXMLに変換するにはscala.xml.XMLオブジェクトを使用する。

scala> val s = """<sample>
     |   <hoge>zzz</hoge>
     | </sample>
     | """
s: java.lang.String =
<sample>
  <hoge>zzz</hoge>
</sample>

scala> import scala.xml.XML

scala> val xml = XML.loadString(s)
xml: scala.xml.Elem =
<sample>
  <hoge>zzz</hoge>
</sample>

XML→文字列

XMLから文字列(String)を取得する方法。

例として、以下の様なXMLがあるものとする。

val xml =
<html>
<head>
  <title>タイトル</title>  <!-- 実験 -->
</head>
<body>
<p align='center'>テスト<br/>です。</p>
</body>
</html>
Scala 内容 備考
val s = xml.toString()
s: String =
<html>
<head>
  <title>タイトル</title>  <!-- 実験 -->
</head>
<body>
<p align="center">テスト<br></br>です。</p>
</body>
</html>
toString()を使うと、基本的に生成した際のXMLが取得できる。
つまりスペース等のインデントはそのまま残る。
ただし、属性値はダブルクォーテーションで囲まれるし、
「<タグ/>」は「<タグ></タグ>」という形になる。
val s = xml.text
s: String =


  タイトル


テストです。

textを使うと、タグを除いたテキスト(文字列)だけが取得できる。
val s = xml.label
s: String = html
labelで要素名を取得できる。
import scala.xml.PrettyPrinter
val pp = new PrettyPrinter(40, 2)
val s = pp.format(xml)
s: String =
<html>
  <head>
    <title>タイトル</title>
    <!-- 実験 -->
  </head>
  <body>
    <p align="center">
      テスト
      <br></br>
      です。
    </p>
  </body>
</html>
インデントを整形するにはPrettyPrinterクラスが使える。
コンストラクターの第2引数でインデントの単位桁数を指定する。
第1引数はwidthだそうだが、どういう効果があるのか不明。
import scala.xml.Xhtml
val s = Xhtml.toXhtml(xml)
s: String =
<html>
<head>
  <title>タイトル</title>  <!-- 実験 -->
</head>
<body>
<p align="center">テスト<br />です。</p>
</body>
</html>
XHTMLスタイルで整形するにはXhtmlオブジェクトが使える。
基本的にはtoString()での出力と同じようだが、
brタグは1つだけしか出力されない。

XMLファイルからの読み込み・ファイルへの保存

scala.xml.XMLオブジェクトにファイルとの読み書きを行うメソッドが用意されている。

XML.load()でInputSourceやInputStream・ReaderからXMLインスタンスを生成することが出来る。
XML.loadFile()でファイル名を指定してXMLファイルを読み込むことも出来る。
XML.loadXML()でSAXParserを指定することが出来る。

XML.save()でファイルへ保存、XML.write()でWriterへ書き出すことが出来る。


save

XMLオブジェクトをXMLファイルに保存する例。[2013-09-18]

import scala.xml.XML
import scala.xml.dtd.DocType
import scala.xml.dtd.PublicID
  val html = <html><head><title>zzz</title></head><body>zzz</body></html>
  val doctype = DocType("html", PublicID("-//W3C//DTD HTML 4.01 Transitional//EN", null), Nil)
  XML.save("test.html", html, "UTF-8", false, doctype)

loadFile

XMLオブジェクトを使ってXMLファイルを読み込む例。[2012-04-14]

import scala.xml.XML
  val f = new File("hoge.xml")
  val xml = XML.loadFile(f)

ただしこの方法だと、ルートエレメント(一番外側の要素)しか取れない。
つまり、XMLの処理命令などを取ることが出来ない。

loadFile()の中のパーサーでは当然処理命令も読み込んでいるのだが、loadFile()がそれを返すようになっていない。
XMLオブジェクトはXMLLoaderトレイトを継承しており、処理命令はその中のadapterで保持している。
したがって、自分でXMLLoaderを継承したクラスを作り、adapterから取り出せるようにすればいい。

adapterはdef(メソッド)なので、呼び出すたびに新しいインスタンスが返ってくる。したがって、一度だけインスタンスを作って、後はそれを返すようにする。
そのため、defをvalでオーバーライドする。

import scala.xml.Elem
import scala.xml.factory.XMLLoader
  val loader = new XMLLoader[Elem] {
    override lazy val adapter = super.adapter
  }
  val xml = loader.loadFile(f)
  val pis = loader.adapter.hStack.toSeq.collect{ case p: ProcInstr => p }.reverse //処理命令一覧

コメントはさらに別の手段でないと取れない。
XMLLoaderの中のparserにLexicalHandlerというハンドラーを登録する必要がある。
XMLドキュメントの中にコメントが見つかるとこのハンドラーが呼ばれるので、そのときにadapter.hStackにコメントオブジェクトを追加してやる。

import org.xml.sax.ext.DefaultHandler2 //LexicalHandlerインターフェースを実装したクラス

import scala.xml.Comment
  val loader = new XMLLoader[Elem] {
    override lazy val adapter = super.adapter

    override def parser = {
      val p = super.parser
      p.setProperty("http://xml.org/sax/properties/lexical-handler", new DefaultHandler2 {
        override def comment(ch: Array[Char], start: Int, length: Int): Unit = {
          val s = String.valueOf(ch, start, length)
          val c = Comment(s)
          adapter.hStack.push(c)
        }
      })
      p
    }
  }

※LexicalHandlerを登録する為のプロパティー名については、SAXParserImpl.JAXPSAXParser#setProperty()を参照


ConstructingParser

ConstructingParserを使ってXMLをパースする方法もある。[2012-04-14]
こちらを使うと、XML宣言に書かれているエンコーディングやDTD<!DOCTYPE〜>)、処理命令<?〜?>)やコメント (<!--〜-->)も取得することが出来る。

import scala.xml.parsing.ConstructingParser
import scala.xml.Elem
import scala.xml.Comment
import scala.xml.ProcInstr
  val f = new File("hoge.xml")
  val doc = ConstructingParser.fromFile(f, false).document()

  println(doc.encoding)	//エンコーディング
  println(doc.dtd)	//DTD

  val xml = doc.docElem	//ルート要素
  println(xml)

  val seq = doc.children	//ノード一覧。処理命令やルート要素など
  seq.foreach {
    _ match {
      case e: Elem      => println("要素(element) " + e)
      case p: ProcInstr => println("処理命令(processing instruction) " + p)
      case c: Comment   => println("コメント(comment) " + c)
      case n            => println("その他(node) " + n.getClass())
    }
  }

fromFile()の第2引数preserveWSには、空白文字(white space)を保持するかどうかを指定する。
trueにしておくと、タグとタグの間のタブや改行とかもテキストとしてノード一覧に入る。


ただし、エンコーディングは実行環境によって決まってしまう模様。(ファイル内のエンコーディングを自動認識してくれない)
決め打ちでいいなら、以下のようにして指定できる。

  val f = new File("hoge.xml")
  val s = Source.fromFile(f, "UTF-8")
  val doc = ConstructingParser.fromSource(s, false).document()

XMLの探索

XML内の要素や属性を取得する方法。

例として、以下の様なXMLがあるものとする。

val xml =
<html>
<body>
<h1>test</h1>
<ul>
  <li>hoge</li>
  <li>fuge</li>
  <li><a href="url">hage</a></li>
</ul>
</body>
</html>
Scala 内容 備考
val b = xml \ "body"
b: scala.xml.NodeSeq =
NodeSeq(<body>
<h1>test</h1>
<ul>
  <li>hoge</li>
  <li>fuge</li>
  <li><a href="url">hage</a></li>
</ul>
</body>)
\メソッドで、要素名を指定してXML要素を取得できる。

返り値はNodeSeq、つまり複数の要素が取れることがある。

1つも見つからなかった場合は空のNodeSeqが返る。
val l = xml \ "body" \ "ul" \ "li"
l: scala.xml.NodeSeq = NodeSeq(<li>hoge</li>, <li>fuge</li>, <li><a href="url">hage</a></li>)
val e = xml \ "body" \ "a"
e: scala.xml.NodeSeq = NodeSeq()
val l = xml \\ "li"
l: scala.xml.NodeSeq = NodeSeq(<li>hoge</li>, <li>fuge</li>, <li><a href="url">hage</a></li>) \\メソッドで、間の要素を飛ばしてXML要素を取得することが出来る。
for (li <- xml \\ "li") println(li)
<li>hoge</li>
<li>fuge</li>
<li><a href="url">hage</a></li>
val h = xml \\ "a" \ "@href"
h: scala.xml.NodeSeq = url 先頭に「@」を付けると、要素名でなく属性名の指定となる。
val ns = xml match {
  case <html>{ n @ _* }</html> => n
}
ns: Seq[scala.xml.Node] =
ArrayBuffer(
, <body>
<h1>test</h1>
<ul>
  <li>hoge</li>
  <li>fuge</li>
  <li><a href="url">hage</a></li>
</ul>
</body>,
)
match式によるパターンマッチを行うことも出来る。
(xml \ "body")(0) match {
  case <body>{ ns @ _* }</body> =>
    for (n <- ns) {
      n match {
        case <h1>{ h      }</h1> => println("h1")
        case <ul>{ u @ _* }</ul> => println("ul")
        case _ =>
      }
    }
}
h1
ul
for (bs <- xml \ "body") {
  for (h @ <h1>{ _ }</h1> <- bs.child) {
    println(h)
  }
}

 

<h1>test</h1> for式によるパターンマッチで
合致する要素だけ取得することも出来る。

「変数名 @ パターン」は、パターンに合致した場合に
その全体を変数に代入する。
パターン内に変数があるときは
その部分だけ変数に代入する。
for (bs <- xml \ "body") {
  for (<h1>{ h }</h1> <- bs.child) {
    println(h)
  }
}
test
for (us <- xml \\ "ul") {
  for (ul @ <ul>{ _* }</ul> <- us) {
    for (<li>{ li }</li> <- ul \ "li") {
      println(li)
    }
  }
}
hoge
fuge
<a href="url">hage</a>

Scala目次へ戻る / 技術メモへ戻る
メールの送信先:ひしだま