4Max/shutterstock.com

15. November 2011 / von Benjamin Rank

Scala
&
XML

… dann stößt man mit dem in meinem letzten Eintrag vorgestellten Verfahren XML zu verarbeiten schnell an die Grenzen des Speichers. Scala bietet aber auch eine StAX-ähnliche XML Pull API, die im Folgenden näher vorgestellt wird.

Vorneweg etwas zu „Push“ und „Pull“

Im Gegensatz zum

XMLLoader

(der DOM-ähnlichen Variante, die ich in meinem letzten Artikel zu Scala und XML vorgestellt habe), wird bei „Pull“- bzw. „Push“-Variante nicht das komplette XML in den Speicher geladen. Es wird einzeln über jedes Element im XML iteriert und für jedes Element ein Event generiert, auf das der verarbeitende Code reagieren kann. „Push“ und „Pull“ unterscheiden sich darin, ob die Kontrolle während des Parsens eines Dokuments beim Parser oder beim verarbeitenden Programm liegt. Ein „Push“-Parser übernimmt die Kontrolle während des Parsens, indem er das Dokument durchläuft und für jedes enthaltene Element eine Handler-Methode beim verarbeitenden Programm aufruft (SAX in Java). Ein „Pull“-Parser hingegen bietet einen Iterator an, mit dem das verarbeitende Programm entscheiden kann, wann es das nächste Element auslesen möchte (StAX in Java). Somit ist es dem Programm auch möglich das Parsen vorzeitig zu beenden, ohne das komplette XML eingelesen zu haben.

Scala.xml.pull

Im Paket 

scala.xml.pull

sind alle Klassen definiert, die für die XML Pull Verarbeitung benötigt werden. Das zentrale Element ist die Klasse

XMLEventReader

. Diese übernimmt das Parsen des XML Dokuments und wird wie ein Iterator verwendet. Nacheinander werden für die Elemente des XML Dokuments Events erzeugt und zurückgeliefert. Das folgenden Beispiel zeigt am Beispiel der „movies.xml“-Datei, die auch schon im meinem letzten Blogeintrag zum Thema XML verwendet wurde (siehe Scala & XML).

def parseXmlSimple(): Unit = {
  val reader = new XMLEventReader(io.Source.fromFile("movies.xml")("UTF-8"))
  while (reader.hasNext) {
    println(reader.next)
  }
}

Das Beispiel initialisiert einen

XMLEventReader

uns lässt ihn die Daten aus der XML Datei lesen. Danach wird mit einer while-Schleife auf den Reader in Form eines Iterators zugegriffen und alle erzeugten Events ausgegeben. Damit bekommen wir eine gute Übersicht, welche Events in welcher Reihenfolge generiert werden:

EvElemStart(null,movies,,)
EvText(
    )
EvElemStart(null,movie, id="1",)
EvText(
        )
EvElemStart(null,title,,)
EvText(Fluch der Karibik)
EvElemEnd(null,title)
EvText(
        )
EvElemStart(null,year,,)
EvText(2003)
EvElemEnd(null,year)
EvText(
        )
EvElemStart(null,director,,)
EvText(Gore Verbinski)
EvElemEnd(null,director)
EvText(
        )
EvElemStart(null,actors,,)
EvText(
            )
EvElemStart(null,actor,,)
EvText(Jonny Depp)
EvElemEnd(null,actor)
EvText(
            )
EvElemStart(null,actor,,)
EvText(Orlando Bloom)
EvElemEnd(null,actor)
EvText(
            )
EvElemStart(null,actor,,)
EvText(Keira Knightley)
EvElemEnd(null,actor)
EvText(
        )
EvElemEnd(null,actors)
EvText(
    )
EvElemEnd(null,movie)

... hier folgen noch die Events für die restlichen Filme ...

EvElemEnd(null,movies)

Das Beispiel zeigt, dass für jedes Element ein

EvElemStart

und ein

EvElemEnd

Event erzeugt werden. Der erste Wert beider Events ist der Namespace-Prefix, der im Beispieldokument nicht gesetzt und somit

null

ist. Darauf folgt der Name des xml-Tags. Das

EvElemStart

Event enthält noch die Liste der Attribute des Knotens und Informationen über das Namespace-Binding als letzen Eintrag. Der Text innerhalb eines XML Elements wird als

EvText

ausgegeben. Wie wir sehen, gilt das auch für alle Whitespaces, die zwischen den Elementen im XML Dokument stehen.

Alle diese Events erben von

XMLEvent

. In der zugehörigen Dokumentation im scaladoc bekommt man einen Überblick über die möglichen Eventtypen. Sie sind als Case Klassen modelliert, was für die weitere Verarbeitung sehr angenehm ist, da somit ein Pattern Matching auf ihnen ermöglicht wird.

Im folgenden Beispiel wird wieder das „movies.xml“ XML geparst, diesmal werden die Events aber weiter verarbeitet. Ziel ist es, alle Filme aus dem Dokument in eine Liste von Scala Objekten einzulesen und somit weiterverarbeiten zu können. Die Definition der Scala Klasse sieht folgendermaßen aus:

case class Movie(id: Int, var title: String, var year: Int, var director: String, var actors: List[String])

Damit können wir uns das Beispiel näher ansehen:

def parseXml(): Unit = {
  val reader = new XMLEventReader(io.Source.fromFile("movies.xml")("UTF-8"))

  var movie: Movie = null
  var currentNode = ""

  while (reader.hasNext) {
    var next = reader.next
    next match {

      case EvElemStart(_, "movie", attr, _) => {
        val id = attr("id").text.toInt
        movie = Movie(id, "", 0, "", List())
      }

      case EvElemStart(_, elem, _, _) => {
        currentNode = elem
      }

      case EvText(text) => {
        currentNode match {
          case "year" => movie.year = text.toInt
          case "director" => movie.director = text
          case "title" => movie.title = text
          case "actor" => movie.actors ::= text
          case _ =>
        }
      }

      case EvElemEnd(_, "movie") => println(movie)

      case EvElemEnd(_, _) => currentNode = ""

      case _ =>
    }
  }
}

Genau wie im vorherigen Beispiel wird ein

XMLEventReader

mit der Beispieldatei initialisiert. Da beim Parsen der Datei immer nur Bruchstücke einzelner Elemente des XML bei einem Event zur Verfügung stehen, muss sich das Programm Informationen über den Kontext merken. Dies geschieht in den Variablen

movie

und

currentNode

.

movie

enthält alle Informationen, die über den aktuellen Film gesammelt wurden und

currentNode

enthält den Namen des aktuellen XML Elements.

Jetzt folgt die Schleife zur Iteration über alle Events des XML Dokuments. Mittels Pattern Matching wird das gerade erzeugte Event untersucht. Der Unterstrich in einem Pattern zeigt an, dass dieser Parameter beliebig belegt sein darf, damit das Pattern gültig ist. Für das Beispiel sind die folgenden Unterscheidungen von Interesse:

  • Start-Tag des Elements
    movie

    Es wird speziell das Element

    movie

    gematcht. Da in den Attributen des Knotens die ID des Films enthalten ist, werden diese mit Hilfe des Matchers ausgelesen und der Variablen

    attr

    zugewiesen. Damit ist es möglich das Attribut „id“ aus der Attributliste auszulesen und einen neue Instanz von

    Movie

    anzulegen, die diese ID enthält. Die restlichen Felder werden mit Platzhaltern gefüllt.

  • Start-Tag aller anderen Elemente
    Durch den Matcher wird der Variablen

    elem

    der Name des Elements zugewiesen. Dieser wird dann in

    currentNode

    gesichert, um beim nächsten

    EvText

    -Event die Zuordnung zum Element zu haben.

  • Übernehmen des Inhalts von Elementen
    Der Inhalt von Elementen wird durch ein

    EvText

    -Event zurückgegeben. Mit Hilfe des gesicherten Kontexts in

    currentNode

    wird bestimmt, welcher Eigenschaft des aktuellen Films der Wert zugewiesen werden muss. Wenn es sich um einen Schauspieler handelt, wird dieser mit dem

    ::=

    Operator an die Liste der Schauspieler angehängt.

  • Ende-Tag des Elements
    movie

    Der Film wurde komplett aus dem XML ausgelesen und in diesem Fall einfach ausgegeben.

  • Ende-Tag aller anderen Elemente
    currentNode

    wird zurückgesetz, da der Kontext des Elements verlassen wird.

Führt man das Beispiel aus, erhält man die folgende Ausgabe:

Movie(1,Fluch der Karibik,2003,Gore Verbinski,List(Keira Knightley, Orlando Bloom, Jonny Depp))
Movie(2,Die Bourne Identität,2002,Doug Liman,List(Chris Cooper, Franka Potente, Matt Damon))
Movie(3,Herr der Ringe: Die Gefährten,2001,Peter Jackson,List(Orlando Bloom, Ian McKellen, Elijah Wood))
Movie(4,Avatar - Aufbruch nach Pandora,2009,James Cameron,List(Sigourney Weaver, Zoe Saldana, Sam Worthington))

Noch ein paar Worte zur Performance

Da die von der Pull API erzeugten Events als Case Klassen umgesetz wurden, lassen sie sich sehr gut mit dem Scala Pattern Matching verarbeiten, so dass sich „schöner“ Scala Code schreiben lässt. Wenn es bei der Verarbeitung des XML allerdings auf die Performance ankommt, dann sollte man von der Verwendug der Scala API absehen und auf Java-Bibliotheken zurückgreifen. Der Java

XMLEventReader

aus dem Paket

javax.xml.stream

 ist beim parsen einer Datei ohne weitere Verarbeitung der Daten nach meinen einfachen Tests mit sehr großen XML-Dateien (ca. 500 MB) mit einer flachen Hierarchie etwa um den Faktor vier schneller als das Scala Pendant.

Autor

Benjamin Rank Benjamin ist seit Januar 2011 bei der Accso GmbH tätig und beschäftigt sich aktuell mit den Möglichkeiten, die Scala bei der Softwareentwicklung bietet.
Weitere Artikel

Das könnte Sie auch interessieren