XML データを解析する

Extensible Markup Language(XML)は、マシンが読める形式でドキュメントをエンコードするための一連のルールです。XML は、インターネット上でデータを共有するための一般的な形式です。ニュースサイトやブログなど、コンテンツが頻繁に更新されるウェブサイトでは、外部プログラムがコンテンツの変更に遅れをとらないようにするために XML フィードを提供することがよくあります。XML データのアップロードと解析は、ネットワーク接続アプリにとっては一般的な作業です。このレッスンでは、XML ドキュメントを解析してそのデータを使用する方法について説明します。

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

パーサーを選択する

XmlPullParser をおすすめします。Android で XML を効率的に解析でき、保守も容易です。歴史的に、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. 以下のメソッドを作成します。

    • 抽出する各タグ用の「読み取り」メソッド。たとえば、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 解析のステップには、抽出対象でない不要なタグをパーサーがスキップする手順が含まれています。パーサーの 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 までのすべてのイベントを消費します。
  • 正しい END_TAG(元の START_TAG タグの後で最初に遭遇するものとは限らない)で停止するために、ネストの深さを追跡します。

つまり、現在の要素にネストされた要素がある場合、パーサーが元の START_TAG とそれに対応する END_TAG の間のすべてのイベントを消費するまで、depth の値は 0 になりません。たとえば、2 つのネストされた要素 <name> および <uri> を持つ <author> 要素をパーサーがどのようにスキップするかを見てみましょう。

  • 初回の while ループで、<author> の次にパーサーが遭遇するタグは、<name>START_TAG です。depth の値は 1 つ増えて 2 になります。
  • 2 回目の while ループで、次にパーサーが遭遇するタグは、</name>END_TAG です。depth の値は 1 つ減って 1 になります。
  • 3 回目の while ループで、次にパーサーが遭遇するタグは、<uri>START_TAG です。depth の値は 1 つ増えて 2 になります。
  • 4 回目の while ループで、次にパーサーが遭遇するタグは、</uri>END_TAG です。depth の値は 1 つ減って 1 になります。
  • 5 回目の最後の while ループで、次にパーサーが遭遇するタグは、</author>END_TAG です。depth の値は 1 つ減って 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 をインスタンス化します。これらのフィールドの XML フィードから抽出された値を保持するために、Entry オブジェクト(entries)の Listおよび titleurlsummary 用の変数も作成します。
  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();
}