SAX Parser

Martin Kompf

SAX (Simple API for XML) stellt Methoden zum schnellen Parsen von XML bereit. Der Artikel zeigt eine Anwendung von SAX zur Auswertung von Daten des OpenStreetMap Projektes.

Der Artikel XML Stream Reader stellt mit dem XML Stream Reader aus der JAXP API eine Variante zum Parsen von XML Daten vor. An dieser Stelle soll nun eine weitere Java-Parser-API vorgestellt werden: Die Simple API for XML (SAX). Als Beispiel soll diesmal die Aufgabe stehen, alle Straßen- und Wegenamen einer bestimmten Region aus XML-Daten des OpenStreetMap Projektes zu extrahieren.

OpenStreetMap XML

OpenStreetMap (OSM) ist ein faszinierendes Projekt, bei dem Tausende Freiwillige an der Kartierung der Erde mitarbeiten. Die Karten und zugehörigen Daten stehen lizenzkostenfrei unter einer Creative Common Lizenz zur Verfügung. OSM liefert nicht nur fertige Karten, sondern auch die zugrundeliegenden XML-Daten. Sie lassen sich ebenfalls von der OSM Site exportieren. Dazu wählt man den Reiter Export und selektiert dort OpenStreetMap XML Data und den zu exportierenden Bereich. Ihn darf man nicht zu groß wählen, da die Datenmenge sonst recht schnell mehrere hundert Megabyte betragen kann.

Oder man programmiert gleich eine kleine Java Funktion, die unter Verwendung der OSM API ein Gebiet exportiert und in einer XML Datei speichert:

import java.io.*;
import java.net.URL;
import java.util.*;

import javax.xml.parsers.*;

import org.xml.sax.*;
import org.xml.sax.helpers.DefaultHandler;

public class OsmSaxNames {

  public void downloadSample(File file) throws IOException {
    URL downloadUrl = new URL(
      "http://api.openstreetmap.org/api/0.6/map?bbox=11.62973,52.12236,11.63736,52.12853");
    InputStream in = downloadUrl.openStream();
    OutputStream out = new FileOutputStream(file);
    byte[] buffer = new byte[10000];
    try {
      int len = in.read(buffer);
      while (len > 0) {
        out.write(buffer, 0, len);
        len = in.read(buffer);
      }
    } finally {
      out.close();
      in.close();
    }
  }

Der URL-Parameter bbox spezifiziert das zu exportierende Gebiet - hier die Innenstadt von Magdeburg. Sonst bietet die Funktion nicht viel bemerkenswertes außer der Tatsache, dass konsequent Byte-Streams für Input und Output verwendet werden. Hier stattdessen Character-basierte Reader und Writer einzusetzen wäre eine schlechte Idee, da diese eine Umkodierung des Zeichenstroms entsprechend der eingestellten Locale vornehmen. XML-Dateien benutzen aber mitunter eine andere Zeichenkodierung, die in der ersten Zeile, der XML-Deklaration, angegeben ist:

<?xml version="1.0" encoding="UTF-8"?>
<osm version="0.6">
  <bounds minlat="..."/>
  <node id="..."/>
  <node id="..."/>
  
  <way id="...">
    <nd ref="..."/>
    <nd ref="..."/>
    <tag k="name" v="Schleinufer"/>
    <tag k="old_name" v="Fürstenufer"/>
    <tag k="oneway" v="yes"/>
  </way>

  <way id="...">
  </way>
  
  <relation id="...">
  </relation>
</osm>

Die Datei ist also in UTF-8 kodiert, das ist auch der Standard, falls die Encoding-Deklaration fehlt. Ein Blick in die heruntergeladene Datei, die hier nur abgekürzt wiedergegeben ist, zeigt die wesentliches Elemente der OSM-Daten: node, way und relation. Uns interessieren gemäß Aufgabenstellung nur die way-Elemente, die Linienzüge beschreiben, wie zum Beispiel Straßen und Wege. Diese Linienzüge sind mit Tags versehen, die im XML als Kindelement tag auftauchen. Das Tag mit dem Attribut k="name" enthält als Attributwert v den gesuchten Namen. Mit dieser Information können wir das Programmieren der Applikation beginnen, weitere Informationen zum OSM Datenformat findet der Interessierte im OSM Wiki.

SAX: Parser und Handler

Das Parsen von XML mit SAX besteht im wesentichen aus den Schritten:

  1. Erzeugen eines SAXParser mittels der SAXParserFactory.
  2. Erzeugen eines anwendungspezifischen Handlers, der von DefaultHandler abgeleitet ist.
  3. Aufruf der parse Methode des Parsers, die den Handler und die zu parsende XML-Datei als Parameter übergeben bekommt.
  public SortedSet<String> findWayNames(File file) throws ParserConfigurationException,
      SAXException, IOException {
    SAXParserFactory parserFactory = SAXParserFactory.newInstance();
    SAXParser parser = parserFactory.newSAXParser();

    OsmNamesHandler handler = new OsmNamesHandler();
    parser.parse(file, handler);
    
    return handler.getNames();
  }

Während sich die parse Methode durch den XML-Strom arbeitet, erzeugt sie an wichtigen Stellen Events, zum Beispiel beim Beginn und Ende eines Elements, beim Auftreten von Text und so weiter. Für jedes dieser Events ruft der Parser eine Methode am Handler auf, eine Liste aller Methoden findet sich in der Javadoc von DefaultHandler. Der im Programm verwendete OsmNamesHandler überschreibt nun alle für den Anwendungsfall benötigten Methoden des DefaultHandler:

  private static class OsmNamesHandler extends DefaultHandler {
    private final SortedSet<String> nameSet = new TreeSet<String>();
    private final Stack<String> eleStack = new Stack<String>();
    
    public SortedSet<String> getNames() {
      return nameSet;
    }

    @Override
    public void startElement(String uri, String localName, String qName,
        Attributes attrs) throws SAXException {
      if ("tag".equals(qName) && "way".equals(eleStack.peek())) {
        String key = attrs.getValue("k");
        if ("name".equals(key)) {
          nameSet.add(attrs.getValue("v"));
        }
      }
      eleStack.push(qName);
    }

    @Override
    public void endElement(String uri, String localName, String qName)
        throws SAXException {
      eleStack.pop();
    }
  }

Die meiste Arbeit findet in startElement statt: Wenn ein tag Element beginnt, werden seine Attribute ausgewertet. Falls das Element ein Attribut k mit dem Wert "name" hat, dann ist der Wert des Attributes v der gesuchte Name, den man sich in einem TreeSet merkt. Das TreeSet sorgt für eine alphabetische Sortierung der Namen.

Da man nun nicht alle möglichen Namen, sondern nur die von Linienzügen ermitteln will, erfolgt außerdem ein Test, ob das jeweilige Parent-Element way heißt. Leider hat SAX keine Methode getParent - deshalb merkt sich der Algorithmus die Hierarchie der Elemente in einem Stack. Der Stack wird am Ende von startElement bestückt und in endElement abgeräumt.

Der restliche Teil des Programms befasst sich mit Ein- und Ausgabe. Die Eingabe berücksichtigt, dass die OSM Daten nicht bei jedem Programmlauf heruntergeladen werden müssen:

  private void printWayNames(SortedSet<String> nameSet, PrintStream out) {
    for (String name : nameSet) {
      out.println(name);
    }
  }

  public static void main(String[] args) throws Exception {
    File osmXmlFile = new File("md.osm.xml");
    OsmSaxNames osmSaxNames = new OsmSaxNames();
    if (! osmXmlFile.canRead()) {
      System.out.println("Downloading OSM data...");
      osmSaxNames.downloadSample(osmXmlFile);
    }
    SortedSet<String> nameSet = osmSaxNames.findWayNames(osmXmlFile);
    osmSaxNames.printWayNames(nameSet, System.out);
  }
}

Fazit

SAX arbeitet wie der XML Stream Reader eventbasiert, nur dass der Programmierer die Events nicht per Pull aus dem Reader herausliest, sondern sie per Push zugestellt bekommt. Man spricht daher bei SAX auch von einem Push-Parser. Die Gemeinsamkeiten mit dem Stream Reader sind die gute Performance und der geringe Speicherverbrauch gekoppelt mit dem Nachteil, dass ein Navigieren auf dem XML-Dokument nicht möglich ist. Der Einsatz des Element-Stacks im Beispielprogramm zeigt eine Möglichkeit, wie man damit umgehen kann.