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.
| Aspetto | SAX | DOM |
|---|---|---|
| Memoria | Costante, indipendente dalla dimensione del file | Proporzionale alla dimensione del documento |
| Modello | Push: il parser chiama i tuoi callback | Pull/albero: percorri l'albero caricato |
| Navigazione | Solo in avanti, singolo passaggio | Accesso casuale, in qualsiasi direzione |
| Modifica | Solo lettura | Lettura e scrittura |
| Ideale per | File enormi, estrazione di un sottoinsieme | Documenti 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):
| Callback | Attivato 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.
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: ogniendElementperpriceviene attivato esattamente una volta, nell'ordine in cui i libri appaiono, mai fuori sequenza. books seen : 3deriva dall'incremento di un contatore instartElementper 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 : 2viene letto dall'attributostocktramiteattr.getValue("stock"), disponibile solo all'interno distartElement. Il librob2hastock="0"e viene escluso, quindi due dei tre risultano disponibili.total price : 135.50è la somma di45.00 + 38.50 + 52.00, accumulata leggendo il testo di ogni elemento<price>al suoendElement. Leggere il testo alla chiusura dell'elemento (non incharacters) è il pattern sicuro, poichécharacterspuò consegnare il testo in più blocchi.- L'intero documento è stato passato attraverso un
ByteArrayInputStreame 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:
| Callback | Significato | Il parsing continua? |
|---|---|---|
warning(SAXParseException e) | Problema minore (ad es. un avviso DTD recuperabile) | Sì |
error(SAXParseException e) | Un errore di validità rispetto a un DTD/schema | Sì, 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());
}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.