W3docs

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.

AspektSAXDOM
SpeicherKonstant, unabhängig von der DateigrößeProportional zur Dokumentgröße
ModellPush: Parser ruft Ihre Callbacks aufPull/Baum: Sie navigieren den geladenen Baum
NavigationNur vorwärts, ein DurchlaufZufälliger Zugriff, in jede Richtung
ÄnderungNur lesenLesen und schreiben
Geeignet fürSehr große Dateien, Teilmengen extrahierenKleine/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):

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

java— editable, runs on the server

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: jedes endElement für price wird genau einmal ausgelöst, in der Reihenfolge, in der die Bücher erscheinen, niemals außer der Reihe.
  • books seen : 3 ergibt sich aus dem Inkrementieren eines Zählers in startElement fü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 : 2 wird aus dem stock-Attribut über attr.getValue("stock") gelesen, das nur innerhalb von startElement verfügbar ist. Buch b2 hat stock="0" und ist ausgeschlossen, sodass zwei von drei qualifizieren.
  • total price : 135.50 ist die Summe von 45.00 + 38.50 + 52.00, die durch Auslesen des Textes jedes <price>-Elements bei seinem endElement angesammelt wird. Den Text am Elementende (nicht in characters) auszulesen ist das sichere Muster, da characters Text in mehreren Teilen liefern kann.
  • Das gesamte Dokument wurde über einen ByteArrayInputStream eingespeist 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:

CallbackBedeutungParsen wird fortgesetzt?
warning(SAXParseException e)Kleineres Problem (z. B. eine behebbare DTD-Warnung)Ja
error(SAXParseException e)Ein Gültigkeitsfehler gegen eine DTD/SchemaJa, 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());
}
Warnung

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.

Übungen

Übung
Warum sammelt man Text in einem SAX-Handler typischerweise in einem StringBuilder in characters() und liest ihn in endElement(), anstatt ihn direkt in characters() zu verwenden?
Warum sammelt man Text in einem SAX-Handler typischerweise in einem StringBuilder in characters() und liest ihn in endElement(), anstatt ihn direkt in characters() zu verwenden?
Was this page helpful?