Home About Contact
XML , Kotlin Script , Kotlin

XmlPullParser を使ってXMLをパースする

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 へ移植してみます。