Java XML SAX Parser
Analiza documentos XML grandes en Java con el parser SAX orientado a eventos.
SAX (Simple API for XML) es el parser XML orientado a eventos y de streaming del JDK. En lugar de construir un árbol en memoria como hace DOM, SAX lee el documento una sola vez de principio a fin y te envía eventos — "elemento iniciado", "texto visto", "elemento terminado" — que manejas a medida que van pasando. Como nunca guarda el documento completo, SAX parsea archivos de cualquier tamaño con una cantidad constante y mínima de memoria. Reside en org.xml.sax y se crea a través de javax.xml.parsers.SAXParserFactory, ambas parte del JDK estándar sin necesidad de instalar nada.
Esta página cubre cómo el parsing por empuje difiere de construir un árbol, la configuración de fábrica y manejador, los callbacks que se sobreescriben, cómo rastrear el estado entre eventos, el manejo de errores y un ejemplo completo ejecutable. Si eres nuevo en XML en Java, comienza con la introducción a XML; cuando necesites acceso aleatorio o quieras editar un documento, usa el parser DOM.
Parsing por empuje vs. construir un árbol
Un parser DOM lee el documento completo y te entrega un objeto Document navegable — conveniente, pero debe caber cada nodo en memoria. SAX invierte el control: el parser dirige, llamando métodos en tu manejador a medida que encuentra cada pieza de marcado. Solo conservas el estado que te interesa. La desventaja es que no puedes retroceder ni mirar hacia adelante — ves cada evento exactamente una vez, en orden de documento.
| Aspecto | SAX | DOM |
|---|---|---|
| Memoria | Constante, independiente del tamaño del archivo | Proporcional al tamaño del documento |
| Modelo | Empuje: el parser llama a tus callbacks | Extracción/árbol: tú recorres el árbol cargado |
| Navegación | Solo hacia adelante, un único paso | Acceso aleatorio, en cualquier dirección |
| Modificación | Solo lectura | Lectura y escritura |
| Ideal para | Archivos enormes, extraer un subconjunto | Documentos pequeños/medianos que necesitas editar |
La fábrica y el manejador
Dos tipos hacen casi todo el trabajo. SAXParserFactory crea un SAXParser, y tú subclasificas DefaultHandler para recibir los eventos. DefaultHandler implementa cada callback como una operación vacía, por lo que solo sobreescribes los que necesitas:
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setNamespaceAware(true); // optional: report namespace URIs
SAXParser parser = factory.newSAXParser();
DefaultHandler handler = new DefaultHandler() {
@Override
public void startElement(String uri, String localName, String qName, Attributes attr) {
System.out.println("start <" + qName + ">");
}
};
parser.parse(new File("data.xml"), handler);Los callbacks principales
Estos son los métodos de ContentHandler que sobreescribes con más frecuencia (DefaultHandler los proporciona todos):
| Callback | Se dispara cuando |
|---|---|
startDocument() / endDocument() | El parsing comienza / termina |
startElement(uri, localName, qName, attr) | Se lee una etiqueta de apertura; attr contiene sus atributos |
endElement(uri, localName, qName) | Se lee una etiqueta de cierre |
characters(ch, start, length) | Se lee contenido de texto — posiblemente en varios fragmentos |
error() / fatalError() | El documento está mal formado o es inválido |
Dos hechos confunden a los principiantes. Primero, characters no garantiza entregar todo el texto de un elemento en una sola llamada — el parser puede dividirlo, por lo que acumulas en un StringBuilder y lo lees en endElement. Segundo, los valores de atributo están disponibles solo dentro de startElement, a través del argumento Attributes:
@Override
public void startElement(String uri, String localName, String qName, Attributes attr) {
String id = attr.getValue("id"); // by name
for (int i = 0; i < attr.getLength(); i++) // or by index
System.out.println(attr.getQName(i) + "=" + attr.getValue(i));
}Rastrear el estado entre eventos
Como SAX no te da ningún árbol, tú mantienes el contexto. Un patrón común es una bandera que se activa en startElement y se limpia en endElement, más un buffer de texto que reseteas al inicio de cada elemento y consumes al final del elemento:
private final StringBuilder text = new StringBuilder();
@Override public void startElement(String u, String l, String q, Attributes a) {
text.setLength(0); // begin collecting fresh text
}
@Override public void characters(char[] ch, int start, int len) {
text.append(ch, start, len); // text may arrive in pieces
}
@Override public void endElement(String u, String l, String q) {
if (q.equals("title")) System.out.println("title = " + text.toString().trim());
}Un ejemplo práctico: contabilizar un catálogo sin árbol
Este programa parsea un pequeño catálogo de libros almacenado en un bloque de texto. El manejador cuenta libros, cuenta cuántos están en stock (leído de un atributo stock) y suma cada precio — todo mientras el parser transmite el documento una sola vez. Solo se usan clases del JDK.
Lo que debes aprender de la ejecución:
- Las tres líneas
parsed:se imprimen en orden de documento —Effective Java,Clean Code,Java Concurrency in Practice— probando que SAX es un único paso hacia adelante: cadaendElementparapricese dispara exactamente una vez, en el orden en que aparecen los libros, nunca fuera de secuencia. books seen : 3proviene de incrementar un contador enstartElementpor cada etiqueta<book>. El conteo reside en tu manejador, no en ningún árbol — SAX no conservó ningún nodo, solo el entero que elegiste rastrear.in stock : 2se lee del atributostockmedianteattr.getValue("stock"), disponible solo dentro destartElement. El librob2tienestock="0"y queda excluido, por lo que dos de los tres califican.total price : 135.50es la suma de45.00 + 38.50 + 52.00, acumulada leyendo el texto de cada elemento<price>en suendElement. Tomar el texto al final del elemento (no encharacters) es el patrón seguro, ya quecharacterspuede entregar el texto en múltiples fragmentos.- El documento completo se pasó a través de un
ByteArrayInputStreamy se consumió una vez; en ningún momento el programa mantuvo un árbol DOM. Esa es exactamente la razón por la que SAX escala a archivos de varios gigabytes donde DOM agotaría el heap.
Manejo de XML mal formado
SAX reporta problemas a través de tres callbacks de ErrorHandler, todos sobreescribibles en DefaultHandler:
| Callback | Significado | ¿El parsing continúa? |
|---|---|---|
warning(SAXParseException e) | Problema menor (p. ej. una advertencia DTD recuperable) | Sí |
error(SAXParseException e) | Un error de validez contra un DTD/schema | Sí, a menos que vuelvas a lanzar |
fatalError(SAXParseException e) | Violación de buena formación (marcado roto) | No — el parsing se detiene |
Por defecto, parse() lanza una SAXParseException ante un error fatal, por lo que envolver la llamada en un try/catch es suficiente para la mayoría del código. La excepción incluye getLineNumber() y getColumnNumber(), lo que facilita señalar el marcado problemático:
try {
parser.parse(new File("data.xml"), handler);
} catch (SAXParseException e) {
System.err.println("bad XML at line " + e.getLineNumber()
+ ", column " + e.getColumnNumber() + ": " + e.getMessage());
}Si tu manejador lanza una excepción no verificada (por ejemplo una NumberFormatException al parsear un atributo), se propaga directamente fuera de parse() y aborta el stream. Valida o protege los valores de atributo dentro del callback en lugar de asumir que la entrada está bien formada.