Cómo analizar datos XML

El lenguaje de marcado extensible (XML) es un conjunto de reglas para codificar documentos en un formato compatible con computadoras. XML es un formato popular para compartir datos en Internet. Los sitios web que actualizan con frecuencia su contenido, como los sitios de noticias o los blogs, suelen proporcionar un feed XML para que los programas externos puedan estar al tanto de los cambios de contenido. Subir y analizar datos XML es una tarea común para las apps conectadas a la red. En esta lección, se explica cómo analizar documentos XML y usar sus datos.

Para obtener más información sobre cómo crear contenido basado en web en tu app para Android, consulta Aplicaciones web.

Elige un analizador

Recomendamos XmlPullParser, que es una forma eficiente y sostenible de analizar XML en Android. Históricamente, Android tuvo dos implementaciones de esta interfaz:

Cualquiera de las dos opciones está bien. El ejemplo de esta sección utiliza ExpatPullParser mediante Xml.newPullParser().

Cómo analizar el feed

El primer paso para analizar un feed es decidir qué campos te interesan. El analizador extrae datos de esos campos y omite el resto.

Aquí tienes un extracto del feed que se analiza en la app de muestra. Cada publicación en StackOverflow.com aparece en el feed como una etiqueta entry que contiene varias etiquetas anidadas:

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

La app de ejemplo extrae datos de la etiqueta entry y sus etiquetas anidadas title, link y summary.

Cómo crear una instancia del analizador

El siguiente paso es crear una instancia de un analizador y comenzar el proceso de análisis. En este fragmento, se inicializa un analizador para que no procese espacios de nombres y a fin de usar el InputStream proporcionado como entrada. Inicia el proceso de análisis con una llamada a nextTag() e invoca el método readFeed(), que extrae y procesa los datos que le interesan a la app:

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

Cómo leer el feed

El método readFeed() realiza el trabajo real de procesar el feed. Busca elementos con la etiqueta "entry" como punto de partida para procesar el feed de manera recursiva. Si una etiqueta no es del tipo entry, se la omite. Una vez que se haya procesado todo el feed, readFeed() mostrará una lista con las entradas de la List (incluidos los miembros de datos anidados) que extrajo del feed. El analizador después mostrará la 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;
    }
    

Cómo analizar XML

Los pasos para analizar un feed XML son los siguientes:

  1. Como se describe en Cómo analizar el feed, identifica las etiquetas que deseas incluir en tu app. En este ejemplo, se extraen datos para la etiqueta entry y sus etiquetas anidadas title, link y summary.
  2. Crea los siguientes métodos:

    • Un método "read" para cada etiqueta que te interese. Por ejemplo, readEntry(), readTitle(), etcétera. El analizador lee etiquetas del flujo de entrada. Cuando se encuentra con una del tipo entry, title, link o summary invoca al método correspondiente. De lo contrario, se omite la etiqueta.
    • Métodos para extraer datos de cada tipo diferente de etiqueta y pasar el analizador a la siguiente etiqueta. Por ejemplo:
      • En el caso de las etiquetas title y summary, el analizador invoca a readText(). Este método extrae los datos de estas etiquetas invocando a parser.getText().
      • En el caso de la etiqueta link, el analizador extrae los datos de los vínculos determinando primero si pertenece al tipo que le interesa. Luego, usa parser.getAttributeValue() para extraer el valor del vínculo.
      • En el caso de la etiqueta entry, el analizador invoca a readEntry(). Este método analiza las etiquetas anidadas de la entrada y muestra un objeto Entry con los miembros de datos title, link y summary.
    • Un método de ayuda skip() que es recursivo. Para obtener más información sobre este tema, consulta Cómo omitir etiquetas que no te interesan.

En este fragmento, se muestra cómo el analizador se encarga de entradas, títulos, vínculos y resúmenes.

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

Cómo omitir las etiquetas que no te interesan

Uno de los pasos del análisis de XML descrito anteriormente sirve para que el analizador omita etiquetas que no le interesan. Este es el método skip() del analizador:

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

Funciona de la siguiente manera:

  • Lanza una excepción si el evento actual no es un elemento START_TAG.
  • Consume el elemento START_TAG y todos los eventos, incluso END_TAG.
  • A fin de asegurarte de que se detenga en el END_TAG correcto y no en la primera etiqueta que encuentra después del elemento START_TAG original, realiza un seguimiento de la profundidad de anidación.

Por lo tanto, si el elemento actual tiene otros elementos anidados, el valor de depth no será 0 hasta que el analizador haya consumido todos los eventos entre el START_TAG original y su END_TAG correspondiente. Por ejemplo, considera cómo el analizador omite el elemento <author>, que tiene 2 elementos anidados (<name> y <uri>):

  • La primera vez que escanea el bucle while, la siguiente etiqueta que el analizador encuentra después de <author> es el START_TAG correspondiente a <name>. Se aumenta el valor de depth a 2.
  • La segunda vez que escanea el bucle while, la siguiente etiqueta que el analizador encuentra es el elemento END_TAG correspondiente a </name>. Se reduce el valor de depth a 1.
  • La tercera vez que escanea el bucle while, la siguiente etiqueta que el analizador encuentra es el elemento START_TAG correspondiente a <uri>. Se aumenta el valor de depth a 2.
  • La cuarta vez que escanea el bucle while, la siguiente etiqueta que el analizador encuentra es el elemento END_TAG correspondiente a </uri>. Se reduce el valor de depth a 1.
  • La quinta y última vez que escanea el bucle while, la siguiente etiqueta que el analizador encuentra es el elemento END_TAG correspondiente a </author>. El valor de depth se reduce a 0, lo que indica que el elemento <author> se omitió correctamente.

Cómo consumir datos XML

La aplicación de ejemplo obtiene y analiza el feed XML dentro de una AsyncTask. De esta manera, se elimina el procesamiento del subproceso principal de IU. Una vez finalizado el procesamiento, la app actualiza la IU en la actividad principal (NetworkActivity).

En el extracto que se muestra a continuación, el método loadPage() hace lo siguiente:

  • Inicializa una variable de string con la URL para el feed XML.
  • Si la configuración del usuario y la conexión de red lo permiten, invoca a new DownloadXmlTask().execute(url). Esto instancia un nuevo objeto DownloadXmlTask (subclase de AsyncTask) y ejecuta su método execute(), que descarga y analiza el feed, y muestra un resultado de string en la IU.

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

La subclase AsyncTask que se muestra a continuación, DownloadXmlTask, implementa los siguientes métodos de la AsyncTask:

  • doInBackground() ejecuta el método de la loadXmlFromNetwork(). Pasa la URL del feed como parámetro. El método loadXmlFromNetwork() obtiene y procesa el feed. Cuando termina, devuelve una string de resultados.
  • onPostExecute() toma la string obtenida y la muestra en la IU.

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

A continuación, se muestra el método loadXmlFromNetwork(), que se invoca desde DownloadXmlTask. Hace lo siguiente:

  1. Inicia un StackOverflowXmlParser. También crea variables para un elemento de la List de objetos de Entry (entries), y title, url y summary a fin de mantener los valores extraídos del feed XML correspondientes a esos campos.
  2. Invoca la downloadUrl(), que recupera el feed y lo muestra como un InputStream.
  3. Usa StackOverflowXmlParser para analizar el InputStream. StackOverflowXmlParser propaga un elemento de la List de entries con datos del feed.
  4. Procesa la List de entries y combina los datos del feed con lenguaje de marcado HTML.
  5. Muestra una cadena HTML que aparece en la IU de la actividad principal mediante el método de la AsyncTask de onPostExecute().

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