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. La carga y el análisis de 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.
Si quieres obtener más información para crear contenido basado en la Web en tu app para Android, consulta Contenido basado en la Web.
Cómo elegir un analizador
Recomendamos XmlPullParser
, que es una forma eficiente y sostenible de analizar XML en Android. Android tiene dos implementaciones de esta interfaz:
KXmlParser
que usaXmlPullParserFactory.newPullParser()
ExpatPullParser
que usaXml.newPullParser()
Cualquiera de las dos opciones está bien. El ejemplo de esta sección utiliza ExpatPullParser
a través de 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.
Consulta el siguiente extracto de un feed analizado en la app de ejemplo. 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 al analizar un feed es crear una instancia de un analizador y comenzar el proceso de análisis. Este fragmento inicializa un analizador para que no procese espacios de nombres y use el InputStream
proporcionado como entrada. Inicia el proceso de análisis con una llamada a nextTag()
y, luego, 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 de manera recursiva, readFeed()
mostrará una List
con las entradas (incluidos los miembros de datos anidados) que extrajo del feed. Luego, el analizador 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:
- 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 anidadastitle
,link
ysummary
. - Crea los siguientes métodos:
- Un método "read" para cada etiqueta que deseas incluir, como
readEntry()
yreadTitle()
. El analizador lee etiquetas del flujo de entrada. Cuando se encuentra con una etiqueta con alguno de los nombres de ejemploentry
,title
,link
osummary
, llama al método apropiado para esa etiqueta. 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. En este ejemplo, los métodos relevantes son los siguientes:
- En el caso de las etiquetas
title
ysummary
, el analizador llama areadText()
. Este método extrae los datos de estas etiquetas invocando aparser.getText()
. - En el caso de la etiqueta
link
, el analizador extrae los datos de los vínculos determinando primero si pertenecen al tipo que le interesa. Luego, usaparser.getAttributeValue()
para extraer el valor de los vínculos. - En el caso de la etiqueta
entry
, el analizador llama areadEntry()
. Este método analiza las etiquetas anidadas de la entrada y muestra un objetoEntry
con los miembros de datostitle
,link
ysummary
.
- En el caso de las etiquetas
- 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.
- Un método "read" para cada etiqueta que deseas incluir, como
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
El analizador debe omitir las 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:
- Arroja una excepción si el evento actual no es un elemento
START_TAG
. - Consume el elemento
START_TAG
y todos los eventos, inclusoEND_TAG
. - Realiza un seguimiento de la profundidad de anidación para garantizar que se detenga en la
END_TAG
correcta y no en la primera etiqueta que encuentre después de laSTART_TAG
original.
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 la 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 laSTART_TAG
correspondiente a<name>
. El valor dedepth
incrementa a 2. - La segunda vez que escanea el bucle
while
, la siguiente etiqueta que el analizador encuentra es laEND_TAG
correspondiente a</name>
. El valor dedepth
disminuye a 1. - La tercera vez que escanea el bucle
while
, la siguiente etiqueta que el analizador encuentra es laSTART_TAG
correspondiente a<uri>
. El valor dedepth
incrementa a 2. - La cuarta vez que escanea el bucle
while
, la siguiente etiqueta que el analizador encuentra es laEND_TAG
correspondiente a</uri>
. El valor dedepth
disminuye a 1. - La quinta y última vez que escanea el bucle
while
, la siguiente etiqueta que el analizador encuentra es laEND_TAG
correspondiente a</author>
. El valor dedepth
disminuye a 0, lo que indica que el elemento<author>
se omite de forma correcta.
Cómo consumir datos XML
La aplicación de ejemplo recupera y analiza el feed XML de forma asíncrona. De esta manera, se elimina el tratamiento del subproceso principal de IU. Una vez finalizado el procesamiento, la app actualiza la IU en la actividad principal (NetworkActivity
).
En el siguiente extracto, el método loadPage()
hace lo siguiente:
- Inicializa una variable de cadena con la URL para el feed XML.
- Invoca el método
downloadXml(url)
, si la configuración del usuario y la conexión de red lo permiten. Este método descarga y analiza el feed, y muestra un resultado de cadena que se mostrará 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 } ... // 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. } }
El método downloadXml
llama a los siguientes métodos en Kotlin:
lifecycleScope.launch(Dispatchers.IO)
, que usa las corrutinas de Kotlin para iniciar el métodoloadXmlFromNetwork()
en el subproceso de IO. Pasa la URL del feed como parámetro. El métodoloadXmlFromNetwork()
recupera y procesa el feed. Cuando termina, vuelve a pasar una cadena de resultados.withContext(Dispatchers.Main)
, que usa las corrutinas de Kotlin para regresar al subproceso principal, toma la cadena que se muestra y la enseña en la IU.
En el lenguaje de programación Java, el proceso es el siguiente:
- Un
Executor
ejecuta el métodoloadXmlFromNetwork()
en un subproceso en segundo plano. Pasa la URL del feed como parámetro. El métodoloadXmlFromNetwork()
recupera y procesa el feed. Cuando termina, vuelve a pasar una cadena de resultados. - Un
Handler
llama apost
para regresar al subproceso principal, toma la cadena que se muestra y la muestra en la IU.
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); }); }); }
El método loadXmlFromNetwork()
que se invoca desde downloadXml
se muestra en el siguiente fragmento. Hace lo siguiente:
- Crea una instancia de
StackOverflowXmlParser
. También crea variables para unaList
de objetos deEntry
(entries
), ytitle
,url
ysummary
, para mantener los valores extraídos del feed XML correspondientes a esos campos. - Llama a la
downloadUrl()
, que recupera el feed y lo muestra como unInputStream
. - Usa
StackOverflowXmlParser
para analizar elInputStream
.StackOverflowXmlParser
propaga unaList
deentries
con datos del feed. - Procesa la
List
deentries
y combina los datos del feed con lenguaje de marcado HTML. - Muestra una cadena HTML que se muestra en la IU de la actividad principal.
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(); }