1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-05-07

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

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

import scala.xml._

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

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

textで取得

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

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

出力結果:


  top node

    middle node

        bottom bode



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

サブノードのテキストを除外

ここで紹介する方法に関しては、このページを参考にしました。
まず、サブノードに含まれるテキストを除くために、childメソッドとcollectメソッド、それとText抽出子を用いてTextクラスだけで構成される配列を取得します。

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

結果:

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

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

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

結果:

res1: String = top node

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

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

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

println(getText(nodeWithChild))

結果:

top node

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

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

結果:

middle node

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

改行文字がある場合

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

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>,
)

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

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

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

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

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

getText(nodeWithChildAndNewline)
res3: String = ""

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

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

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

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

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

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

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

参考

  1. https://github.com/scala/scala-xml (scala.xmlのソースコードを確認するときに参考にしました。)
  2. http://www.scala-lang.org/api/2.11.1/scala-xml/#scala.xml.package (パッケージの中身一覧を調べるときに参考にしました。)
  1. xml.Utility.escapeで特殊文字をエスケープする方がシンプルでした。(2016/05/19追記)

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?