The Android Developer Challenge is back! Submit your idea before December 2.

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 タグと、そのネストされたタグ 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 タグと、そのネストされたタグ titlelinksummary のデータが抽出されます。
  2. 次のメソッドを作成します。

    • ご自身が興味のある各タグの「read」メソッド(readEntry()readTitle() など)。パーサーは入力ストリームからタグが読み取り、entrytitlelinksummary という名前のタグを見つけると、そのタグに対して適切なメソッドを呼び出します。それ以外の場合、タグはスキップされます。
    • さまざまな種類の各タグに対してデータを抽出し、パーサーを次のタグに進めるメソッド。次に例を示します。
      • title タグおよび summary タグについては、パーサーは readText() を呼び出します。このメソッドは、parser.getText() を呼び出すことで、これらのタグのデータを抽出します。
      • link タグについては、パーサーは、リンクが興味の対象かどうかを最初に確認することで、そのリンクのデータを抽出します。次に、parser.getAttributeValue() を使用してリンクの値を抽出します。
      • entry タグについては、パーサーは readEntry() を呼び出します。このメソッドは、エントリのネストされたタグを解析し、データメンバー titlelinksummary を含む Entry オブジェクトを返します。
    • 再帰的なヘルパー 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 で停止していることを確認するために、ネストの深さを追跡します。

したがって、ネストされた要素が現在の要素に含まれる場合、パーサーによって元の 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
            }
        }
    

以下に示す AsyncTask サブクラス DownloadXmlTask では、次の 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 オブジェクトの Listentries)と、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();
    }