martes, 8 de julio de 2008

Java y XML: SAX (I)

Tras lo comentado en el post Java y XML, me gustaría centrar la atención sobre el parseador SAX (Simple API for XML). Lo comento porque SAX fue el primer parseador de XML adoptado como API para XML en Java. Como se podrá ver a lo largo del post, SAX deja mucho que desear con respecto a otras APIs para XML como las basadas en DOM.

Como dije en el post anterior, SAX es un parseador de acceso serie y basado en eventos. Es decir, va recorriendo el xml poco a poco (en serie) y va generando eventos conforme va encontrando determinadas partes del xml, como el principio de etiquieta, el fin de etiqueta, texto, ....

A continuación expongo un ejemplo de uso de SAX. Se trata de dos clases: ManejadorEjemplo y ProcesaXML.

ManejadorEjemplo

Se trata de la clase que va a procesar cada evento que lance el procesador SAX. Basta con heredar del manejador por defecto de SAX DefaultHandler y sobreescribir los métodos correspondiente a los eventos deseados. En este caso he sobreescrito los más comunes:

  • startDocument: se produce al comenzar el procesado del documento xml.
  • endDocument: se produce al finalizar el procesado del documento xml.
  • startElement: se produce al comenzar el procesado de una etiqueta xml. Es aquí donde se leen los atributos de las etiquetas.
  • endElement: se produce al finalizar el procesado de una etiqueta xml.
  • characters: se produce al encontrar una cadena de texto.

Cada vez que el parseador encuentre una etiqueta se va a llamar a startElement. Sea la etiqueta que sea. Recibiremos como parámetro el nombre de la etiqueta pero no la etiqueta padre. Por tanto si tenemos etiquetas anidadas con el mismo nombre, debemos almacenar en un objeto persistente (al menos durante el procesado del documento xml) la ruta de etiquetas por la que vamos procesando. Es decir, un objeto que almacene el de estado en que se encuentra el procesado del documento.

Este inconveniente se acentúa al leer los valores de las etiquetas ya que al método characters se llama sin indicar el element al que se está accediendo. Para saber a qué etiqueta pertenece el texto debíamos haberlo marcado previamente al pasar por el startElement correspondiente. En este caso es imprescindible el objeto que nos marca el estado.

Por este tipo de cosas digo que este API es arcano y anticuado. Cuando veamos DOM, veremos que se puede tener acceso aleatorio a cualquier parte del documento XML, lo cuál nos evita toda esta parafernalia.

ManejadorEjemplo.java

package es.latascadexela.xml.sax;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

/**
* Manejador de eventos SAX de ejemplo.
*
* @author Xela
*
*/
public class ManejadorEjemplo extends DefaultHandler{

   @Override
   public void startDocument() throws SAXException {
      System.out.println("\nPrincipio del documento...");
   }

   @Override
   public void endDocument() throws SAXException {
      System.out.println("\nFin del documento...");
   }

   @Override
   public void startElement(String uri, String localName, String name,
         Attributes attributes) throws SAXException {
      System.out.println("\nProcesando etiqueta...");
      System.out.println("\tNamespace uri: "+uri);
      System.out.println("\tNombre: "+localName);
      System.out.println("\tNombre con prefijo: "+name);
      
      //Recorremos los atributos
      System.out.println("\tProcesando "+attributes.getLength()+" atributos...");
      for(int i=0;i<attributes.getLength();i++){
         System.out.println("\t\tNombre: "+attributes.getQName(i));
         System.out.println("\t\tValor: "+attributes.getValue(i));
      }
      
      // También podemos obtener los atributos por nombre
      String valorId = attributes.getValue("id");
      if(valorId!=null){
         System.out.println("\tId: "+valorId);
      }

   }
   
   @Override
   public void characters(char[] ch, int start, int length)
         throws SAXException {
      System.out.println("\nProcesando texto dentro de una etiqueta... ");
      System.out.println("\tTexto: "+String.valueOf(ch, start, length));
   }

   @Override
   public void endElement(String uri, String localName, String name)
         throws SAXException {
      System.out.println("\nFin de etiqueta...");
      System.out.println("\tNamespace uri: "+uri);
      System.out.println("\tNombre: "+localName);
      System.out.println("\tNombre con prefijo: "+name);
   }

}


Como se puede observar la clase este ejemplo es muy tonto. Simplemente voy poniendo mensajes por la salida estándar conforme van saltando eventos.

ProcesaXML

Se trata del método main en el que se procesa el xml mediante nuestro handler ManejadorEjemplo.

ProcesaXML.java

package es.latascadexela.xml.sax;

import java.io.FileInputStream;
import java.io.IOException;

import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;

/**
* Clase que procesa un XML de ejemplo mediante el handler SAX ManejadorEjemplo
*
* @author Xela
*
*/
public class ProcesaXML {

   public static void main(String[] args) {
      
      try {
         // Creamos la factoria de parseadores por defecto
         XMLReader reader = XMLReaderFactory.createXMLReader();
         // Añadimos nuestro manejador al reader
         reader.setContentHandler(new ManejadorEjemplo());         
         // Procesamos el xml de ejemplo
         reader.parse(new InputSource(new FileInputStream("/ruta_hasta_el_fichero/fichero.xml")));
      } catch (SAXException e) {
         e.printStackTrace();
      } catch (IOException e) {
         e.printStackTrace();
      }

   }

}

Ejecutando nuestro parseador

Si realizamos el procesado de un xml de ejemplo como el siguiente:


<?xml version="1.0" encoding="UTF-8"?>
<etiquetaPrincipal xmlns:xela="http://www.latascadexela.es" >
   <etiquetaHija id="1" atributo1="valorAtributo1" atributo2="valorAtributo2">
      Texto dentro de la etiqueta hija
   </etiquetaHija>
   <!-- Comentario -->
   <xela:etiquetaConNamespace descripcion="etiqueta con un namespace"/>   
</etiquetaPrincipal>

podemos observar que obtenemos el siguiente resultado:


Principio del documento...

Procesando etiqueta...
   Namespace uri:
   Nombre: etiquetaPrincipal
   Nombre con prefijo: etiquetaPrincipal
   Procesando 0 atributos...

Procesando texto dentro de una etiqueta...
   Texto:
   

Procesando etiqueta...
   Namespace uri:
   Nombre: etiquetaHija
   Nombre con prefijo: etiquetaHija
   Procesando 3 atributos...
      Nombre: id
      Valor: 1
      Nombre: atributo1
      Valor: valorAtributo1
      Nombre: atributo2
      Valor: valorAtributo2
   Id: 1

Procesando texto dentro de una etiqueta...
   Texto:
      Texto dentro de la etiqueta hija

Procesando texto dentro de una etiqueta...
   Texto:
   

Fin de etiqueta...
   Namespace uri:
   Nombre: etiquetaHija
   Nombre con prefijo: etiquetaHija

Procesando texto dentro de una etiqueta...
   Texto:
   

Procesando texto dentro de una etiqueta...
   Texto:
   

Procesando etiqueta...
   Namespace uri: http://www.latascadexela.es
   Nombre: etiquetaConNamespace
   Nombre con prefijo: xela:etiquetaConNamespace
   Procesando 1 atributos...
      Nombre: descripcion
      Valor: etiqueta con un namespace

Fin de etiqueta...
   Namespace uri: http://www.latascadexela.es
   Nombre: etiquetaConNamespace
   Nombre con prefijo: xela:etiquetaConNamespace

Procesando texto dentro de una etiqueta...
   Texto:    

Procesando texto dentro de una etiqueta...
   Texto:


Fin de etiqueta...
   Namespace uri:
   Nombre: etiquetaPrincipal
   Nombre con prefijo: etiquetaPrincipal

Fin del documento...


Como se puede observar aparece mucho la llamada al método characters. No sólo justo dentro de las etiquetas sino también como separación entre una etiqueta y otra. Se puede deducir que el control del elemento que se está procesando es imprescindible para casi cualquier XML.

Por otro lado, el comentario xml ni lo ha detectado. SAX2 ya tiene un juego de eventos más completo y se pueden detectar comentarios, CDATA, etc.

Una vez visto esto, como resumen podemos enumerar las ventajas e inconvenientes de usar SAX:

Ventajas
  • No consume mucha memoria ya que va procesando el documento en serie
Inconvenientes
  • Es poco intuitivo para el desarrollador.
  • Requiere una serie de controles de estado adicionales que complican el procesado.
  • No permite construir XML. Sólo permite parsearlos.
  • No permite el acceso aleatorio al documento xml.

Como conclusión final, decir que sólo se puede justificar su uso desde el punto de vista del rendimiento al tratar XMLs grandes, ya que al acceder en serie al xml no necesita mantener en memoria todo el XML o representación equivalente, como hace DOM. En el resto de los casos, es mejor usar alguna API basada en DOM.

Supongo que la gente de saxproject.org tendrá una opinión muy diferente. Simplemente remarcar que lo aquí escrito es tan sólo mi opinión, la cuál está fundamentada en mi experiencia personal con Java y el mundo XML. Espero que te haya sido útil.

Más en la segunda parte.

15 comentarios:

Os. dijo...

Xela, Tnx a lot!
Muy buena tu explicacion y el ejemplo super claro...
Now...Cheers, salud, chinchin, campay....

Tina dijo...

Gracias por la explicación me ha ayudado mucho a comprender el parseo de xml's, gracias!

Anónimo dijo...

Hola
Muy buena la información pero me gustaría hacer una cosa con SAX que no se si se puede y quería saber si alguien me podría echar un cable.
Quiero indexar un fichero XML con Lucene2.4 y para ello uso SAX para leerlo e irlo indexando.
El problema que creo que tengo es que la función startElement "no distingue" entre la etiquetaPrincipal y la etiquetaHija por lo que me hace un indice de cada etiqueta cuando solo me debría crear un índice por cada etiquetaPrincipal y dentro de dicha etiqueta crearme otros indices a un 2º nivel con las etiquetasHija.
¿Alguien sabría decirme si se puede hacer esto que quiero hacer?
Saludos

Xela dijo...

Hola "Anónimo",

veo que usas Lucene. Muy buena elección. Yo también uso Lucene en el curro y es pilar principal de nuestros desarrollos, ya que tener la información indexada nos evitas pesadas consultas en base de datos. Y además va muy rápido.

En cuanto a tu pregunta, lo que tienes que hacer es controlar el estado del procesado. Puedes ver ejemplos en el siguiente post de SAX. No sé si sabes que el parseo es secuencial, por tanto siempre pasarás antes por la etiqueta padre que por las hijas. Cuando pases por la etiqueta padre, guarda esa información en un atributo de la clase, por ejemplo un entero que te diga el nivel de profundidad en que te encuentras. Cuando llegues a la etiqueta hija, tan sólo tienes que leer el nivel y sabrás dónde estás.

En el evento endElement puedes decrementar de nuevo el nivel de profundidad, ya que habrás acabado de parsear un nivel.

No sé esto responde a tu pregunta o me he ido por los cerros de Úbeda. :-p

Saludos

Antonio dijo...

Hola Xela

Soy el "Anónimo" del anterior mensaje.

El objetivo que tengo es con SAX conseguir leer el fichero XML e irlo indexando con Lucene 2.4 (q es la última versión).

Los XML que "querría" leer me gustaría que no tuvieran una estructura fija como en el ejemplo SAX2 por eso me decante por el código y el ejemplo del SAX1.

El problema que tengo es que la función startElement me detecta cada inicio de etiqueta XML, es decir, en este ejemplo:

< ? xml version="1.0" encoding="UTF-8" ? >
< cliente>
< persona>
< nombre>Antonio< /nombre>
< apellido>Ruiz< /apellido>
< edad>22< /edad>
< /persona>
< persona>
< nombre>Bill< /nombre>
< apellido>Gates< /apellido>
< edad>46< /edad>
< /persona>
< /cliente>

me crea un "indice lucene" por cada eiqueta ya sea cliente, persona, nombre...etc cuando realmente solo quiero que me haga un "indice lucene" en cada persona.
No he encontrado el modo de hacerlo para que al detectarme una etiqueta de inicio < persona> me cree el indice ya que puse el código de crear el writer e hice posteriormente una funcion crear_writer que llamo desde dentro de la función stratelement por lo que a cada inicio de etiqueta me crea unnuevo indice.

Entiendo lo que me comentas del parseado secuencial pero no sabría como hacerlo ya que x ej en el XML que te he puesto antes pues tengo 3 atributos xo si hubiera un 4º no funcionaría no ??
¿Y que pasaría si por ejemplo si hubiera más de un nivel ? Por ejemplo que hubiera un atributo domicilio y dentro de el la calle, el num y el piso..me detectaría que esta a otro nivel más bajo?

GRACIAS por la ayuda.

P.D.: Te dejo mi correo (antonio_ruiz_cebrian@yahoo.es) por si no quieres que te siga preguntando "dudas más personales" a través de los comentarios del post y prefieres que hablemos por correo.

Xela dijo...

Antonio,

prefiero usar los comentarios. Para eso está el blog, para que todos aportemos. Puede que haya usuarios que tengan un problema parecido al tuyo y le ayuden nuestros comentarios.

En cuanto a tu problema, creo que estás mezclando las cosas. Por un lado, me dices que creas un índice por cliente. Esto la verdad es que me choca. ¿No tiene más sentido crear un único índice en el que haya un document(registro de lucene) por cada cliente?

En cualquier caso, creo que te aclararás mejor si separas por un lado el parseo de xml y por otro la creación del índice.

Mi consejo es que te mires el siguiente post, y al igual que yo me creo el la clase Libro, create tú la clase Cliente. En el parseador, cada vez que acabes de procesar una etiqueta llamada "cliente", metes el objeto cliente ya generado en una lista y así con cada cliente.

Una vez acabes de parsear el xml tendrás como resultado una lista de objetos clientes (List<Cliente>).

Recorre la lista y empieza a indexar clientes.

Te resultará más fácil si separas las dos cosas. Si después de haberlo hecho separado lo quieres unir, puedes hacerlo, pero creo que es más claro y lleva a menos error hacer cada cosa por su lado.

Antonio dijo...

Hola Xela.
Tras muchas pruebas con tus 2 ejemplos, errores de novato con lucene 2.4, chapuzas, horas perdidas...etc consegui indexar un fichero XML con Lucene 2.4 leyendo este a través de Java.
Ahora me surge la duda...¿Como recupero los datos? He leido que existe un searcher y tal...xo poco mas...
¿Hay que leerlo desde Java y mostrarlo por pantalla o se puede mostrar en un formulario HTML por ejemplo tras introducir una palabra de busqueda en un Textfield?
Saludos

Xela dijo...

Antonio,

Lucene, tanto la indexación como búsquedas, tiene suficiente entidad como para no poder explicarlo todo en un simple comentario. Además, Lucene no es el objeto de este post.

Probablemente en el futuro escriba algo sobre Lucene, pero lo más seguro es que cuando lo haga, ya lo habrás descubierto por tí mismo y no lo necesitarás.

Aún así, me sabe mal no ayudarte, porque veo que andas bastante perdido ;-). Y no te lo tomes a mal, que no lo digo con ninguna mala intención.

He encontrado un ejemplo de indexación y búsqueda. Te puede servir. Aquí tienes otro enlace a un tutorial de Lucene.

En cuanto a las consultas sobre el índice te vendrá bien la documentación oficial sobre la sintaxis de búsqueda.

Por último te dejo un enlace a una herramienta que te permitirá explorar el índice generado y probar consultas. Se llama Luke.

Espero que esto te ayude.

Antonio dijo...

Xela por supuesto que no me lo tomo a mal.

Ando más perdido que un mono en una cocina....quizás es lo que pasa cuando quieres echar un vistazo a cosas amplísimas como SAX y Lucene pensando que cambiara un poco la sintaxis (como cuando estás programando en Java y quieres echar un ojo a la sintaxis del lenguaje C ) pero veo que esto es bastante más amplio de lo que esperaba y quizás sin dedicarle el tiempo necesario no pueda captar los conceptos básicos y elementos imprescindibles para entender ambas cosas.

El ejemplo que me pones debe ser de una versión anterior a la 2.4 (quizás no debí de coger la última versión de Lucene) ya que me dice que algunos métodos están "deprecated".

El Luke si que le he estado usando de ahí que supiera que no me indexaba bien....y aunque ahora ya indexa bien he de decirte que el xml que me indexo es muy básico.

Pues nada, no te preguntare más dudas de Lucene en este post para no desviar el tema de los comentarios del post.

Te dejo mi correo antonio_ruiz_cebrian@yahoo.es para cuando hagas un post de Lucene me avises aunque echare un ojo al blog de vez en cuando para ver si le has publicado.

GRACIAS por la ayuda y tu disposición.

Saludos

Antonio dijo...

Xela me ha surgido una duda en la utilización de SAX que no se si me podrás ayudar a resolver o me sabrán resolver en el enlace donde encontre el codigo.

Veo en este enlace http://www.webtaller.com/construccion/lenguajes/java/lecciones/transformar_xml_en_html_utilizando_xsl_y_api_java_xml_transform.php
que se puede hacer a traves de Java la "union" del XML y el XSl.

Al compilar Xalan para la generacion de los HTML yo le puedo pasar parametros del siguiente modo (esto es el comando que meto en MSDOS)
C:\xalan-j_2_7_1>java -classpath “xalan.jar;serializer.j
ar;xml-apis.jar;xercesImpl.jar” org.apache.xalan.xslt.Process -IN ficheroxml.xml -XSL ficheroxsl.xsl -OUT salida.html -PARAM nombreparametro valor

¿Como puedo pasar parametros usando el codigo que hay ahi que es muy similar al que uso yo?

He probado añadiendolo detras de los nombres en las siguientes lineas pero da error o genera ficheros lalamdos “glossary.fo -PARAM nombreparametro valor”

Document documento = builder.parse(archivoXML)+ “-PARAM nombreparametro valor”;

StreamSource stylesource = new StreamSource(archivoXSLT)+”-PARAM nombreparametro valor”;

StreamResult result = new StreamResult(”glossary.fo”)+”-PARAM nombreparametro valor”;3

Espero que me puedas ayudar.

Saludos

Pepinho dijo...

Hola Xela,

he estado leyendo, bueno y continuo leyendo tus post sobre java y xml, y tan sólo te quería agradecer tú ayuda con este tema. Me están siendo de una grandísima ayuda.

Muchas gracias por todo, un saludo.

Verónica dijo...

Hola!..

aunque el post ya lleva su tiempo, quería agradecerte por el buen ejemplo que tienes.. Me ha servido bastante. Aún sigo tratando de entender más acerca del uso del DOM, pero este ejemplo me ha sido bastante útil. Muchas gracias

Lucky javi dijo...

hola, excelente tu explicación, espero me puedas ayudar he estado intentando leer archivos de xml pero tienen namespaces y prefijos, es decir para la factura electronica que tiene una forma parecida




necesito leer el atributo rfc y usarlo en otra parte de una aplicación, pero no puedo obtenerlo, me puedes dar algún consejo

de antemano muchas gracias

David Becerra Montellano dijo...

Muchas gracias por la información, me fue de utilidad. Me funciono correctamente. Saludos...

Jon Inazio Sanchez Martinez dijo...

Para todo aquel que tiene problemas con los elementos CDATA. Si no se sigue lo que dice el compañero en este mensaje http://stackoverflow.com/a/8325741/828551, los elementos CDATA no serán leidos correctamente.