W3docs

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.

AspectoSAXDOM
MemóriaConstante, independente do tamanho do arquivoProporcional ao tamanho do documento
ModeloPush: o parser chama seus callbacksPull/árvore: você percorre a árvore carregada
NavegaçãoSomente adiante, passagem únicaAcesso aleatório, em qualquer direção
ModificaçãoSomente leituraLeitura e escrita
Ideal paraArquivos enormes, extração de subconjuntoDocumentos 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):

CallbackDisparado 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.

java— editable, runs on the server

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: cada endElement para price dispara exatamente uma vez, na ordem em que os livros aparecem, nunca fora de sequência.
  • books seen : 3 vem do incremento de um contador em startElement para 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 atributo stock via attr.getValue("stock"), disponível apenas dentro de startElement. O livro b2 tem stock="0" e é excluído, então dois dos três se qualificam.
  • total price : 135.50 é a soma de 45.00 + 38.50 + 52.00, acumulada lendo o texto de cada elemento <price> em seu endElement. Ler o texto no final do elemento (não em characters) é o padrão seguro, pois characters pode entregar o texto em vários fragmentos.
  • O documento inteiro foi alimentado por um ByteArrayInputStream e 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:

CallbackSignificadoParsing continua?
warning(SAXParseException e)Problema menor (ex.: um aviso DTD recuperável)Sim
error(SAXParseException e)Erro de validade contra um DTD/schemaSim, 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());
}
Aviso

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.

Prática

Prática
Em um handler SAX, por que você normalmente acumula texto em um StringBuilder em characters() e o lê em endElement(), em vez de usar o texto diretamente dentro de characters()?
Em um handler SAX, por que você normalmente acumula texto em um StringBuilder em characters() e o lê em endElement(), em vez de usar o texto diretamente dentro de characters()?
Was this page helpful?