XML データを解析する

拡張マークアップ言語(XML)は、機械で判読可能な形式でドキュメントをエンコードするためのルールのセットです。XML は、インターネット上でデータを共有するための一般的な形式です。ニュースサイトやブログなど、コンテンツが頻繁に更新されるウェブサイトは、多くの場合、外部プログラムがコンテンツ変更についていけるように XML フィードを提供しています。XML データのアップロードと解析は、ネットワークに接続されたアプリにとって、ありふれたタスクです。このレッスンでは、XML ドキュメントを解析して、そのデータを利用する方法について説明します。

Android アプリでのウェブベースのコンテンツ作成について詳しくは、ウェブアプリをご覧ください。

パーサーを選択する

パーサーとしては、Android 上でメンテナンス可能な方法で効率的に XML を解析できる XmlPullParser をおすすめします。Android のこのインターフェースには、これまで 2 つの実装がありました。

どちらを使用しても構いません。このセクションの例では、Xml.newPullParser() 経由の ExpatPullParser を使用しています。

フィードを分析する

フィードを解析するには、まず、対象とするフィールドを決定します。パーサーは対象フィールドのデータを抽出し、残りのフィールドを無視します。

サンプルアプリで解析するフィードからの抜粋を以下に示します。このフィード内では、StackOverflow.com への投稿がそれぞれ entry タグとして表示されており、このタグの内部には、ネストされたタグがいくつか含まれています。

<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:creativeCommons="http://backend.userland.com/creativeCommonsRssModule" ...">
<title type="text">newest questions tagged android - Stack Overflow</title>
...
    <entry>
    ...
    </entry>
    <entry>
        <id>http://stackoverflow.com/q/9439999</id>
        <re:rank scheme="http://stackoverflow.com">0</re:rank>
        <title type="text">Where is my data file?</title>
        <category scheme="http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest/tags" term="android"/>
        <category scheme="http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest/tags" term="file"/>
        <author>
            <name>cliff2310</name>
            <uri>http://stackoverflow.com/users/1128925</uri>
        </author>
        <link rel="alternate" href="http://stackoverflow.com/questions/9439999/where-is-my-data-file" />
        <published>2012-02-25T00:30:54Z</published>
        <updated>2012-02-25T00:30:54Z</updated>
        <summary type="html">
            <p>I have an Application that requires a data file...</p>

        </summary>
    </entry>
    <entry>
    ...
    </entry>
...
</feed>

サンプルアプリは、entry タグと 3 つのネストタグ(titlelinksummary)のデータを抽出します。

パーサーをインスタンス化する

次のステップとして、パーサーをインスタンス化して、解析プロセスを開始します。パーサーを初期化するスニペットを以下に示します。ここでは、名前空間を処理せず、提供された InputStream を入力として使用するように指定されています。パーサーは、nextTag() を呼び出すことで解析プロセスを開始した後、readFeed() メソッドを呼び出し、アプリで解析したいデータを抽出して処理します。

Kotlin

// We don't use namespaces
private val ns: String? = null

class StackOverflowXmlParser {

    @Throws(XmlPullParserException::class, IOException::class)
    fun parse(inputStream: InputStream): List<*> {
        inputStream.use { inputStream ->
            val parser: XmlPullParser = Xml.newPullParser()
            parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
            parser.setInput(inputStream, null)
            parser.nextTag()
            return readFeed(parser)
        }
    }
 ...
}

Java

public class StackOverflowXmlParser {
    // We don't use namespaces
    private static final String ns = null;

    public List parse(InputStream in) throws XmlPullParserException, IOException {
        try {
            XmlPullParser parser = Xml.newPullParser();
            parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
            parser.setInput(in, null);
            parser.nextTag();
            return readFeed(parser);
        } finally {
            in.close();
        }
    }
 ...
}

フィードを読む

実際にフィードを処理するのは readFeed() メソッドです。このメソッドは、フィードを再帰的に処理するために、まず「entry」のタグが付いた要素を探します。entry タグ以外のタグはスキップします。フィード全体を再帰的に処理したら、readFeed() は、フィードから抽出したエントリ(ネストされたデータメンバーを含む)を含む List を返します。この List は、続いてパーサーによって返されます。

Kotlin

@Throws(XmlPullParserException::class, IOException::class)
private fun readFeed(parser: XmlPullParser): List<Entry> {
    val entries = mutableListOf<Entry>()

    parser.require(XmlPullParser.START_TAG, ns, "feed")
    while (parser.next() != XmlPullParser.END_TAG) {
        if (parser.eventType != XmlPullParser.START_TAG) {
            continue
        }
        // Starts by looking for the entry tag
        if (parser.name == "entry") {
            entries.add(readEntry(parser))
        } else {
            skip(parser)
        }
    }
    return entries
}

Java

private List readFeed(XmlPullParser parser) throws XmlPullParserException, IOException {
    List entries = new ArrayList();

    parser.require(XmlPullParser.START_TAG, ns, "feed");
    while (parser.next() != XmlPullParser.END_TAG) {
        if (parser.getEventType() != XmlPullParser.START_TAG) {
            continue;
        }
        String name = parser.getName();
        // Starts by looking for the entry tag
        if (name.equals("entry")) {
            entries.add(readEntry(parser));
        } else {
            skip(parser);
        }
    }
    return entries;
}

XML を解析する

XML フィードを解析する手順は次のとおりです。

  1. フィードを分析するで説明したように、アプリ内に含めるタグを特定します。今回の例の場合、entry タグと 3 つのネストタグ(titlelinksummary)のデータを抽出します。
  2. 次のメソッドを作成します。

    • 対象とする各タグに合う「read」メソッド(readEntry()readTitle() など)。パーサーは、入力ストリームからタグを読み取り、entrytitlelinksummary という名前のタグを見つけると、そのタグに対して適切なメソッドを呼び出します。それ以外のタグはスキップします。
    • 各タグタイプのデータを抽出し、パーサーを次のタグに進めるメソッド。次に例を示します。
      • title タグと summary タグの場合、パーサーは readText() を呼び出します。このメソッドは、parser.getText() を呼び出すことで、各タグのデータを抽出します。
      • link タグの場合、パーサーは、解析対象のリンクかどうかを最初に判断したうえで、そのリンクのデータを抽出します。次に、parser.getAttributeValue() を使用して、リンクの値を抽出します。
      • entry タグの場合、パーサーは readEntry() を呼び出します。このメソッドは、エントリ内にネストされたタグを解析し、データメンバーが titlelinksummaryEntry オブジェクトを返します。
    • 再帰的な skip() ヘルパー メソッド。このトピックの詳細については、不要なタグをスキップするをご覧ください。

このスニペットは、パーサーがエントリ、タイトル、リンク、サマリーをどのように解析するかを示しています。

Kotlin

data class Entry(val title: String?, val summary: String?, val link: String?)

// Parses the contents of an entry. If it encounters a title, summary, or link tag, hands them off
// to their respective "read" methods for processing. Otherwise, skips the tag.
@Throws(XmlPullParserException::class, IOException::class)
private fun readEntry(parser: XmlPullParser): Entry {
    parser.require(XmlPullParser.START_TAG, ns, "entry")
    var title: String? = null
    var summary: String? = null
    var link: String? = null
    while (parser.next() != XmlPullParser.END_TAG) {
        if (parser.eventType != XmlPullParser.START_TAG) {
            continue
        }
        when (parser.name) {
            "title" -> title = readTitle(parser)
            "summary" -> summary = readSummary(parser)
            "link" -> link = readLink(parser)
            else -> skip(parser)
        }
    }
    return Entry(title, summary, link)
}

// Processes title tags in the feed.
@Throws(IOException::class, XmlPullParserException::class)
private fun readTitle(parser: XmlPullParser): String {
    parser.require(XmlPullParser.START_TAG, ns, "title")
    val title = readText(parser)
    parser.require(XmlPullParser.END_TAG, ns, "title")
    return title
}

// Processes link tags in the feed.
@Throws(IOException::class, XmlPullParserException::class)
private fun readLink(parser: XmlPullParser): String {
    var link = ""
    parser.require(XmlPullParser.START_TAG, ns, "link")
    val tag = parser.name
    val relType = parser.getAttributeValue(null, "rel")
    if (tag == "link") {
        if (relType == "alternate") {
            link = parser.getAttributeValue(null, "href")
            parser.nextTag()
        }
    }
    parser.require(XmlPullParser.END_TAG, ns, "link")
    return link
}

// Processes summary tags in the feed.
@Throws(IOException::class, XmlPullParserException::class)
private fun readSummary(parser: XmlPullParser): String {
    parser.require(XmlPullParser.START_TAG, ns, "summary")
    val summary = readText(parser)
    parser.require(XmlPullParser.END_TAG, ns, "summary")
    return summary
}

// For the tags title and summary, extracts their text values.
@Throws(IOException::class, XmlPullParserException::class)
private fun readText(parser: XmlPullParser): String {
    var result = ""
    if (parser.next() == XmlPullParser.TEXT) {
        result = parser.text
        parser.nextTag()
    }
    return result
}
...

Java

public static class Entry {
    public final String title;
    public final String link;
    public final String summary;

    private Entry(String title, String summary, String link) {
        this.title = title;
        this.summary = summary;
        this.link = link;
    }
}

// Parses the contents of an entry. If it encounters a title, summary, or link tag, hands them off
// to their respective "read" methods for processing. Otherwise, skips the tag.
private Entry readEntry(XmlPullParser parser) throws XmlPullParserException, IOException {
    parser.require(XmlPullParser.START_TAG, ns, "entry");
    String title = null;
    String summary = null;
    String link = null;
    while (parser.next() != XmlPullParser.END_TAG) {
        if (parser.getEventType() != XmlPullParser.START_TAG) {
            continue;
        }
        String name = parser.getName();
        if (name.equals("title")) {
            title = readTitle(parser);
        } else if (name.equals("summary")) {
            summary = readSummary(parser);
        } else if (name.equals("link")) {
            link = readLink(parser);
        } else {
            skip(parser);
        }
    }
    return new Entry(title, summary, link);
}

// Processes title tags in the feed.
private String readTitle(XmlPullParser parser) throws IOException, XmlPullParserException {
    parser.require(XmlPullParser.START_TAG, ns, "title");
    String title = readText(parser);
    parser.require(XmlPullParser.END_TAG, ns, "title");
    return title;
}

// Processes link tags in the feed.
private String readLink(XmlPullParser parser) throws IOException, XmlPullParserException {
    String link = "";
    parser.require(XmlPullParser.START_TAG, ns, "link");
    String tag = parser.getName();
    String relType = parser.getAttributeValue(null, "rel");
    if (tag.equals("link")) {
        if (relType.equals("alternate")){
            link = parser.getAttributeValue(null, "href");
            parser.nextTag();
        }
    }
    parser.require(XmlPullParser.END_TAG, ns, "link");
    return link;
}

// Processes summary tags in the feed.
private String readSummary(XmlPullParser parser) throws IOException, XmlPullParserException {
    parser.require(XmlPullParser.START_TAG, ns, "summary");
    String summary = readText(parser);
    parser.require(XmlPullParser.END_TAG, ns, "summary");
    return summary;
}

// For the tags title and summary, extracts their text values.
private String readText(XmlPullParser parser) throws IOException, XmlPullParserException {
    String result = "";
    if (parser.next() == XmlPullParser.TEXT) {
        result = parser.getText();
        parser.nextTag();
    }
    return result;
}
  ...
}

不要なタグをスキップする

上記の XML 解析ステップの 1 つとして、パーサーは、解析の対象ではないタグをスキップする必要があります。パーサーの skip() メソッドは次のようになります。

Kotlin

@Throws(XmlPullParserException::class, IOException::class)
private fun skip(parser: XmlPullParser) {
    if (parser.eventType != XmlPullParser.START_TAG) {
        throw IllegalStateException()
    }
    var depth = 1
    while (depth != 0) {
        when (parser.next()) {
            XmlPullParser.END_TAG -> depth--
            XmlPullParser.START_TAG -> depth++
        }
    }
}

Java

private void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
    if (parser.getEventType() != XmlPullParser.START_TAG) {
        throw new IllegalStateException();
    }
    int depth = 1;
    while (depth != 0) {
        switch (parser.next()) {
        case XmlPullParser.END_TAG:
            depth--;
            break;
        case XmlPullParser.START_TAG:
            depth++;
            break;
        }
    }
 }

仕組みは次のとおりです。

  • 現在のイベントが START_TAG でない場合は、例外をスローします。
  • START_TAG と、それに対応する END_TAG までのすべてのイベントを読み進めます。
  • 最初に遭遇した START_TAG の直後の END_TAG ではなく、正しい End_TAG で停止するように、ネストの深さを把握しながら進みます。

現在の要素の中にネスト要素がある場合、パーサーが最初の START_TAG とそれに対応する END_TAG との間にあるすべてのイベントを読み進めるまで、depth の値は 0 になりません。たとえば、2 つのネスト要素(<name><uri>)を持つ <author> 要素を、パーサーがどのようにスキップするのかを考えてみます。

  • 最初の while ループでは、<author> の後にパーサーが遭遇する次のタグは、<name>START_TAG です。depth の値は 2 に増えます。
  • 2 回目の while ループでは、パーサーが遭遇する次のタグは END_TAG </name> です。depth の値は 1 に減ります。
  • 3 回目の while ループでは、パーサーが遭遇する次のタグは START_TAG <uri> です。depth の値は 2 に増えます。
  • 4 回目の while ループでは、パーサーが遭遇する次のタグは END_TAG </uri> です。depth の値は 1 に減ります。
  • 最後の 5 回目の while ループでは、パーサーが遭遇する次のタグは END_TAG </author> です。depth の値は 0 に減り、<author> 要素が適切にスキップされたことを示します。

XML データを使用する

サンプルアプリは、AsyncTask 内で XML フィードを取得して解析します。これにより、処理がメイン UI スレッドから離れます。処理が完了すると、アプリは、メイン アクティビティ(NetworkActivity)内で UI をアップデートします。

下記の抜粋で、loadPage() メソッドは次の処理を行います。

  • XML フィードの URL で文字列変数を初期化します。
  • ユーザー設定とネットワーク接続で許可されている場合、new DownloadXmlTask().execute(url) を呼び出します。これにより、新しい DownloadXmlTask オブジェクト(AsyncTask サブクラス)がインスタンス化され、その execute() メソッドが実行されます。このメソッドはフィードをダウンロードして解析し、UI に表示される文字列結果を返します。

Kotlin

class NetworkActivity : Activity() {

    companion object {

        const val WIFI = "Wi-Fi"
        const val ANY = "Any"
        const val SO_URL = "http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest"
        // Whether there is a Wi-Fi connection.
        private var wifiConnected = false
        // Whether there is a mobile connection.
        private var mobileConnected = false

        // Whether the display should be refreshed.
        var refreshDisplay = true
        // The user's current network preference setting.
        var sPref: String? = null
    }

    ...

    // Uses AsyncTask subclass to download the XML feed from stackoverflow.com.
    // Uses AsyncTask to download the XML feed from stackoverflow.com.
    fun loadPage() {

        if (sPref.equals(ANY) && (wifiConnected || mobileConnected)) {
            DownloadXmlTask().execute(SO_URL)
        } else if (sPref.equals(WIFI) && wifiConnected) {
            DownloadXmlTask().execute(SO_URL)
        } else {
            // show error
        }
    }

    ...
}

Java

public class NetworkActivity extends Activity {
    public static final String WIFI = "Wi-Fi";
    public static final String ANY = "Any";
    private static final String URL = "http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest";

    // Whether there is a Wi-Fi connection.
    private static boolean wifiConnected = false;
    // Whether there is a mobile connection.
    private static boolean mobileConnected = false;
    // Whether the display should be refreshed.
    public static boolean refreshDisplay = true;
    public static String sPref = null;

    ...

    // Uses AsyncTask to download the XML feed from stackoverflow.com.
    public void loadPage() {

        if((sPref.equals(ANY)) && (wifiConnected || mobileConnected)) {
            new DownloadXmlTask().execute(URL);
        }
        else if ((sPref.equals(WIFI)) && (wifiConnected)) {
            new DownloadXmlTask().execute(URL);
        } else {
            // show error
        }
    }

以下に示す DownloadXmlTaskAsyncTask サブクラス)は、次の AsyncTask メソッドを実装しています。

  • doInBackground() は、loadXmlFromNetwork() メソッドを実行し、フィード URL をパラメータとして渡します。loadXmlFromNetwork() メソッドは、フィードを取得して処理します。終了したら結果文字列を返します。
  • onPostExecute() は、返された文字列を受け取り、UI 内に表示します。

Kotlin

// Implementation of AsyncTask used to download XML feed from stackoverflow.com.
private inner class DownloadXmlTask : AsyncTask<String, Void, String>() {
    override fun doInBackground(vararg urls: String): String {
        return try {
            loadXmlFromNetwork(urls[0])
        } catch (e: IOException) {
            resources.getString(R.string.connection_error)
        } catch (e: XmlPullParserException) {
            resources.getString(R.string.xml_error)
        }
    }

    override fun onPostExecute(result: String) {
        setContentView(R.layout.main)
        // Displays the HTML string in the UI via a WebView
        findViewById<WebView>(R.id.webview)?.apply {
            loadData(result, "text/html", null)
        }
    }
}

Java

// Implementation of AsyncTask used to download XML feed from stackoverflow.com.
private class DownloadXmlTask extends AsyncTask<String, Void, String> {
    @Override
    protected String doInBackground(String... urls) {
        try {
            return loadXmlFromNetwork(urls[0]);
        } catch (IOException e) {
            return getResources().getString(R.string.connection_error);
        } catch (XmlPullParserException e) {
            return getResources().getString(R.string.xml_error);
        }
    }

    @Override
    protected void onPostExecute(String result) {
        setContentView(R.layout.main);
        // Displays the HTML string in the UI via a WebView
        WebView myWebView = (WebView) findViewById(R.id.webview);
        myWebView.loadData(result, "text/html", null);
    }
}

DownloadXmlTask から呼び出される loadXmlFromNetwork() メソッドを以下に示します。このメソッドは次の処理を行います。

  1. StackOverflowXmlParser をインスタンス化します。また、Entry オブジェクト(entries)の List と、titleurlsummary の変数を作成します。これは XML フィードから抽出する各フィールドの値を保持するためです。
  2. downloadUrl() を呼び出します。これはフィードを取得し、それを InputStream として返します。
  3. StackOverflowXmlParser を使用して InputStream を解析します。StackOverflowXmlParser は、entriesList に、フィードのデータを入力します。
  4. entries List を処理して、フィードデータと HTML マークアップを組み合わせます。
  5. AsyncTask メソッドの onPostExecute() によって、メイン アクティビティの UI に表示される HTML 文字列を返します。

Kotlin

// Uploads XML from stackoverflow.com, parses it, and combines it with
// HTML markup. Returns HTML string.
@Throws(XmlPullParserException::class, IOException::class)
private fun loadXmlFromNetwork(urlString: String): String {
    // Checks whether the user set the preference to include summary text
    val pref: Boolean = PreferenceManager.getDefaultSharedPreferences(this)?.run {
        getBoolean("summaryPref", false)
    } ?: false

    val entries: List<Entry> = downloadUrl(urlString)?.use { stream ->
        // Instantiate the parser
        StackOverflowXmlParser().parse(stream)
    } ?: emptyList()

    return StringBuilder().apply {
        append("<h3>${resources.getString(R.string.page_title)}</h3>")
        append("<em>${resources.getString(R.string.updated)} ")
        append("${formatter.format(rightNow.time)}</em>")
        // StackOverflowXmlParser returns a List (called "entries") of Entry objects.
        // Each Entry object represents a single post in the XML feed.
        // This section processes the entries list to combine each entry with HTML markup.
        // Each entry is displayed in the UI as a link that optionally includes
        // a text summary.
        entries.forEach { entry ->
            append("<p><a href='")
            append(entry.link)
            append("'>" + entry.title + "</a></p>")
            // If the user set the preference to include summary text,
            // adds it to the display.
            if (pref) {
                append(entry.summary)
            }
        }
    }.toString()
}

// Given a string representation of a URL, sets up a connection and gets
// an input stream.
@Throws(IOException::class)
private fun downloadUrl(urlString: String): InputStream? {
    val url = URL(urlString)
    return (url.openConnection() as? HttpURLConnection)?.run {
        readTimeout = 10000
        connectTimeout = 15000
        requestMethod = "GET"
        doInput = true
        // Starts the query
        connect()
        inputStream
    }
}

Java

// Uploads XML from stackoverflow.com, parses it, and combines it with
// HTML markup. Returns HTML string.
private String loadXmlFromNetwork(String urlString) throws XmlPullParserException, IOException {
    InputStream stream = null;
    // Instantiate the parser
    StackOverflowXmlParser stackOverflowXmlParser = new StackOverflowXmlParser();
    List<Entry> entries = null;
    String title = null;
    String url = null;
    String summary = null;
    Calendar rightNow = Calendar.getInstance();
    DateFormat formatter = new SimpleDateFormat("MMM dd h:mmaa");

    // Checks whether the user set the preference to include summary text
    SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
    boolean pref = sharedPrefs.getBoolean("summaryPref", false);

    StringBuilder htmlString = new StringBuilder();
    htmlString.append("<h3>" + getResources().getString(R.string.page_title) + "</h3>");
    htmlString.append("<em>" + getResources().getString(R.string.updated) + " " +
            formatter.format(rightNow.getTime()) + "</em>");

    try {
        stream = downloadUrl(urlString);
        entries = stackOverflowXmlParser.parse(stream);
    // Makes sure that the InputStream is closed after the app is
    // finished using it.
    } finally {
        if (stream != null) {
            stream.close();
        }
     }

    // StackOverflowXmlParser returns a List (called "entries") of Entry objects.
    // Each Entry object represents a single post in the XML feed.
    // This section processes the entries list to combine each entry with HTML markup.
    // Each entry is displayed in the UI as a link that optionally includes
    // a text summary.
    for (Entry entry : entries) {
        htmlString.append("<p><a href='");
        htmlString.append(entry.link);
        htmlString.append("'>" + entry.title + "</a></p>");
        // If the user set the preference to include summary text,
        // adds it to the display.
        if (pref) {
            htmlString.append(entry.summary);
        }
    }
    return htmlString.toString();
}

// Given a string representation of a URL, sets up a connection and gets
// an input stream.
private InputStream downloadUrl(String urlString) throws IOException {
    URL url = new URL(urlString);
    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
    conn.setReadTimeout(10000 /* milliseconds */);
    conn.setConnectTimeout(15000 /* milliseconds */);
    conn.setRequestMethod("GET");
    conn.setDoInput(true);
    // Starts the query
    conn.connect();
    return conn.getInputStream();
}