Java XML SAX Parser
Analise documentos XML grandes em Java com o parser SAX orientado a eventos.
SAX (Simple API for XML) é o parser XML orientado a eventos e streaming do JDK. Em vez de construir uma árvore na memória como o DOM faz, o SAX lê o documento uma única vez do início ao fim e empurra eventos para você — "elemento iniciado", "texto encontrado", "elemento encerrado" — que você trata conforme passam. Como nunca mantém o documento inteiro, o SAX analisa arquivos de qualquer tamanho com uma quantidade constante e mínima de memória. Ele reside em org.xml.sax e é criado por meio de javax.xml.parsers.SAXParserFactory, ambos parte do JDK padrão, sem nada a instalar.
Esta página aborda como o parsing por push difere da construção de uma árvore, a configuração de factory e handler, os callbacks que você sobrescreve, como rastrear estado entre eventos, o tratamento de erros e um exemplo completo executável. Se você é novo em XML em Java, comece com a introdução ao XML; quando precisar de acesso aleatório ou quiser editar um documento, use o parser DOM.
Parsing por push vs. construção de árvore
Um parser DOM lê o documento inteiro e entrega a você um objeto Document navegável — conveniente, mas é necessário que todos os nós caibam na memória. O SAX inverte o controle: o parser conduz, chamando métodos no seu handler conforme encontra cada parte da marcação. Você mantém apenas o estado que lhe interessa. A desvantagem é que você não pode voltar atrás nem antecipar — você vê cada evento exatamente uma vez, na ordem do documento.
| Aspecto | SAX | DOM |
|---|---|---|
| Memória | Constante, independente do tamanho do arquivo | Proporcional ao tamanho do documento |
| Modelo | Push: o parser chama seus callbacks | Pull/árvore: você percorre a árvore carregada |
| Navegação | Somente adiante, passagem única | Acesso aleatório, em qualquer direção |
| Modificação | Somente leitura | Leitura e escrita |
| Ideal para | Arquivos enormes, extração de subconjunto | Documentos pequenos/médios que você precisa editar |
A factory e o handler
Dois tipos fazem quase todo o trabalho. SAXParserFactory cria um SAXParser, e você cria uma subclasse de DefaultHandler para receber os eventos. DefaultHandler implementa cada callback como uma operação nula, então você sobrescreve apenas os que precisar:
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);Os callbacks principais
Estes são os métodos de ContentHandler que você sobrescreve com mais frequência (DefaultHandler os fornece todos):
| Callback | Disparado quando |
|---|---|
startDocument() / endDocument() | O parsing começa / termina |
startElement(uri, localName, qName, attr) | Uma tag de abertura é lida; attr contém seus atributos |
endElement(uri, localName, qName) | Uma tag de fechamento é lida |
characters(ch, start, length) | Conteúdo de texto é lido — possivelmente em vários fragmentos |
error() / fatalError() | O documento está malformado ou inválido |
Dois fatos confundem iniciantes. Primeiro, characters não garante a entrega de todo o texto de um elemento em uma única chamada — o parser pode dividi-lo, então você acumula em um StringBuilder e lê em endElement. Segundo, os valores de atributo estão disponíveis somente dentro de startElement, via o 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));
}Rastreamento de estado entre eventos
Como o SAX não fornece uma árvore, você mantém o contexto. Um padrão comum é um sinalizador definido em startElement e limpo em endElement, mais um buffer de texto que você reinicia no início de cada elemento e consome ao final do 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());
}Um exemplo prático: totalizando um catálogo sem uma árvore
Este programa analisa um pequeno catálogo de livros mantido em um bloco de texto. O handler conta os livros, conta quantos estão em estoque (lido de um atributo stock) e soma todos os preços — tudo enquanto o parser transmite o documento uma única vez. Apenas classes do JDK são usadas.
O que observar na execução:
- As três linhas
parsed:imprimem na ordem do documento —Effective Java,Clean Code,Java Concurrency in Practice— provando que o SAX é uma única passagem adiante: cadaendElementparapricedispara exatamente uma vez, na ordem em que os livros aparecem, nunca fora de sequência. books seen : 3vem do incremento de um contador emstartElementpara cada tag<book>. O contador reside no seu handler, não em nenhuma árvore — o SAX não manteve nenhum nó, apenas o inteiro que você escolheu rastrear.in stock : 2é lido do atributostockviaattr.getValue("stock"), disponível apenas dentro destartElement. O livrob2temstock="0"e é excluído, então dois dos três se qualificam.total price : 135.50é a soma de45.00 + 38.50 + 52.00, acumulada lendo o texto de cada elemento<price>em seuendElement. Ler o texto no final do elemento (não emcharacters) é o padrão seguro, poischaracterspode entregar o texto em vários fragmentos.- O documento inteiro foi alimentado por um
ByteArrayInputStreame consumido uma única vez; em nenhum momento o programa manteve uma árvore DOM. É exatamente por isso que o SAX escala para arquivos de vários gigabytes onde o DOM esgotaria o heap.
Tratamento de XML malformado
O SAX relata problemas por meio de três callbacks de ErrorHandler, todos sobrescrevíveis em DefaultHandler:
| Callback | Significado | Parsing continua? |
|---|---|---|
warning(SAXParseException e) | Problema menor (ex.: um aviso DTD recuperável) | Sim |
error(SAXParseException e) | Erro de validade contra um DTD/schema | Sim, a menos que você relance |
fatalError(SAXParseException e) | Violação de boa formação (marcação quebrada) | Não — o parsing para |
Por padrão, parse() lança uma SAXParseException em um erro fatal, então envolver a chamada em um try/catch é suficiente para a maioria dos casos. A exceção carrega getLineNumber() e getColumnNumber(), o que facilita apontar a marcação problemática:
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());
}Se o seu handler lançar uma exceção não verificada (por exemplo, uma NumberFormatException ao analisar um atributo), ela se propagará diretamente para fora de parse() e abortará o stream. Valide ou proteja os valores de atributo dentro do callback em vez de presumir que a entrada está bem formada.