W3docs

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.

AspectoSAXDOM
MemoriaConstante, independiente del tamaño del archivoProporcional al tamaño del documento
ModeloEmpuje: el parser llama a tus callbacksExtracción/árbol: tú recorres el árbol cargado
NavegaciónSolo hacia adelante, un único pasoAcceso aleatorio, en cualquier dirección
ModificaciónSolo lecturaLectura y escritura
Ideal paraArchivos enormes, extraer un subconjuntoDocumentos 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):

CallbackSe 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, 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.

java— editable, runs on the server

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: cada endElement para price se dispara exactamente una vez, en el orden en que aparecen los libros, nunca fuera de secuencia.
  • books seen : 3 proviene de incrementar un contador en startElement por 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 : 2 se lee del atributo stock mediante attr.getValue("stock"), disponible solo dentro de startElement. El libro b2 tiene stock="0" y queda excluido, por lo que dos de los tres califican.
  • total price : 135.50 es la suma de 45.00 + 38.50 + 52.00, acumulada leyendo el texto de cada elemento <price> en su endElement. Tomar el texto al final del elemento (no en characters) es el patrón seguro, ya que characters puede entregar el texto en múltiples fragmentos.
  • El documento completo se pasó a través de un ByteArrayInputStream y 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:

CallbackSignificado¿El parsing continúa?
warning(SAXParseException e)Problema menor (p. ej. una advertencia DTD recuperable)
error(SAXParseException e)Un error de validez contra un DTD/schemaSí, 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());
}
Advertencia

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.

Práctica

Práctica
En un manejador SAX, ¿por qué normalmente acumulas el texto en un StringBuilder en characters() y lo lees en endElement(), en lugar de usar el texto directamente dentro de characters()?
En un manejador SAX, ¿por qué normalmente acumulas el texto en un StringBuilder en characters() y lo lees en endElement(), en lugar de usar el texto directamente dentro de characters()?
Was this page helpful?