Android の場合、標準で XmlPullParser が使えるようになっている。 これを Kotlin Script で使用したい。
XmlPullParser の実装があったので、とりあえずこれを使ってみる。 https://mvnrepository.com/artifact/net.sf.kxml/kxml2/2.3.0
環境:
$ kotlin -version
Kotlin version 1.9.22-release-704 (JRE 17.0.10+7-Ubuntu-122.04.1)
このページ https://developer.android.com/reference/org/xmlpull/v1/XmlPullParserにあるコードをおおざっぱに Kotlin に変換したコード。
ハードコーディングした次の XML テキストをパースしてみる。
val text = "<html><body><p>Hello, World!</p></body></html>"
xml.main.kts:
@file:Repository("https://repo1.maven.org/maven2/")
@file:DependsOn("net.sf.kxml:kxml2:2.3.0")
import org.xmlpull.v1.XmlPullParserFactory
import org.xmlpull.v1.XmlPullParser
import java.io.StringReader
val text = "<html><body><p>Hello, World!</p></body></html>"
StringReader(text).use { reader->
val factory = XmlPullParserFactory.newInstance()
factory.isNamespaceAware = true
factory.isValidating = false
val xpp = factory.newPullParser()
xpp.setInput(reader)
var eventType: Int = xpp.getEventType()
while( eventType != XmlPullParser.END_DOCUMENT){
when(eventType){
XmlPullParser.START_DOCUMENT-> println("Start Document")
XmlPullParser.START_TAG-> println("Start Tag: ${xpp.name}")
XmlPullParser.END_TAG-> println("End Tag")
XmlPullParser.TEXT-> println("Text: ${xpp.text}")
else -> {
println("Unknown EventType: ${eventType}")
}
}
eventType = xpp.next()
}
}
実行する。
$ kotlin xml.main.kts
Start Document
Start Tag: html
Start Tag: body
Start Tag: p
Text: Hello, World!
End Tag
End Tag
End Tag
xpp (XmlPullParser のインスタンス) を xpp.next() することで XML文書を先頭から順番に送りながら調べていく感じのコードです。
XmlPullParser のインスタンスを生成するコードをリファクタリング。 apply を使っただけ。まあ大差ないが多少は読みやすくなった。
/*
val factory = XmlPullParserFactory.newInstance()
factory.isNamespaceAware = true
factory.isValidating = false
val xpp = factory.newPullParser()
xpp.setInput(reader)
*/
val factory = XmlPullParserFactory.newInstance().apply {
isNamespaceAware = true
isValidating = false
}
val xpp = factory.newPullParser().apply {
setInput(reader)
}
XMLをパースする部分を parseXml という再帰関数にする。
tailrec fun parseXml(
xpp: XmlPullParser,
eventType: Int,
acc: List<String>): List<String> {
return if( eventType == XmlPullParser.END_DOCUMENT){
acc
} else {
val item: String = when(eventType){
XmlPullParser.START_DOCUMENT-> "Start Document"
XmlPullParser.START_TAG-> "Start Tag: ${xpp.name}"
XmlPullParser.END_TAG-> "End Tag"
XmlPullParser.TEXT-> "Text: ${xpp.text}"
else -> {
"Unknown EventType: ${eventType}"
}
}
val newEventType = xpp.next()
val newAcc = acc + listOf(item)
parseXml(xpp, newEventType, newAcc)
}
}
いままではパース結果をそのまま標準出力していましたが、 parseXml では acc に蓄積しています。
parseXml を使うコード:
val eventType: Int = xpp.getEventType()
val resultItems = parseXml(xpp, eventType, listOf<String>())
println(resultItems.joinToString("\n"))
これらの変更を反映するとリファクタリング後のコードは次のようになりました。
@file:Repository("https://repo1.maven.org/maven2/")
@file:DependsOn("net.sf.kxml:kxml2:2.3.0")
import org.xmlpull.v1.XmlPullParserFactory
import org.xmlpull.v1.XmlPullParser
import java.io.StringReader
tailrec fun parseXml(
xpp: XmlPullParser,
eventType: Int,
acc: List<String>): List<String> {
return if( eventType == XmlPullParser.END_DOCUMENT){
acc
} else {
val item: String = when(eventType){
XmlPullParser.START_DOCUMENT-> "Start Document"
XmlPullParser.START_TAG-> "Start Tag: ${xpp.name}"
XmlPullParser.END_TAG-> "End Tag"
XmlPullParser.TEXT-> "Text: ${xpp.text}"
else -> {
"Unknown EventType: ${eventType}"
}
}
val newEventType = xpp.next()
val newAcc = acc + listOf(item)
parseXml(xpp, newEventType, newAcc)
}
}
val text = "<html><body><p>Hello, World!</p></body></html>"
StringReader(text).use { reader->
val factory = XmlPullParserFactory.newInstance().apply {
isNamespaceAware = true
isValidating = false
}
val xpp = factory.newPullParser().apply {
setInput(reader)
}
val eventType: Int = xpp.getEventType()
val resultItems = parseXml(xpp, eventType, listOf<String>())
println(resultItems.joinToString("\n"))
}
実行して、先ほどと同じ結果になることを確認しましょう。
たとえば p 要素に id 属性があったら:
<html><body><p id="001">Hello, World!</p></body></html>
属性をパースするには XmlPullParser.START_TAG がきたときに処理します、このように。
XmlPullParser.START_TAG-> {
val attributes = 0.until(xpp.attributeCount).map { index->
val name: String = xpp.getAttributeName(index)
val value: String = xpp.getAttributeValue(index)
"$name=$value"
}.joinToString(",")
"Start Tag: ${xpp.name} ${attributes}"
}
それだけです。
実行して id=001 が取得できることを確認しましょう。
$ kotlin xml.main.kts
Start Document
Start Tag: html
Start Tag: body
Start Tag: p id=001
Text: Hello, World!
End Tag
End Tag
End Tag
うまくいきました。
最終的に完成したコード:
@file:Repository("https://repo1.maven.org/maven2/")
@file:DependsOn("net.sf.kxml:kxml2:2.3.0")
import org.xmlpull.v1.XmlPullParserFactory
import org.xmlpull.v1.XmlPullParser
import java.io.StringReader
tailrec fun parseXml(
xpp: XmlPullParser,
eventType: Int,
acc: List<String>): List<String> {
return if( eventType == XmlPullParser.END_DOCUMENT){
acc
} else {
val item: String = when(eventType){
XmlPullParser.START_DOCUMENT-> "Start Document"
XmlPullParser.START_TAG-> {
val attributes = 0.until(xpp.attributeCount).map { index->
val name: String = xpp.getAttributeName(index)
val value: String = xpp.getAttributeValue(index)
"$name=$value"
}.joinToString(",")
"Start Tag: ${xpp.name} ${attributes}"
}
XmlPullParser.END_TAG-> "End Tag"
XmlPullParser.TEXT-> "Text: ${xpp.text}"
else -> {
"Unknown EventType: ${eventType}"
}
}
val newEventType = xpp.next()
val newAcc = acc + listOf(item)
parseXml(xpp, newEventType, newAcc)
}
}
val text = "<html><body><p id=\"001\">Hello, World!</p></body></html>"
StringReader(text).use { reader->
val factory = XmlPullParserFactory.newInstance().apply {
isNamespaceAware = true
isValidating = false
}
val xpp = factory.newPullParser().apply {
setInput(reader)
}
val eventType: Int = xpp.getEventType()
val resultItems = parseXml(xpp, eventType, listOf<String>())
println(resultItems.joinToString("\n"))
}
次回はこのコードを Kotlin Native へ移植してみます。