Mengurai data XML

Extensible Markup Language (XML) adalah serangkaian aturan untuk mengenkode dokumen dalam bentuk yang dapat dibaca mesin. XML merupakan format populer untuk berbagi data di internet.

Situs yang sering memperbarui kontennya, seperti situs berita atau blog, sering menyediakan feed XML sehingga program eksternal dapat terus mengikuti perubahan konten. Mengupload dan menguraikan data XML adalah tugas umum untuk aplikasi yang terhubung ke jaringan. Topik ini menjelaskan cara mengurai dokumen XML dan menggunakan datanya.

Untuk mempelajari lebih lanjut cara membuat konten berbasis web di aplikasi Android, lihat Konten berbasis web.

Memilih parser

Sebaiknya gunakan XmlPullParser, sebuah cara yang efisien dan mudah dipelihara untuk mengurai XML di Android. Android memiliki dua implementasi antarmuka ini:

Anda dapat memilih yang mana saja. Contoh di bagian ini menggunakan ExpatPullParser dan Xml.newPullParser().

Menganalisis feed

Langkah pertama dalam mengurai feed adalah menentukan kolom yang Anda inginkan. Parser mengekstrak data untuk kolom tersebut dan mengabaikan lainnya.

Lihat kutipan berikut dari feed yang diuraikan dalam aplikasi contoh. Setiap postingan ke StackOverflow.com akan muncul di feed sebagai tag entry yang berisi beberapa tag bertingkat:

<?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>

Aplikasi contoh akan mengekstrak data untuk tag entry serta title, link, dan summary tag bertingkat di dalamnya.

Membuat instance parser

Langkah berikutnya dalam mengurai feed adalah membuat instance parser dan memulai proses penguraian. Cuplikan ini menginisialisasi parser untuk tidak memproses namespace dan menggunakan InputStream yang disediakan sebagai inputnya. Ini akan memulai proses penguraian dengan panggilan ke nextTag() dan memanggil metode readFeed(), yang akan mengekstrak dan memproses data yang diperlukan aplikasi:

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();
        }
    }
 ...
}

Membaca feed

Metode readFeed() akan melakukan tugas sebenarnya dalam memproses feed. Metode ini mencari elemen dengan tag "entri" sebagai titik awal untuk memproses feed secara berulang. Jika tag bukan tag entry, tag tersebut akan dilewati. Setelah seluruh feed diproses secara berulang, readFeed() akan menampilkan List yang berisi entri (termasuk anggota data bertingkat) yang diekstrak dari feed. List ini kemudian akan ditampilkan oleh parser.

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;
}

Mengurai XML

Langkah-langkah untuk mengurai feed XML adalah sebagai berikut:

  1. Seperti yang dijelaskan dalam Menganalisis feed, identifikasikan tag yang ingin Anda sertakan dalam aplikasi Anda. Contoh ini akan mengekstrak data untuk tag entry serta tag bertingkat: title, link, dan summary di dalamnya.
  2. Buat metode berikut:

    • Metode "baca" untuk setiap tag yang ingin Anda sertakan, seperti readEntry() dan readTitle(). Parser akan membaca tag dari aliran input. Jika menemukan tag bernama, dalam contoh ini, entry, title, link, atau summary, metode ini akan memanggil metode yang sesuai untuk tag tersebut. Jika tidak, parser akan melewati tag.
    • Metode untuk mengekstrak data bagi setiap jenis tag yang berbeda dan untuk melanjutkan parser ke tag berikutnya. Dalam contoh ini, metode yang relevan adalah sebagai berikut:
      • Untuk tag title dan summary, parser akan memanggil readText(). Metode ini akan mengekstrak data untuk tag ini dengan memanggil parser.getText().
      • Untuk tag link, parser akan mengekstrak data untuk link dengan menentukan terlebih dahulu apakah link tersebut jenis yang diperlukan. Parser kemudian akan menggunakan parser.getAttributeValue() untuk mengekstrak nilai link.
      • Untuk tag entry, parser akan memanggil readEntry(). Metode ini mengurai tag bertingkat entri dan menampilkan objek Entry dengan title, link, dan summary anggota data.
    • Metode skip() helper yang berulang. Untuk pembahasan topik ini lebih lanjut, lihat Lewati tag yang tidak Anda perlukan.

Cuplikan ini menunjukkan cara parser mengurai entri, judul, link, dan ringkasan.

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;
}
  ...
}

Melewati tag yang tidak Anda perlukan

Parser harus melewati tag yang tidak diperlukannya. Berikut metode skip() parser:

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;
        }
    }
 }

Begini cara kerjanya:

  • Parser akan menampilkan pengecualian jika peristiwa saat ini bukan START_TAG.
  • START_TAG akan digunakan beserta semua peristiwa hingga dan termasuk END_TAG yang cocok.
  • Fitur ini terus melacak kedalaman tingkatan untuk memastikan kedalamannya berhenti di END_TAG yang benar dan bukan pada tag pertama yang ditemuinya setelah START_TAG yang asli.

Dengan begitu, jika elemen saat ini memiliki elemen bertingkat, nilai depth tidak akan menjadi 0 sampai parser menggunakan semua peristiwa antara START_TAG asli dan END_TAG yang cocok. Misalnya, perhatikan cara parser melewati elemen <author>, yang memiliki 2 elemen bertingkat, <name> dan <uri>:

  • Saat kali pertama melalui loop while, tag berikutnya yang ditemukan parser setelah <author> adalah START_TAG untuk <name>. Nilai untuk penambahan depth menjadi 2.
  • Kali kedua melalui loop while, tag berikutnya yang ditemukan parser adalah END_TAG </name> Nilai untuk pengurangan depth menjadi 1.
  • Kali ketiga melalui loop while, tag berikutnya yang ditemukan parser adalah START_TAG <uri>. Nilai untuk depth bertambah menjadi 2.
  • Kali keempat melalui loop while, tag berikutnya yang ditemukan parser adalah END_TAG </uri>. Nilai untuk pengurangan depth menjadi 1.
  • Kali kelima dan saat terakhir melalui loop while, tag berikutnya yang ditemukan parser adalah END_TAG </author>. Nilai untuk pengurangan depth menjadi 0, yang menunjukkan bahwa elemen <author> berhasil dilewati.

Menggunakan data XML

Aplikasi contoh mengambil dan mengurai feed XML secara asinkron. Tindakan ini akan menghilangkan pemrosesan UI thread utama. Saat pemrosesan selesai, aplikasi akan memperbarui UI dalam aktivitas utamanya, yaitu NetworkActivity.

Dalam cuplikan berikut, metode loadPage() melakukan hal berikut:

  • Menginisialisasi variabel string dengan URL untuk feed XML.
  • Memanggil metode downloadXml(url), jika setelan pengguna dan koneksi jaringan mengizinkannya. Metode ini mendownload dan mengurai feed dan menampilkan hasil string yang akan ditampilkan di 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.
        }
    }

Metode downloadXml memanggil metode berikut di Kotlin:

  • lifecycleScope.launch(Dispatchers.IO), yang menggunakan coroutine Kotlin untuk meluncurkan metode loadXmlFromNetwork() pada thread IO. URL feed akan diteruskan sebagai parameter. Metode loadXmlFromNetwork() akan mengambil dan memproses feed. Jika selesai, metode ini akan mengembalikan string hasil.
  • withContext(Dispatchers.Main), yang menggunakan coroutine Kotlin untuk kembali ke thread utama, mengambil string yang ditampilkan, dan menampilkannya di UI.

Dalam bahasa pemrograman Java, prosesnya adalah sebagai berikut:

  • Executor mengeksekusi metode loadXmlFromNetwork() di thread latar belakang. URL feed akan diteruskan sebagai parameter. Metode loadXmlFromNetwork() akan mengambil dan memproses feed. Jika selesai, metode ini akan mengembalikan string hasil.
  • Handler memanggil post untuk kembali ke thread utama, mengambil string yang ditampilkan, dan menampilkannya di 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);
        });
    });
}

Metode loadXmlFromNetwork() yang dipanggil dari downloadXml akan ditampilkan dalam cuplikan berikutnya. Metode ini akan melakukan hal berikut:

  1. Membuat instance StackOverflowXmlParser. Tindakan ini juga membuat variabel untuk List objek Entry (entries) serta untuk title, url, dan summary, untuk menyimpan nilai yang diekstrak dari feed XML untuk kolom tersebut.
  2. Memanggil downloadUrl(), yang akan mengambil feed dan menampilkannya sebagai InputStream.
  3. Menggunakan StackOverflowXmlParser untuk mengurai InputStream. StackOverflowXmlParser mengisi List entries dengan data dari feed.
  4. Memproses entries List dan menggabungkan data feed dengan markup HTML.
  5. Menampilkan string HTML yang ditampilkan di UI aktivitas utama.

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();
}