W3docs

Java XML SAX Parser

Analizza documenti XML di grandi dimensioni in Java con il parser SAX event-driven.

SAX (Simple API for XML) è il parser XML event-driven e streaming del JDK. Invece di costruire un albero in memoria come fa il DOM, SAX legge il documento una sola volta dall'inizio alla fine e ti invia eventi — "elemento aperto", "testo trovato", "elemento chiuso" — che gestisci man mano che arrivano. Poiché non mantiene mai l'intero documento, SAX analizza file di qualsiasi dimensione con una quantità costante e minima di memoria. Risiede in org.xml.sax e viene creato tramite javax.xml.parsers.SAXParserFactory, entrambi parte del JDK standard senza nulla da installare.

Questa pagina spiega come il parsing a push differisce dalla costruzione di un albero, la configurazione di factory e handler, i callback da sovrascrivere, come tenere traccia dello stato tra gli eventi, la gestione degli errori e un esempio completo eseguibile. Se sei nuovo all'XML in Java, inizia con l'introduzione all'XML; quando hai bisogno di accesso casuale o vuoi modificare un documento, usa invece il parser DOM.

Push parsing vs. costruzione di un albero

Un parser DOM legge l'intero documento e ti consegna un oggetto Document navigabile — comodo, ma deve contenere ogni nodo in memoria. SAX inverte il controllo: è il parser a guidare, chiamando metodi sul tuo handler man mano che incontra ogni parte del markup. Conservi solo lo stato che ti interessa. Il compromesso è che non puoi tornare indietro né guardare avanti — vedi ogni evento esattamente una volta, nell'ordine del documento.

AspettoSAXDOM
MemoriaCostante, indipendente dalla dimensione del fileProporzionale alla dimensione del documento
ModelloPush: il parser chiama i tuoi callbackPull/albero: percorri l'albero caricato
NavigazioneSolo in avanti, singolo passaggioAccesso casuale, in qualsiasi direzione
ModificaSolo letturaLettura e scrittura
Ideale perFile enormi, estrazione di un sottoinsiemeDocumenti piccoli/medi da modificare

La factory e l'handler

Due tipi svolgono quasi tutto il lavoro. SAXParserFactory crea un SAXParser, e si sottoclassa DefaultHandler per ricevere gli eventi. DefaultHandler implementa ogni callback come un no-op, quindi si sovrascrivono solo quelli necessari:

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);

I callback principali

Questi sono i metodi di ContentHandler che si sovrascrivono più spesso (DefaultHandler li fornisce tutti):

CallbackAttivato quando
startDocument() / endDocument()Il parsing inizia / termina
startElement(uri, localName, qName, attr)Viene letto un tag di apertura; attr contiene i suoi attributi
endElement(uri, localName, qName)Viene letto un tag di chiusura
characters(ch, start, length)Viene letto del testo — possibilmente in più blocchi
error() / fatalError()Il documento è malformato o non valido

Due aspetti mettono in difficoltà i principianti. Primo, characters non garantisce di consegnare tutto il testo di un elemento in una sola chiamata — il parser può suddividerlo, quindi lo si accumula in un StringBuilder e lo si legge in endElement. Secondo, i valori degli attributi sono disponibili solo all'interno di startElement, tramite l'argomento 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));
}

Tenere traccia dello stato tra gli eventi

Poiché SAX non fornisce un albero, sei tu a mantenere il contesto. Un pattern comune è un flag impostato in startElement e azzerato in endElement, più un buffer di testo che si azzera all'inizio di ogni elemento e si consuma alla fine dell'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 esempio completo: calcolare totali da un catalogo senza un albero

Questo programma analizza un piccolo catalogo di libri contenuto in un blocco di testo. L'handler conta i libri, conta quanti sono disponibili in magazzino (letto da un attributo stock) e somma tutti i prezzi — il tutto mentre il parser scorre il documento una sola volta. Vengono usate solo classi del JDK.

java— editable, runs on the server

Cosa osservare dall'esecuzione:

  • Le tre righe parsed: vengono stampate nell'ordine del documento — Effective Java, Clean Code, Java Concurrency in Practice — dimostrando che SAX effettua un singolo passaggio in avanti: ogni endElement per price viene attivato esattamente una volta, nell'ordine in cui i libri appaiono, mai fuori sequenza.
  • books seen : 3 deriva dall'incremento di un contatore in startElement per ogni tag <book>. Il conteggio vive nel tuo handler, non in nessun albero — SAX non ha conservato nodi, solo l'intero che hai scelto di tracciare.
  • in stock : 2 viene letto dall'attributo stock tramite attr.getValue("stock"), disponibile solo all'interno di startElement. Il libro b2 ha stock="0" e viene escluso, quindi due dei tre risultano disponibili.
  • total price : 135.50 è la somma di 45.00 + 38.50 + 52.00, accumulata leggendo il testo di ogni elemento <price> al suo endElement. Leggere il testo alla chiusura dell'elemento (non in characters) è il pattern sicuro, poiché characters può consegnare il testo in più blocchi.
  • L'intero documento è stato passato attraverso un ByteArrayInputStream e consumato una sola volta; in nessun momento il programma ha mantenuto un albero DOM. È esattamente per questo che SAX scala a file da diversi gigabyte dove il DOM esaurirebbe l'heap.

Gestione dell'XML malformato

SAX segnala i problemi tramite tre callback di ErrorHandler, tutti sovrascrivibili su DefaultHandler:

CallbackSignificatoIl parsing continua?
warning(SAXParseException e)Problema minore (ad es. un avviso DTD recuperabile)
error(SAXParseException e)Un errore di validità rispetto a un DTD/schemaSì, a meno che non si rilanci l'eccezione
fatalError(SAXParseException e)Violazione del well-formedness (markup non valido)No — il parsing si interrompe

Per impostazione predefinita parse() lancia una SAXParseException in caso di errore fatale, quindi avvolgere la chiamata in un try/catch è sufficiente per la maggior parte del codice. L'eccezione fornisce getLineNumber() e getColumnNumber(), che rendono facile individuare il markup problematico:

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());
}
Attenzione

Se il tuo handler lancia un'eccezione non controllata (ad esempio una NumberFormatException durante l'analisi di un attributo), essa si propaga direttamente fuori da parse() e interrompe lo stream. Valida o controlla i valori degli attributi all'interno del callback invece di assumere che l'input sia well-formed.

Pratica

Pratica
In un handler SAX, perché di solito si accumula il testo in uno StringBuilder in characters() e lo si legge in endElement(), invece di usare il testo direttamente dentro characters()?
In un handler SAX, perché di solito si accumula il testo in uno StringBuilder in characters() e lo si legge in endElement(), invece di usare il testo direttamente dentro characters()?
Was this page helpful?