scalaでxml nodeからサブノードを除外してテキストを取得する

scalaではscala-xmlパッケージでパーシングやノード作成などがサポートされているため、基本的にはあまり不便がありません。
ですが、textメソッドだとタグを除いた全てのテキストが返ってきてしまうため、サブノードを除いてノードが持つテキストだけを取得することが出来ません。
textメソッドを使わずにサブノードのテキストを含まない文字列を取得する方法について、備忘録として記録しておきます。

以下の例ではscala.xmlをインポートした前提で行っています。

```scala
import scala.xml._
```

サンプルとして下記のようなノードを用います。

```xml:samlple.xml
<top>
  top node
  <middle>
  middle node
    <bottom>
    bottom node
    </bottom>
  </middle>
</top>
```

# textで取得

サンプルノードをXML.loadFileで読み込んでtextメソッドを使ってみます。

```scala
val nodeWithChild = XML.loadFile("samlple.xml")
println(nodeWithChild.text)
```

出力結果：

```

  top node

    middle node

        bottom bode



```

タグが空白で置き換わり、`top`以下の全てのサブノードに含まれるテキストが得られました。
このようにtextメソッドだと全てのテキストが返ってきてしまうため、`top`ノードのテキスト（`top node`）だけ取得したい場合や、パーシングしながら各サブノードのテキストを順に取得したい場合などにちょっと不便です。

# サブノードのテキストを除外
ここで紹介する方法に関しては、[このページ](http://stackoverflow.com/questions/19875569/in-scala-how-do-i-get-the-text-of-a-xml-node-without-the-text-of-its-subnodes)を参考にしました。
まず、サブノードに含まれるテキストを除くために、childメソッドとcollectメソッド、それとText抽出子を用いてTextクラスだけで構成される配列を取得します。

```scala
val nodeWithChild = XML.loadFile("samlple.xml")
val childArray = nodeWithChild.child.collect{case Text(t) => t}
```

結果：

```scala
childArray: Seq[String] =
ArrayBuffer("
  top node
  ", "
")
```

得られた配列に対してmkStringメソッドを使えば文字列を得られるのですが、タグの部分が空白に置き換わっているために余白が残ってしまいます。
そこで、trimメソッドを使って余白を除いてから文字列にする処理を加えてみます。

```scala
childArray.map(_.trim).filterNot(_.isEmpty).mkString
```

結果：

```
res1: String = top node
```

`top`ノードのテキストだけを余白を除いた形で得られました。
以上の処理をまとめて、getText関数を作成します。

```scala:getText.scala
def getText(x: Node): String = {
  (x.child.collect{case Text(t) => t}).map(_.trim).filterNot(_.isEmpty).mkString
}
```

nodeWithChildにgetText関数を使用して、`top`ノードのテキストだけ得られるか確認してみます。

```scala
println(getText(nodeWithChild))
```

結果：

```
top node
```

`top`ノードのテキストだけ得られるようになりました。
つぎに、`middle`ノードに対して使用してみます。`middle`ノードの取得にはchildメソッドを使います。

```scala
println(getText(nodeWithChild.child(1)))
```

結果:

```
middle node
```

サブノードに対しても目的のテキストを得られています。

# 改行文字がある場合
改行文字（\n）が含まれたノードに対してchildメソッドを適用すると、空の要素がTextオブジェクトとして、テキスト要素がAtomオブジェクトとして返ってきてしまうみたいです。
例えば、下記のようにscalaのXMLリテラルで作成した改行文字を含んだノードに対して、childメソッドを使用してみます。

```scala:generateNode.scala
val nodeWithChildAndNewline = 
<top>
  {"top node\n"}
  <middle>
    {"middle node\n"}
    <bottom>
      {"bottom node\n"}
    </bottom>
  </middle>
</top>
nodeWithChildAndNewline.child
```

結果：

```
ArrayBuffer(
, top node
,
, <middle>
middle node

<bottom>
bottom node

</bottom>
</middle>,
)
```

１番目の要素として空の要素が入っています。
この要素のクラスをgetClassメソッドで確認すると、下記のようにTextとなっています。

```scala
nodeWithChildAndNewline.child(0).getClass
res1: Class[_ <: scala.xml.Node] = class scala.xml.Text
```

２番目の要素もTextクラスのように見えるのですが、これはAtomとなっています。

```scala
nodeWithChildAndNewline.child(1).getClass
res2: Class[_ <: scala.xml.Node] = class scala.xml.Atom
```

先ほど作ったgetText関数ではText抽出子を用いているため、このノードに対して使用すると何も返ってきません。

```scala
getText(nodeWithChildAndNewline)
res3: String = ""
```

そこでやや冗長なやり方ですが、toStringとsplit関数を用いて改行を除いた文字列を取得して、それをloadStringメソッドで新しいノードとして作りなおすtrimNode関数を用意します。[^1]


```scala:getTextWithTrim.scala
def trimNode(x: Node): Node = {
  XML.loadString(x.toString.split("\n").mkString)
}
```

trimNode関数によるトリミング結果をgetText関数で使います。

```scala
def getText(x: Node): String = {
  (trimNode(x).child.collect{case Text(t) => t}).map(_.trim).filterNot(_.isEmpty).mkString
}
```

さきほどのnodeWithChildAndNewlineに使用してみます。

```scala
println(getText(nodeWithChildAndNewline))
res4: String = top node
```

改行文字による影響を受けずに、目的のテキストが得られました。

[^1]: xml.Utility.escapeで特殊文字をエスケープする方がシンプルでした。(2016/05/19追記)

# 参考
1. https://github.com/scala/scala-xml (scala.xmlのソースコードを確認するときに参考にしました。）
1. http://www.scala-lang.org/api/2.11.1/scala-xml/#scala.xml.package （パッケージの中身一覧を調べるときに参考にしました。）