Java XML SAX-Parser
Große XML-Dokumente in Java mit dem ereignisgesteuerten SAX-Parser streamen.
SAX (Simple API for XML) ist der ereignisgesteuerte, streamingbasierte XML-Parser des JDK. Anstatt wie DOM einen Baum im Speicher aufzubauen, liest SAX das Dokument einmal von Anfang bis Ende und schickt Ihnen Ereignisse — „Element begonnen", „Text gesehen", „Element beendet" — die Sie verarbeiten, während sie vorbeifließen. Da SAX das gesamte Dokument niemals im Speicher hält, kann es Dateien beliebiger Größe mit einem konstanten, minimalen Speicherbedarf parsen. Es befindet sich in org.xml.sax und wird über javax.xml.parsers.SAXParserFactory erstellt, beides Teil des Standard-JDK ohne zusätzliche Installation.
Diese Seite erklärt, wie sich Push-Parsing vom Baumaufbau unterscheidet, die Einrichtung von Factory und Handler, die zu überschreibenden Callbacks, die Zustandsverfolgung über Ereignisse hinweg, die Fehlerbehandlung und ein vollständiges, ausführbares Beispiel. Wenn Sie XML in Java noch nicht kennen, beginnen Sie mit der XML-Einführung; wenn Sie zufälligen Zugriff benötigen oder ein Dokument bearbeiten möchten, verwenden Sie stattdessen den DOM-Parser.
Push-Parsing vs. Baumaufbau
Ein DOM-Parser liest das gesamte Dokument und übergibt Ihnen ein navigierbares Document-Objekt — praktisch, aber es müssen alle Knoten in den Speicher passen. SAX kehrt die Kontrolle um: der Parser treibt und ruft Methoden Ihres Handlers auf, während er auf jedes Markup-Stück trifft. Sie behalten nur den Zustand, den Sie benötigen. Der Kompromiss besteht darin, dass Sie sich nicht rückwärts bewegen oder vorausschauen können — Sie sehen jedes Ereignis genau einmal, in Dokumentreihenfolge.
| Aspekt | SAX | DOM |
|---|---|---|
| Speicher | Konstant, unabhängig von der Dateigröße | Proportional zur Dokumentgröße |
| Modell | Push: Parser ruft Ihre Callbacks auf | Pull/Baum: Sie navigieren den geladenen Baum |
| Navigation | Nur vorwärts, ein Durchlauf | Zufälliger Zugriff, in jede Richtung |
| Änderung | Nur lesen | Lesen und schreiben |
| Geeignet für | Sehr große Dateien, Teilmengen extrahieren | Kleine/mittlere Dokumente, die bearbeitet werden müssen |
Die Factory und der Handler
Zwei Typen erledigen fast die gesamte Arbeit. SAXParserFactory erstellt einen SAXParser, und Sie erben von DefaultHandler, um die Ereignisse zu empfangen. DefaultHandler implementiert alle Callbacks als No-Op, sodass Sie nur die benötigten überschreiben:
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);Die wichtigsten Callbacks
Dies sind die ContentHandler-Methoden, die Sie am häufigsten überschreiben (DefaultHandler stellt sie alle bereit):
| Callback | Wird ausgelöst, wenn |
|---|---|
startDocument() / endDocument() | Das Parsen beginnt / endet |
startElement(uri, localName, qName, attr) | Ein öffnendes Tag wird gelesen; attr enthält seine Attribute |
endElement(uri, localName, qName) | Ein schließendes Tag wird gelesen |
characters(ch, start, length) | Textinhalt wird gelesen — möglicherweise in mehreren Teilen |
error() / fatalError() | Das Dokument ist fehlerhaft oder ungültig |
Zwei Tatsachen stolpern Anfänger. Erstens liefert characters nicht garantiert den gesamten Text eines Elements in einem einzigen Aufruf — der Parser kann ihn aufteilen, also sammeln Sie in einem StringBuilder und lesen Sie ihn bei endElement. Zweitens sind Attributwerte nur innerhalb von startElement über das Attributes-Argument verfügbar:
@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));
}Zustandsverfolgung über Ereignisse hinweg
Da SAX Ihnen keinen Baum liefert, müssen Sie den Kontext verwalten. Ein gängiges Muster ist ein Flag, das bei startElement gesetzt und bei endElement gelöscht wird, zusammen mit einem Textpuffer, den Sie bei jedem Elementstart zurücksetzen und am Elementende auslesen:
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());
}Ein ausgearbeitetes Beispiel: Katalog ohne Baum auswerten
Dieses Programm parst einen kleinen Buchkatalog in einem Textblock. Der Handler zählt Bücher, zählt, wie viele auf Lager sind (aus einem stock-Attribut gelesen), und summiert jeden Preis — während der Parser das Dokument nur einmal streamt. Es werden ausschließlich JDK-Klassen verwendet.
Was aus dem Ablauf zu entnehmen ist:
- Die drei
parsed:-Zeilen werden in Dokumentreihenfolge ausgegeben —Effective Java,Clean Code,Java Concurrency in Practice— was beweist, dass SAX ein einziger Vorwärtsdurchlauf ist: jedesendElementfürpricewird genau einmal ausgelöst, in der Reihenfolge, in der die Bücher erscheinen, niemals außer der Reihe. books seen : 3ergibt sich aus dem Inkrementieren eines Zählers instartElementfür jedes<book>-Tag. Der Zähler lebt in Ihrem Handler, nicht in einem Baum — SAX hat keine Knoten behalten, nur den Integer, den Sie verfolgen wollten.in stock : 2wird aus demstock-Attribut überattr.getValue("stock")gelesen, das nur innerhalb vonstartElementverfügbar ist. Buchb2hatstock="0"und ist ausgeschlossen, sodass zwei von drei qualifizieren.total price : 135.50ist die Summe von45.00 + 38.50 + 52.00, die durch Auslesen des Textes jedes<price>-Elements bei seinemendElementangesammelt wird. Den Text am Elementende (nicht incharacters) auszulesen ist das sichere Muster, dacharactersText in mehreren Teilen liefern kann.- Das gesamte Dokument wurde über einen
ByteArrayInputStreameingespeist und einmal verarbeitet; zu keinem Zeitpunkt hielt das Programm einen DOM-Baum. Das ist genau der Grund, warum SAX auf mehrere Gigabyte große Dateien skaliert, bei denen DOM den Heap erschöpfen würde.
Fehlerbehandlung bei fehlerhaftem XML
SAX meldet Probleme über drei ErrorHandler-Callbacks, die alle auf DefaultHandler überschreibbar sind:
| Callback | Bedeutung | Parsen wird fortgesetzt? |
|---|---|---|
warning(SAXParseException e) | Kleineres Problem (z. B. eine behebbare DTD-Warnung) | Ja |
error(SAXParseException e) | Ein Gültigkeitsfehler gegen eine DTD/Schema | Ja, außer Sie werfen erneut |
fatalError(SAXParseException e) | Verletzung der Wohlgeformtheit (fehlerhaftes Markup) | Nein — das Parsen stoppt |
Standardmäßig wirft parse() bei einem schwerwiegenden Fehler eine SAXParseException, sodass das Einbetten des Aufrufs in einen try/catch-Block für die meisten Fälle ausreicht. Die Exception enthält getLineNumber() und getColumnNumber(), was es einfach macht, auf das fehlerhafte Markup zu zeigen:
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());
}Wenn Ihr Handler eine unkontrollierte Exception wirft (zum Beispiel eine NumberFormatException beim Parsen eines Attributs), breitet sie sich direkt aus parse() heraus und bricht den Stream ab. Validieren oder sichern Sie Attributwerte innerhalb des Callbacks, anstatt davon auszugehen, dass die Eingabe wohlgeformt ist.