XPath (XML Path Language) ist die Selektor-Sprache für XML-Bäume — ähnlich wie CSS-Selektoren für HTML, aber mächtiger. XPath beantwortet die Frage „wie komme ich an genau diese Knoten in einem XML-Dokument?”. Es ist die Grundlage von XSLT (Transformation), XQuery (Abfragesprache), Schematron (Validierung) und vielen XML-Editoren und -Bibliotheken.

In der GLAM- und DH-Praxis ist XPath die natürliche Sprache für Abfragen auf TEI, EAD, METS, LIDO, MODS und beliebigen anderen XML-Auslieferungen. Wer Massen-Korrekturen an einer TEI-Edition vornimmt oder aus einer EAD-Datei alle Personennamen extrahiert, verwendet XPath — direkt im Editor, in xmlstarlet, in einem Python-Skript oder in einer XSLT-Transformation.

Versionen

VersionStatusHinweis
XPath 1.01999überall implementiert; Default in vielen Tools
XPath 2.02010Sequenzen, mehr Datentypen, regex; in Saxon, BaseX, eXist
XPath 3.0 / 3.12014 / 2017Maps, Arrays, Funktions-Variablen; in Saxon-EE, BaseX

In der Praxis wird oft mit XPath 1.0 gearbeitet, weil viele Tools (lxml, libxml2, xmlstarlet) damit arbeiten. Sobald regex, group-by oder Maps gebraucht werden, geht’s auf XPath 2.0+ via Saxon oder BaseX.

Datenmodell — Knoten-Typen

TypBeispiel
Element<persName>
Attributexml:id="p1"
TextGoethe
Comment<!-- ... -->
Processing Instruction<?xml-stylesheet …?>
Namespacexmlns:tei="..."
Document (root)das Wurzeldokument

Ein XML-Dokument ist ein Baum aus diesen Knoten; XPath navigiert in diesem Baum.

Syntax — die zentralen Bausteine

AusdruckBedeutung
/von Root aus, absoluter Pfad
./vom aktuellen Knoten aus
../Eltern-Knoten
//beliebig tief im Baum
*beliebiges Element
@*beliebiges Attribut
@attrAttribut attr
text()Text-Knoten
node()beliebiger Knoten
[…]Predicate (Filter)
|Vereinigungsmenge
axis::nodeAchsen-Selektor

Beispiele aus dem GLAM-Alltag

# Alle Personennamen in einem TEI-Dokument
//persName

# Alle persName mit GND-Referenz
//persName[@ref]

# Alle persName, deren ref mit "https://d-nb.info/gnd/" beginnt
//persName[starts-with(@ref, "https://d-nb.info/gnd/")]

# Alle Personen ohne ref-Attribut
//persName[not(@ref)]

# Erstes Vorkommen
(//persName)[1]

# Letztes Vorkommen
(//persName)[last()]

# Inhalt aller titleStmt/title in einem TEI-Header
//teiHeader//titleStmt/title/text()

# In EAD: alle controlaccess-Personen
//controlaccess/persname

# In METS: Datei-IDs aller Image-Files
//mets:fileGrp[@USE="MASTER"]/mets:file/@ID

# In MODS: alle Sprachcodes
//mods:language/mods:languageTerm[@type="code"]/text()

# Konditionale Selektion: Briefe an Goethe
//letter[descendant::persName[@ref="#goethe"]/preceding::salute]

# Element nach Position
//chapter[3]//paragraph[1]

# Eltern-Element holen
//persName[@ref="#goethe"]/parent::*

# Alle Geschwister-Elemente nach einem bestimmten
//head/following-sibling::*

# Werte mehrerer Attribute zusammenziehen (XPath 2.0)
//persName/concat(@xml:id, ": ", text())

Achsen — präzise Navigation im Baum

XPath-Achsen erlauben gezielte Bewegung jenseits der einfachen Eltern/Kind-Beziehung:

AchseBedeutung
child:: (Default)direkte Kind-Knoten
descendant::alle Nachfahren (= // ab dem Knoten)
descendant-or-self::inkl. Knoten selbst
parent::Eltern-Knoten
ancestor::alle Vorfahren bis Root
following-sibling::spätere Geschwister
preceding-sibling::frühere Geschwister
following::alle nach diesem Knoten im Dokument
preceding::alle davor
attribute:: (@)Attribute des Knotens
namespace::Namespace-Deklarationen
self::der Knoten selbst
# Alle Personennamen, die nach dem ersten Brief vorkommen
//letter[1]/following::persName

# Alle übergeordneten Kapitel-Container eines Knotens
//persName[@xml:id="goethe"]/ancestor::div

# Geschwister-Elemente vor einem Knoten
//head[text()="Brief 12"]/preceding-sibling::head

Predicates — filtern

# Numerischer Index
//chapter[3]

# Vergleich
//author[@birth-year > 1800]

# Mehrere Bedingungen
//persName[@ref][not(@type="fictional")]

# Kombiniert mit Funktionen
//note[contains(., "Goethe")][string-length(.) > 100]

# Existenz eines Kind-Knotens
//person[name][not(birth)]

Wichtige Funktionen

FunktionBeispiel
text()Textknoten
name() / local-name()Elementname / ohne Namespace
count(//x)Anzahl
position() last()Index in Sequenz
string()Stringwert (alle Textknoten konkateniert)
string-length()Länge
concat(a, b)Verkettung
starts-with(s, p)Präfix-Test
contains(s, sub)Substring-Test
substring(s, start, len)Teilstring
substring-before(s, sep) substring-after(s, sep)bis/nach Trennzeichen
normalize-space(s)Whitespace zusammenziehen
translate(s, from, to)Zeichen-für-Zeichen-Ersetzung
not(...)Negation
boolean(...)nach Boolean wandeln
id("xyz")Element mit xml:id="xyz"

In XPath 2.0+ kommen dazu: matches(s, regex), replace(s, regex, repl), tokenize(s, sep), string-join(seq, sep), for $x in ... return ....

Namespaces — die häufigste Stolperfalle

XML-Dokumente mit Namespace (TEI, METS, MODS, LIDO, EAD3) erzwingen einen Namespace-Präfix in XPath. Ein nackter //persName matcht in TEI nichts, weil der TEI-Namespace nicht implizit ist.

# Falsch (in TEI):
//persName

# Richtig — Präfix vorher binden:
//tei:persName

# In Tools mit Default-Namespace-Option:
*:persName
# oder mit local-name():
//*[local-name()="persName"]

In xmlstarlet, lxml, Saxon usw. wird der Präfix typisch beim Tool-Aufruf gebunden:

xmlstarlet sel -N tei="http://www.tei-c.org/ns/1.0" \
  -t -v '//tei:persName/@ref' datei.xml

In xmlstarlet

Das CLI-Schweizermesser für XML — XPath-Abfragen direkt in der Shell.

# Alle persName-Elemente extrahieren
xmlstarlet sel -t -m '//persName' -v 'text()' -n datei.xml

# Mit Namespace (TEI)
xmlstarlet sel -N t='http://www.tei-c.org/ns/1.0' \
  -t -m '//t:persName' -v 'text()' -n datei.tei.xml

# Attribute holen
xmlstarlet sel -t -v '//persName/@ref' datei.xml

# Validieren
xmlstarlet val --xsd schema.xsd datei.xml

# Editieren — Attribut auf alle persName setzen
xmlstarlet ed --inplace \
  -u '//persName[not(@cert)]/@cert' -v 'high' \
  datei.xml

# Knoten löschen
xmlstarlet ed --inplace -d '//note[@type="todo"]' datei.xml

# Knoten einfügen
xmlstarlet ed --inplace \
  -s '//body' -t elem -n 'div' -v 'Neuer Inhalt' \
  datei.xml

In Python (lxml)

from lxml import etree

doc = etree.parse("datei.xml")

# XPath 1.0 mit Namespace-Map
ns = {"tei": "http://www.tei-c.org/ns/1.0"}
namen = doc.xpath("//tei:persName/text()", namespaces=ns)

# Attribute
refs = doc.xpath("//tei:persName/@ref", namespaces=ns)

# Komplexere Abfrage
briefe_an_goethe = doc.xpath(
    '//tei:letter[.//tei:persName[@ref="#goethe"]]',
    namespaces=ns
)

Für XPath 2.0+ in Python: elementpath-Library, oder Saxon-via-saxonche.

In Visual Studio Code

VS Code selbst hat kein eingebautes XPath, aber zwei nützliche Erweiterungen:

  • „XML” (Red Hat / LemMinX) — XML-Schema-Validation, Auto-Complete, kleine XPath-Auswertung über Hover
  • „XPath helper”-Extensions — interaktiver XPath-Tester im Editor; Evaluierung gegen das aktuelle Dokument

In der Suche lassen sich XPath-Resultate nicht direkt einsetzen, aber für Massen-Edits in TEI-/EAD-Dateien ist die Kombination XPath für Recherche + Regex-Find/Replace für die Änderung etabliert.

In Oxygen XML Editor

Oxygen ist die Oberklasse-Lösung für TEI-/EAD-Editierarbeit; XPath ist dort first-class:

  • XPath-Builder mit Live-Evaluierung gegen das offene Dokument
  • XPath 1.0, 2.0, 3.1 wählbar
  • „Find/Replace in Files” mit XPath-Selektor + regex-Replace
  • Saxon eingebaut für komplexe XSLT-Transformationen

In Oxygen führt XPath direkt zum gefundenen Knoten und markiert ihn — die produktivste Umgebung für TEI-Massen-Operationen.

In XSLT

XPath ist das Selektor-Herz von XSLT:

<xsl:template match="tei:persName[@ref]">
  <a href="{@ref}">
    <xsl:value-of select="."/>
  </a>
</xsl:template>

<xsl:apply-templates select="//tei:body/tei:div"/>

Jeder match, select, test in einem XSLT-Stylesheet ist ein XPath-Ausdruck. Wer XSLT macht, macht XPath.

Häufige Fallen

  • Namespace vergessen//persName matcht in TEI nichts. Immer prüfen, ob ein Default-Namespace deklariert ist.
  • Verwechslung Element vs. Text//title liefert Elemente, //title/text() die Textknoten. Im Stringkontext meist gleich, in mixed-content nicht.
  • Index 1-basiert//chapter[1] ist das erste, nicht das zweite Kapitel.
  • //x[last()] — funktioniert pro Kontext. //chapter[last()] liefert das letzte Kapitel pro Eltern-Knoten, nicht das insgesamt letzte. Für „insgesamt letztes”: (//chapter)[last()].
  • text() ohne normalize-space() — Whitespace kann unerwartet drin sein. normalize-space(.) für saubere Strings.
  • Kontext-Sensitivität in Predicates//x[@a or @b] ist nicht dasselbe wie //x[@a] | //x[@b] (zweiteres ist langsamer und liefert eine Vereinigungsmenge mit potenziell anderer Reihenfolge).
  • XPath 1.0 hat keine regexmatches() und replace() brauchen XPath 2.0+ (Saxon, BaseX). lxml kann nur XPath 1.0 plus EXSLT-Erweiterungen.
  • Attribute haben keine Namespace-Default — anders als Elemente. @ref matcht ohne Präfix; @xml:id braucht den xml-Präfix.

Werkzeuge

  • xmlstarlet — Unix-CLI für alle XML-Operationen (XPath, validate, edit, format)
  • Saxon (HE Open Source, EE kommerziell) — der XSLT-/XQuery-Prozessor; Goldstandard für XPath 2.0/3.1
  • BaseX — XML-Datenbank mit XQuery-Frontend und XPath-Live-Evaluator
  • eXist-db — XML-Datenbank, basis für TEI Publisher
  • lxml (Python) — XPath 1.0 + EXSLT
  • elementpath (Python) — XPath 2.0/3.1 für lxml
  • Oxygen XML Editor — kommerzielle Editier-/Test-Umgebung

Verhältnis zu anderen Standards

  • TEI — XPath ist die natürliche Sprache zur Navigation in TEI-Dokumenten; jede TEI-Praxis kommt früher oder später nicht ohne aus.
  • EAD und METS und MODS und LIDO — alle XML-basiert, alle via XPath abfragbar.
  • XSLT / XQuery — XPath ist deren Selektor-Layer.
  • CSS-Selektoren — analoges Konzept für HTML, aber strukturell ärmer (kein Eltern-Selektor, keine Achsen).
  • JSONPath — analoges Konzept für JSON; kein 1 : 1 mit XPath, aber inspiriert davon.
  • Regex — komplementär: Regex für unstrukturierten Text, XPath für strukturierte XML-Bäume. Häufig in derselben Pipeline kombiniert.

Häufige Patterns

# Aufzählung aller eindeutigen ref-Werte (XPath 2.0+)
distinct-values(//persName/@ref)

# Personen ohne Authority-Verlinkung
//persName[not(@ref)]

# Briefe mit fehlendem Datum
//letter[not(date) or date/normalize-space() = ""]

# Alle Bilder in einem METS, gruppiert nach fileGrp
//mets:fileGrp/@USE

# In TEI: Wortzahl pro Kapitel grob (XPath 2.0+)
for $div in //tei:div return
  count(tokenize(string($div), "\s+"))