XML データを解析する

拡張マークアップ言語(XML)は、機械で判読可能な形式でドキュメントをエンコードするためのルールのセットです。XML は、インターネット上でデータを共有するための一般的な形式です。

ニュースサイトやブログなど、コンテンツが頻繁に更新されるウェブサイトは、多くの場合、外部プログラムがコンテンツ変更についていけるように XML フィードを提供しています。XML データのアップロードと解析は、ネットワークに接続されたアプリにとって、ありふれたタスクです。このトピックでは、XML ドキュメントを解析して、そのデータを利用する方法について説明します。

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

パーサーを選択する

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

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

フィードを分析する

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

サンプルアプリの解析したフィードの抜粋を以下に示します。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;
}
  ...
}

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

パーサーは、解析の対象ではないタグをスキップする必要があります。パーサーの 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 データを使用する

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

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

  • XML フィードの URL で文字列変数を初期化します。
  • ユーザー設定とネットワーク接続で許可されている場合、downloadXml(url) メソッドを呼び出します。このメソッドはフィードをダウンロードして解析し、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
    }
    ...
    // Asynchronously downloads the XML feed from stackoverflow.com.
    fun loadPage() {

        if (sPref.equals(ANY) && (wifiConnected || mobileConnected)) {
            downloadXml(SO_URL)
        } else if (sPref.equals(WIFI) && wifiConnected) {
            downloadXml(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;
    ...
    // Asynchronously downloads the XML feed from stackoverflow.com.
    public void loadPage() {

        if((sPref.equals(ANY)) && (wifiConnected || mobileConnected)) {
            downloadXml(URL);
        }
        else if ((sPref.equals(WIFI)) && (wifiConnected)) {
            downloadXml(URL);
        } else {
            // Show error.
        }
    }

downloadXml メソッドは Kotlin で次のメソッドを呼び出します。

  • lifecycleScope.launch(Dispatchers.IO)。Kotlin のコルーチンを使用して、IO スレッドで loadXmlFromNetwork() メソッドを開始します。フィード URL をパラメータとして渡します。loadXmlFromNetwork() メソッドは、フィードを取得して処理します。終了したら結果文字列を返します。
  • withContext(Dispatchers.Main)。Kotlin のコルーチンを使用してメインスレッドに戻り、返された文字列を受け取って UI に表示します。

Java プログラミング言語でのプロセスは次のとおりです。

  • Executor は、バックグラウンド スレッドで loadXmlFromNetwork() メソッドを実行します。フィード URL をパラメータとして渡します。loadXmlFromNetwork() メソッドは、フィードを取得して処理します。終了したら結果文字列を返します。
  • Handler は、post を呼び出してメインスレッドに戻り、返された文字列を受け取って UI に表示します。

Kotlin

// Implementation of Kotlin coroutines used to download XML feed from stackoverflow.com.
private fun downloadXml(vararg urls: String) {
    var result: String? = null
    lifecycleScope.launch(Dispatchers.IO) {
        result = try {
            loadXmlFromNetwork(urls[0])
        } catch (e: IOException) {
            resources.getString(R.string.connection_error)
        } catch (e: XmlPullParserException) {
            resources.getString(R.string.xml_error)
        }
        withContext(Dispatchers.Main) {
            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 Executor and Handler used to download XML feed asynchronously from stackoverflow.com.
private void downloadXml(String... urls) {
    ExecutorService executor = Executors.newSingleThreadExecutor();
    Handler handler = new Handler(Looper.getMainLooper());
    executor.execute(() -> {
        String result;
            try {
                result = loadXmlFromNetwork(urls[0]);
            } catch (IOException e) {
                result = getResources().getString(R.string.connection_error);
            } catch (XmlPullParserException e) {
                result = getResources().getString(R.string.xml_error);
            }
        String finalResult = result;
        handler.post(() -> {
            setContentView(R.layout.main);
            // Displays the HTML string in the UI via a WebView.
            WebView myWebView = (WebView) findViewById(R.id.webview);
            myWebView.loadData(finalResult, "text/html", null);
        });
    });
}

downloadXml から呼び出される loadXmlFromNetwork() メソッドを次のスニペットに示します。このメソッドは次の処理を行います。

  1. StackOverflowXmlParser をインスタンス化します。また、Entry オブジェクト(entries)の List と、titleurlsummary の変数を作成します。これは XML フィードから抽出する各フィールドの値を保持するためです。
  2. downloadUrl() を呼び出します。これはフィードを取得し、それを InputStream として返します。
  3. StackOverflowXmlParser を使用して InputStream を解析します。StackOverflowXmlParser は、entriesList に、フィードのデータを入力します。
  4. entries List を処理して、フィードデータと HTML マークアップを組み合わせます。
  5. メイン アクティビティの 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 ->
        // Instantiates 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;
    // Instantiates 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();
}