Leer/parsear archivo XML en Android mediante SAX

febrero 19, 2012

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


CompartirTweet about this on TwitterShare on TumblrShare on FacebookShare on LinkedInShare on Google+Email this to someone

Tags ; , , , , , , , ,

4 comentarios

    juliostyle77 mar 14, 2012

    Esta muy interesante tu articulo, de seguro me servirá de mucha ayuda.

    Responder
    Basajaun jun 12, 2012

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

    Responder
    edwin toala ene 18, 2013

    Estan Buenisimas las tutos pero como hago para escribir un xml :D con los datos que tengo en el programa

    Responder
    Fox may 21, 2013

    Muy bueno, funciona de maravilla en android 2.X pero en las versiones 4.X falla, ¿por que se da esto? Gracias

    Responder

Escribe un comentario

Los comentarios son moderados y se utiliza rel="nofollow" para los enlaces.