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
| Version | Status | Hinweis |
|---|---|---|
| XPath 1.0 | 1999 | überall implementiert; Default in vielen Tools |
| XPath 2.0 | 2010 | Sequenzen, mehr Datentypen, regex; in Saxon, BaseX, eXist |
| XPath 3.0 / 3.1 | 2014 / 2017 | Maps, 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
| Typ | Beispiel |
|---|---|
| Element | <persName> |
| Attribute | xml:id="p1" |
| Text | Goethe |
| Comment | <!-- ... --> |
| Processing Instruction | <?xml-stylesheet …?> |
| Namespace | xmlns:tei="..." |
| Document (root) | das Wurzeldokument |
Ein XML-Dokument ist ein Baum aus diesen Knoten; XPath navigiert in diesem Baum.
Syntax — die zentralen Bausteine
| Ausdruck | Bedeutung |
|---|---|
/ | von Root aus, absoluter Pfad |
./ | vom aktuellen Knoten aus |
../ | Eltern-Knoten |
// | beliebig tief im Baum |
* | beliebiges Element |
@* | beliebiges Attribut |
@attr | Attribut attr |
text() | Text-Knoten |
node() | beliebiger Knoten |
[…] | Predicate (Filter) |
| | Vereinigungsmenge |
axis::node | Achsen-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:
| Achse | Bedeutung |
|---|---|
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
| Funktion | Beispiel |
|---|---|
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 —
//persNamematcht in TEI nichts. Immer prüfen, ob ein Default-Namespace deklariert ist. - Verwechslung Element vs. Text —
//titleliefert 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()ohnenormalize-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 regex —
matches()undreplace()brauchen XPath 2.0+ (Saxon, BaseX). lxml kann nur XPath 1.0 plus EXSLT-Erweiterungen. - Attribute haben keine Namespace-Default — anders als Elemente.
@refmatcht ohne Präfix;@xml:idbraucht denxml-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+"))