Die XPath API

Martin Kompf

Mittels XPath Ausdrücken lassen sich auf elegante Art und Weise gezielt Informationen aus XML Dokumenten extrahieren. Um XPath in einem Java Programm zu verwenden, sind nur wenige Zutaten erforderlich.

Einfacher mit XPath

Der Artikel XML Stream Reader stellt eine Methode vor, um mit Hilfe der Streaming API für XML (StAX) die Titel der Nachrichten aus einem Atom Feed auszulesen. Das vom Feed ausgelieferte XML sieht zum Beispiel so aus:

<feed xmlns="http://www.w3.org/2005/Atom">
  <title>heise online News</title>
  <entry>
    <title>Videotelefonie-App für Android</title>
    <link href="http://..." />
  </entry>
  <entry>
    <title>Kritische Sicherheitslücken in Firefox und Thunderbird geschlossen</title>
    <link href="..." />
  </entry>
</feed>

Die Aufgabenstellung lässt sich dabei nach einer Analyse des Problems in einem einzigen Satz hinschreiben: «Extrahiere den Textinhalt aller title Elemente, die direkte Kinder von entry» sind. Zur Implementierung mittels StAX waren dann aber doch zwei ineinander geschachtelte while-Schleifen und mehrere if-Statements notwendig. Daher stellt sich die Frage: Geht es auch einfacher?

Die Antwort ist: Ja natürlich, mittels XPath! XPath steht für «XML Path Language» und ist eine vom W3C entwickelte Abfragesprache, um Teile eines XML Dokuments zu adressieren. Eine kurze Einführung zu XPath findet man in Wikipedia, die ausführliche Referenz stellt das W3C bereit. Ein passender XPath Ausdruck für unser Problem lässt sich aus der Aufgabenstellung relativ leicht ableiten:

//entry/title/text()

lokalisiert ausgehend von entry alle Textknoten unterhalb von title.

DOM als Ausgangspunkt

Um diesen XPath Ausdruck auf einen Atom Feed, zum Beispiel die Heise News oder einen Twitter Feed anwenden zu können, muss er zunächst mit einem Parser in ein Document Object Model (DOM) überführt werden. Das ist im Artikel DOM API beschrieben. Die folgende Funktion setzt voraus, dass das XML als InputStream in und die XPath Expression als String xpathExpr vorliegen:

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

import javax.xml.XMLConstants;
import javax.xml.parsers.*;
import javax.xml.xpath.*;

import org.w3c.dom.*;
import org.xml.sax.SAXException;

public class XPathReader {
  void eval(InputStream in, String , Collection<String> result)
      throws ParserConfigurationException, SAXException, IOException,
      XPathExpressionException {
    DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
    docFactory.setNamespaceAware(false); // important!
    DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
    Document doc = docBuilder.parse(in);

Das JAXP konforme Vorgehen zur Erzeugung einer XPathExpression ist die Verwendung der XPathFactory. Nach dem Kompilieren des XPath-Strings wird das DOM an die Methode evaluate übergeben. Diese liefert im Beispiel eine Liste von Textknoten zurück, deswegen wird als zweiter Parameter XPathConstants.NODESET übergeben und das Ergebnis nach NodeList gecastet - siehe dazu die Javadoc der Package javax.xml.xpath.

    XPath xpath = XPathFactory.newInstance().newXPath();
    XPathExpression expr = xpath.compile(xpathExpr);

    NodeList nodeList = (NodeList) expr.evaluate(doc, XPathConstants.NODESET);

Durch Iterieren über die NodeList und Aufruf von getNodeValue kommt man an die gesuchten Werte:

    for (int i = 0; i < nodeList.getLength(); ++i) {
      Node node = nodeList.item(i);
      result.add(node.getNodeValue());
    }
  }

Es ist angerichtet

Damit hat man alle erforderlichen Zutaten an Bord, um zum Beispiel die Titel der neuesten Heise News zu lesen:

  void readHeiseFeed() throws Exception {
    URL heiseFeed = new URL("http://www.heise.de/newsticker/heise-atom.xml");
    InputStream in = heiseFeed.openStream();
    Collection<String> result = new LinkedList<String>();
    eval(in, "//entry/title/text()", result);
    printResult(result, System.out);
  }

Interessiert man sich mehr für das neueste Gezwitscher auf Twitter ersetzt man die URL einfach mit http://api.twitter.com/1/statuses/public_timeline.atom.

Universell verwendbar

Und es kommt noch besser: Sogar die Aufgabenstellung aus dem Artikel SAX Parser lässt sich nun ohne weiteres realisieren! Aus «Lese aus einem OpenStreetMap XML alle Wegenamen» wird:

  void readOsmWayNames() throws Exception {
    URL osmUrl = new File("md.osm.xml").toURI().toURL();
    InputStream in = osmUrl.openStream();
    Collection<String> result = new TreeSet<String>();
    eval(in, "/osm/way/tag[@k='name']/@v", result);
    printResult(result, System.out);
  }

Den Trick schafft hier der XPath Ausdruck

/osm/way/tag[@k='name']/@v

Er selektiert das v Attribut aller tag Elemente, die unmittelbares Kind von way sind und ein Attribut k mit dem Wert name haben (Vergleiche dazu das Beispiel-XML aus dem Artikel SAX Parser). Als result wird hier ein TreeSet anstelle einer LinkedList verwendet - dadurch ist das Ergebnis sortiert und unique.

Vorsicht, Namespaces!

Das Parsen des InputStream mit dem DOM Parser erfolgte wohlweislich ohne die Berücksichtigung von Namespaces. Dafür sorgte die Anweisung setNamespaceAware(false) vor der Erzeugung des Parsers. Dadurch kann die Tatsache, dass sich alle XML Elemente eines Atom Feeds eigentlich im Namespace http://www.w3.org/2005/Atom befinden (siehe XML Stream Writer und Namespaces), komplett ignoriert werden.

Muss man den Namespace jedoch berücksichtigen, so sind folgende Änderungen am Code vorzunehmen:

Im Beispielcode findet sich eine vollständige Implementierung hierfür.

XPath - alles Gut?

XPath bietet eine elegante Möglichkeit, Daten aus XML Dokumenten zu extrahieren. Es integriert sich seit Java 6 nahtlos in die existierenden Java XML APIs (JAXP).

Wozu braucht man also noch XML Stream Reader und SAX Parser? Die Antwort kann man leicht selbst finden, indem man eine etwas größere XML Datei von OpenStreetMap herunterlädt und einmal mit dem hier vorgestellten XPath Verfahren und zum anderen mit dem SAX Parser verarbeitet. Während das Extrahieren mit dem SAX Parser bei großen Datenmengen länger dauern kann, liefert es jedoch die gewünschten Ergebnisse. Bei der Verwendung von XPath kommt es jedoch binnen kürzester Zeit zu einem OutOfMemoryError. Die Ursache dafür ist, dass zunächst die kompletten XML Daten als DOM in den Hauptspeicher geladen werden müssen, bevor sie sich mit XPath evaluieren lassen. Dieses Problem tritt bei SAX oder dem Stream Reader nicht auf, daher sind diese Verfahren insbesondere für die performante Verarbeitung großer Datenmengen vorzuziehen.