Leer/parsear archivo XML en Android mediante SAX
Con motivo del concurso #BigBangApps convocado por la web Ideas4All en que proponían desarrollar una aplicación utilizando su API, me puse a programar una aplicación para Android de la web. La API de Ideas4All devuelve los datos en XML (hubiese preferido en JSON pero de momento no lo permite) por lo que el «gran secreto» de la app es leer correctamente este tipo de archivos.
Hay varias formas de leer archivos XML, siendo una de ellas mediante SAX (Simple API for XML). En este post voy a explicar los pasos para leer/parsear correctamente un archivo XML disponible en Internet.
El fichero de ejemplo que queremos leer es el listado de categorías:
1 2 3 4 5 6 7 8 9 10 11 12 13 | <?xml version="1.0" encoding="UTF-8"?> <categories type="array"> <category> <id type="integer">6</id> <name>Vida/Salud</name> </category> <category> <id type="integer">3</id> <name>Sostenibilidad</name> </category> ... ... </categories> |
Lo primero que necesitamos es una clase que utilizaremos para guardar los datos de cada categoría. El objetivo es devolver una lista de objetos del tipo categoría, donde se encontraran todas las categorías leídas del XML.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | public class ParsedCategoryDataSet { private String id = null; private String name = null; public String getId() { return id; } public void setId(String extractedString) { this.id = extractedString; } public String getName() { return name; } public void setName(String extractedString) { this.name = extractedString; } public String toString(){ return "name = " + this.name; } } |
Como podemos observar, solamente se trata de una clase con setters y getters para almacenar los datos de cada categoría.
Ahora pasamos a crear la clase con la que parsearemos el archivo XML mediante SAX en Android.
El modelo SAX lee secuencialmente el fichero XML y ejecuta varios métodos (que podemos controlar) por cada elemento leído. Estos métodos son:
– startDocument(): comienza a leer el XML.
– endDocument(): ha terminado de leer XML.
– startElement(): ha encontrado el comienzo de una etiqueta XML.
– endElement(): ha encontrado el cierre de una etiqueta XML.
– characters(): texto que ha encontrado entre el comienzo y el cierre de una etiqueta
Estos métodos se encuentran en la clase org.xml.sax.helpers.DefaultHandler, de la que heredaremos nuestro propio parseador de categorías:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | import java.util.Vector; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; public class CategoryHandler extends DefaultHandler{ public CategoryHandler() { super(); this.myParsedCategoryDataSet = new Vector<ParsedCategoryDataSet>(); } // Variables de control para saber cuando estamos en el interior de cada etiqueta @SuppressWarnings("unused") private boolean in_category = false; private boolean in_id = false; private boolean in_name = false; // En esta variable guardamos el texto encontrado entre las etiquetas StringBuilder builder; // Aquí guardamos cada objeto categoria private ParsedCategoryDataSet DataSet; // Vector donde se guardaran todas las categorías encontradas private Vector<ParsedCategoryDataSet> myParsedCategoryDataSet; public Vector<ParsedCategoryDataSet> getParsedCategoryDataSets() { return this.myParsedCategoryDataSet; } public Vector<ParsedCategoryDataSet> getParsedData() { return this.myParsedCategoryDataSet; } @Override public void startDocument() throws SAXException { // Comenzamos a leer el fichero xml, creamos el vector donde se guardarán las categorías this.myParsedCategoryDataSet = new Vector<ParsedCategoryDataSet>(); } @Override public void endDocument() throws SAXException { // Ha terminado de leer el fichero, en este paso no hacemos nada } @Override public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException { if (localName.equals("category")) { // Ha encontrado la etiqueta principal de cada elemento "category" // Creamos un nuevo objeto categoría donde iremos guardando los datos this.in_category = true; DataSet = new ParsedCategoryDataSet(); }else if (localName.equals("id")) { // Estamos dentro de la etiqueta "id", creamos el StringBuilder que utilizaremos // en el método characters para guardar el contenido this.in_id = true; builder = new StringBuilder(); }else if (localName.equals("name")) { // Estamos dentro de la etiqueta "name", creamos el StringBuilder que utilizaremos // en el método characters para guardar el contenido this.in_name = true; builder = new StringBuilder(); } } @Override public void endElement(String namespaceURI, String localName, String qName) throws SAXException { if (localName.equals("category")) { // Hemos llegado al final de la etiqueta principal de cada elemento "category" // Añadimos al vector el elemento leído this.in_category = false; myParsedCategoryDataSet.add(DataSet); }else if (localName.equals("id")) { // Ha encontrado la etiqueta de cierre de "id" this.in_id = false; }else if (localName.equals("name")) { // Ha encontrado la etiqueta de cierre de "name" this.in_name = false; } } @Override public void characters(char ch[], int start, int length) { // Si estamos dentro de la etiqueta "id" if(this.in_id){ if (builder!=null) { for (int i=start; i<start+length; i++) { // Añadimos al StringBuilder (definido al encontrar el comienzo de la etiqueta "id") // lo que haya entre las etiquetas de inicio y fin builder.append(ch[i]); } } // Lo asignamos al "id" del objeto categoría (DataSet) DataSet.setId(builder.toString()); } // Si estamos dentro de la etiqueta "id" if(this.in_name){ if (builder!=null) { for (int i=start; i<start+length; i++) { // Añadimos al StringBuilder (definido al encontrar el comienzo de la etiqueta "name") // lo que haya entre las etiquetas de inicio y fin builder.append(ch[i]); } } // Lo asignamos al "name" del objeto categoría (DataSet) DataSet.setName(builder.toString()); } } } |
El código está comentado y creo que se explica bastante bien. Me consta que algunos programadores leen el contenido que se encuentra entre las etiquetas de inicio y cierre, cuando llegan a esta última etiqueta en lugar de en el método characters. Yo me he encontrado con muchos problemas haciéndolo así ya que se corre el riesgo de no conseguir leer todos los caracteres que realmente hay. La propia documentación del método lo explica:
The Parser will call this method to report each chunk of character data. SAX parsers may return all contiguous character data in a single chunk, or they may split it into several chunks; however, all of the characters in any single event must come from the same external entity so that the Locator provides useful information.
Ya tenemos casi todo lo necesario para leer el fichero XML de la API. Solamente nos queda implementar el método que parseará el archivo utilizando nuestra clase.
Esto solo es el extracto del método que lee el XML, encontraréis el código completo de la activity aquí, donde también se hace uso de la combinación de ProgressDialog y Thread para mostrar un mensaje de «cargando» mientras se leen los datos (este tema da para otro post 🙂 ).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | ... ... private Vector<ParsedCategoryDataSet> categories; private String categories_url = "http://url_de_la_api/categories.xml"; ... ... public void loadCategories() { try { // Url del archivo XML URL url = new URL(categories_url); SAXParserFactory spf = SAXParserFactory.newInstance(); SAXParser sp = spf.newSAXParser(); XMLReader xr = sp.getXMLReader(); // Utilizamos nuestro propio parseador (CategoryHandler) CategoryHandler myExampleHandler = new CategoryHandler(); xr.setContentHandler(myExampleHandler); InputSource is = new InputSource(url.openStream()); // Le indicamos la codificación para evitar errores is.setEncoding("UTF-8"); xr.parse(is); // Asignamos al vector categories los datos parseados categories = myExampleHandler.getParsedData(); } catch (Exception e) { // Ha ocurrido algún error Log.e("Ideas4All", "Error", e); } } |
Y eso es todo, no olvidéis indicar en el AndroidManifest.xml solicitar permisos de acceso a Internet.
<uses-permission android:name="android.permission.INTERNET"></uses-permission> |
Podéis encontrar la aplicación completa en github:
Código fuente de la aplicación de Ideas4All para Android
Esta muy interesante tu articulo, de seguro me servirá de mucha ayuda.
Kaixo Jon!
Antes de nada, gracias por tus aportaciones, aprendo muchísimo con ellas.
Estoy empezando a desarrollar apps para android y estoy muy interesado en parsear XML.
Me podrías recomendar cualquier publicación que desarrolle este tema más en profundidad?? (no hay problema si es en inglés).
Eskerrik Asko!!
Estan Buenisimas las tutos pero como hago para escribir un xml 😀 con los datos que tengo en el programa
Muy bueno, funciona de maravilla en android 2.X pero en las versiones 4.X falla, ¿por que se da esto? Gracias