60. XML III: DOM

Das DOM (Document Object Model) ist eine alternative Schnittstelle zu XML-Daten. In ihr baut eine Bibliothek einen Baum aus dem Dokument (warum geht das?). Diesen Baum kann man dann manipulieren.

Vorteil des DOM ist, wie gesagt, dass zur Auswertung von Daten, die in einem DOM-Baum gehalten werden, meist weit weniger State durch das Programm geschleppt werden muss als bei SAX. Hauptnachteil ist, dass DOM (fast) immer das ganze Dokument in den Speicher ziehen muss (siehe aber auch xml.dom.pulldom).

Leider ist das scheinbar so einfache XML doch so kompliziert, dass auch DOM ein recht komplizierter Standard geworden ist. Zum Glück kommt man für die meisten Anwendungen mit einer kleinen Untermenge aus. Eine weitere gute Nachricht ist, dass DOM in moderne Web-Browser eingebaut ist und Kenntnisse von DOM helfen, ganz tolle, bunte und dynamische Webseiten zu bauen. Das soll man zwar eigentlich aus vielen Gründen nicht tun, aber Spaß macht es doch

Als Anwendung für DOM wollen wir uns eine hypothetische lexikalische Datenbank ansehen, in der Lemmata, Lesungen und Lexikalische Relationen vermerkt werden. Dabei besteht ein Lemma aus mehreren Lesungen, zwischen denen Relationen wie Hypernymie, Antonymie usf bestehen können. So eine Datei könnte dann etwa folgendermaßen aussehen:

<lexdata>
<lemma name="Wissenschaft" id="l1">
<lesung id="l1_1"><def>Was Schlaues</def>
  <lexrel type="anto" idref="l2_1"/>
  <lexrel type="anto" idref="l2_2"/>
</lesung>
<lesung id="l1_2"><def>Was weniger Schlaues</def>
  <lexrel type="hyper" idref="l2_1"/>
  <lexrel type="hyper" idref="l2_2"/>
</lesung>
</lemma>
<lemma name="Wissenschaftspolitik" id="l2">
<lesung id="l2_1">
  <def>Was extrem Dummes</def>
  <lexrel type="anto" idref="l1_1"/>
  <lexrel type="hypo" idref="l1_2"/>
</lesung>
<lesung id="l2_2">
  <def>Wichtige Leute machen totalen Quatsch</def>
  <lexrel type="anto" idref="l1_1"/>
  <lexrel type="hypo" idref="l1_2"/>
</lesung>
</lemma>
</lexdata>

Wir haben dabei von einer weiteren Standardeigenschaft von XML Gebrauch gemacht: id/idref. Man kann Elementen ein id-Attribut geben, das von bestimmten XML-Technologien speziell ausgewertet wird. Im Effekt gibt man dem Element einen Schlüssel, über den wir nachher schnell und einfach zugreifen können – insbesondere können wir damit quer über verschiedene Äste des Baumes referenzieren. ids spielen z.B. in XSLT eine Rolle, DOM hat ab Level 2 (den minidom nicht implementiert, aber siehe unten) eine Methode getElementById, die zu einer Id das passende Element liefert. Nebenbei bemerkt ist es natürlich ein Fehler, wenn zwei verschiedene Elemente die gleiche id tragen.

Wie Profis Daten dieser Art verwalten, könnt ihr übrigens beispielsweise auf der Wordnet-Seite nachlesen.

Was wir brauchen, steht in xml.dom.minidom. Die parse-Funktion aus diesem Modul nimmt einen Dateinamen (oder eine Datei) und gibt (im Wesentlichen) ein xml.dom.Document-Objekt zurück. Dieses modelliert einen Baum, dessen Wurzelobjekt im documentElement-Attribut steht.

Die Knoten des Baumes stehen in Node-Objekten; sie haben eine Unzahl von Attributen (auf die man hier getrost lesend zugreifen darf). Elemente stehen in Element-Objekten, die von Node abgeleitet sind. Sie unterstützen z.B. die getAttribute-Methode.

Genaueres in der Dokumentation zu xml.dom. Dort gibt es auch Links auf die DOM-Spezifikationen des W3C.

Ein paar Funktionen, die die Suche nach Lesungen erlauben, die in einem bestimmten semantischen Verhältnis targetLexRel zu den Lesungen eines Lemmas stehen:

def findLemma(rootNode, lemma):
  for node in nodeIter(rootNode):
    if (node.nodeName=="lemma" and
      node.getAttribute("name")==lemma):
      return node

def getRelLexForLesung(lesungNode, targetLexRel,
  lexDb):
  result = []
  for lexrel in lesungNode.getElementsByTagName(
    "lexrel"):
    if lexrel.getAttribute("type")==targetLexRel:
      result.append(lesungAsStr(
        lexDb.getElementById(lexrel.getAttribute(
        "idref"))))
  return result

def findRelLemma(lemma, targetLexRel, lexDb):
  relLes = []
  lemmaNode = findLemma(lexDb.documentElement,
    lemma)
  if lemmaNode:
    for lesung in lemmaNode.getElementsByTagName(
      "lesung"):
      lesRelLes = getRelLexForLesung(lesung,
        targetLexRel, lexDb)
      if lesRelLes:
        relLes.extend([(lesungAsStr(lesung), a)
          for a in lesRelLes])
  return relLes

(die kompletten Quellen sind im Anhang dieser Seite).

In findLemma traversieren wir den Baum, bis wir ein lemma-Element gefunden haben, dessen name-Attribut gerade das gesuchte Lemma ist. Wir gehen dabei vom rootNode aus, also dem documentElement des DOM-Objekts, das wir von parse zurückbekommen. Die hier verwendete Funktion nodeIter ist dabei ein Generator, den man folgendermaßen definieren kann:

def nodeIter(aNode):
  """returns an iterator over all nodes below aNode
  """
  yield aNode
  for node in aNode.childNodes:
    for child in nodeIter(node):
      yield child

– DOM-Objekte haben sowas nicht als Methode, weil diese Sorte Generator nicht in allen Sprachen verfügbar ist und DOM den Anspruch der Sprachunabhängigkeit hat.

Die Funktion getRelLexForLesung soll eine Liste von String-Repräsentationen für Lesungen zurückgeben, die in der Relation (also etwa „anto”, „syn”, „hypo” usf.) targetLexRel zur Lesung lesungNode stehen. Es iteriert dazu über eine Liste aller Nodes (das sind dann automatisch Elemente) mit Tagnamen lexrel (die entsprechende Methode ist Teil von DOM), prüft für jeden, ob das typ-Attribut gleich der gesuchten Relation ist und hängt, wenn das so ist, eine String-Repräsentation des „Ziels” der entsprechenden Relation.

Wir brauchen zum Finden des Ziels die oben erwähnte getElementById-Methode. Da das hier verwendete minidom-Modul diese (noch) nicht bereitstellt, müssen wir sie quasi per Hand nachrüsten. Der Umstand, dass man in Python-Elementen im Nachhinein wild rumrühren kann, hilft uns hier – man sollte Entsprechendes aber wirklich nur dann tun, wenn alles andere vergeblich war:

def _makeIdCache(domOb):
  """adds a getElementById-Method to the DOM object domOb.  This is
  a bad hack and will be obsoleted when minidom becomes DOM level 2
  compliant.
  """
  idDict = {}
  for node in nodeIter(domOb.documentElement):
    try:
      idDict[node.getAttribute("id")] = node
    except (AttributeError, KeyError):
      pass
  domOb._mdsIdDict = idDict
  domOb.getElementById = new.instancemethod(
    lambda self, id: self._mdsIdDict.get(id, None), domOb)

Interessant ist hier vielleicht, dass wir (1) für das Dictionary, das von ids auf die zugehörigen Elemente zeigt, einen Namen verwendet haben, der es unwahrscheinlich macht, dass wir ein Attribut, das die Document-Klasse selbst definiert, überschreiben (_mdsIdDict) und (2) dem Objekt eine neue Methode unterschieben. Das ist etwas aufwändig, weil wir die Methode an das Objekt „binden” müssen. Wir verwenden dafür die instancemethod-Funktion aus dem new-Modul. Die Details würden den Rahmen dieses Skripts sprengen – nehmt es als Hinweis, dass Python immer noch viele spannende Ecken hat, von deren Existenz ihr bisher noch nichts gehört habt

Die findRelLemma-Funktion schließlich kombiniert die anderen Funktionen schließlich so, dass sie schließlich sämtliche Ziele der gesuchten Relationen in einer Liste zurückgeben kann.

Dateien zu diesem Abschnitt


Markus Demleitner

Copyright Notice