1 Einführung und Übersicht der Lernziele

Wie strukturiert man Texte sinnvoll, und wie findet man gezielt bestimmte Muster in großen Datenmengen? In diesem Workshop lernst du den praktischen Einsatz von XML und regulären Ausdrücken — z.B. zur Analyse von Logfiles oder zur Prüfung von Texteingaben. Am Beispiel von TEI-XML zeigen wir, wie man Textdaten in den Digital Humanities professionell enkodiert. Als konkretes Anwendungsszenario vereinheitlichen wir die Schreibvarianten von Personen- und Ortsnamen, um diese trotz der vorhandenen Variation denselben Personen beziehungsweise Orten zuordnen zu können. Zum Beispiel wird der Name des Komponisten Georg Friedrich Händel aufgrund seines Wirkens in England oft zu „George Frideric Handel“ anglisiert, während die Stadt London in vielen romanischen Sprachen als „Londres“ geschrieben wird.

Konkret lernen wir:

  • Die Gemeinsamkeiten von HTML und XML, und wie man eine XML-Datei erstellt, oder eine HTML-Datei in eine XML-Datei umwandelt.
  • Wie man ein XML-Schema verwendet, in unserem Fall TEI-XML.
  • Wie man anhand regulärer Ausdrücke die Annotation von Texten wesentlich effizienter gestalten kann.
  • Wie man Informationen aus XML-Dateien extrahieren kann.
  • Wie man nachvollziehbare Referenzen trotz Schreibvarianten erstellt.
  • Welche fortgeschrittene Möglichkeiten es für einen noch einen effektiveren Umgang mit XML gibt.

2 HTML und XML: Gemeinsamkeiten und Unterschiede

Aufgrund ihrer gemeinsamen Herkunft haben HTML und XML viele Gemeinsamkeiten:

  • Beide Formate bestehen aus Elementen, die Anhand von Tags markiert werden.
  • Elemente können einen Inhalt anhand von einem Starttag und Endtag umklammern, wie z.B. <Elementname>Inhalt</Elementname>, oder inhaltslos mit einem Leertag vorkommen, wie z.B. <Elementname/>.
  • Sowohl Starttags als auch Leertags können mit Attributen zusätzliche Informationen enthalten, wie z.B. <Elementname Attribut="Wert">Inhalt</Elementname>.
  • Elemente könnten verschachtelt werden, z.B. <Element1><Element2>Inhalt</Element2></Element1>.
  • Anhand von Formatvorlagen als CSS (Cascading Style Sheet) lassen sich sowohl HTML als auch XML in einem Webbrowser darstellen.
  • Kommentare folgen demselben Format: <!-- Kommentar -->.

Jedoch gibt es einige wesentliche Unterschiede:

  • HTML ist auf eine vordefinierte Liste von Tags beschränkt. XML hingegen ist erweiterungsfähig (eXtensible), sodass man prinzipiell beliebige Tags hinzufügen kann.
  • HTML erlaubt es, Elemente durch Auslassen eines Endtags geöffnet zu lassen. Dies entspricht zwar nicht der bewährten Vorgehensweise und wird als „tag soup“ verschmäht, ist aber grundsätzlich möglich. XML hingegen fordert eine strikte Entsprechung von Starttags und Endtags.
  • Außerdem muss ein XML-Dokument von genau einem Wurzelelement umrahmt sein.

Einfach mal ausprobieren 🎛️

  • Wir öffnen Visual Studio Code und erstellen eine Datei namens test.xml.
  • Wir verschachteln mehrere Elemente und achten auf die Fehleranzeige. Z.B. können wir im Text „Dies ist ein Satz. Und dies ist auch ein Satz.“ die Sätze und Wörter markieren.
  • Wir fügen auch inhaltslose Elemente (wie z.B. einen Textanfang und ein Textende) sowie Kommentare hinzu.

Beauty-Tipp 💅

Visual Studio Code kann den Quellcode automatisch formatieren, sodass die Einrückung der Hierarchie der Elemente entspricht. Dieser Vorgang wird im Fachjargon prettify genannt. Unter „Anzeige > Befehlspalette…“ ist der Befehl „Auswahl formatieren“ zu finden, der dies bewirkt. Alternativ kann auch der Tastaturbefehl „Ctrl/Cmd K Ctrl/Cmd F“ verwendet werden.

3 Anwendungsbeispiel für XML

Wir bedienen uns der Kurzgeschichte „Kleine Fabel“ von Franz Kafka (1920) als konkretes Anwendungsbeispiel. Die Version von Projekt Gutenberg ist wie folgt in HTML erfasst:

<h3 class="title">Kleine Fabel</h3>
<p>»Ach«, sagte die Maus, »die Welt wird enger mit jedem Tag. Zuerst war sie so breit,
daß ich Angst hatte, ich lief weiter und war glücklich, daß ich endlich rechts und links
in der Ferne Mauern sah, aber diese langen Mauern eilen so schnell aufeinander zu,
daß ich schon im letzten Zimmer bin, und dort im Winkel steht die Falle, in die ich laufe.«
– »Du mußt nur die Laufrichtung ändern«, sagte die Katze und fraß sie.</p>

Diese Version lässt sich in einem Texteditor wie Visual Studio Code lokal speichern (z.B. als Kafka_Kleine_Fabel.html) und verursacht dort keine Fehlermeldung. Auch lässt sich die Datei in einem Browser öffnen und darstellen.

Denkpause 🤔

  • Was passiert, wenn wir eine Kopie der Datei erstellen und in Kafka_Kleine_Fabel.xml umbenennen, um sie dann in Visual Studio Code zu öffnen?
  • Welche Änderung(en) müssen wir vornehmen, um die Fehlermeldung loszuwerden?

Das XML-Format erwartet eine strikte Einhaltung eines einzelnen Wurzelelements. Deshalb müssen die Elemente <h3> und <p> in einem zusätzlichen Element eingebettet werden. Dadurch verschwindet die Fehlermeldung. Wie das Element heißt, ist im Prinzip unwichtig, da XML erweiterbar ist. Aus diesem Grund ist es auch kein Hindernis, dass ursprüngliche HTML-Tags verwendet werden.

Nun können wir den Text mit zusätzlichen Informationen bereichern, wie z.B.:

  • Direkte Rede, um Textpassagen von Erzähler und Figuren auseinanderzuhalten.
  • Figuren markieren, um unterschiedliche Figuren auseinanderzuhalten.
<xml>
    <titel>Kleine Fabel</titel>
    <p><rede figur="Maus">»Ach«</rede>, sagte die Maus, <rede figur="Maus">»die Welt
    wird enger mit jedem Tag. Zuerst war sie so breit, daß ich Angst hatte, ich lief
    weiter und war glücklich, daß ich endlich rechts und links in der Ferne Mauern sah,
    aber diese langen Mauern eilen so schnell aufeinander zu, daß ich schon im letzten
    Zimmer bin, und dort im Winkel steht die Falle, in die ich laufe.«</rede> –
    <rede figur="Katze">»Du mußt nur die Laufrichtung ändern«</rede>, sagte die
    Katze und fraß sie.</p>
</xml>

4 XML-Schemata

Wir haben nun ein valides XML-Dokument, jedoch haben wir bei der Wahl unserer Tags etwas improvisiert. Prinzipiell dürfen wir das auch, doch bei dieser Vorgehensweise wird bei jedem Ansatz das Rad neu erfunden und Projekte sind untereinander nicht kompatibel. Daher gibt es, je nach Anwendungszweck und Fachrichtung, mehrere XML-Schemata, die gewisse Standards vorgeben, wie z.B.:

  • SVG (Scalable Vector Graphics), um zweidimensionale Vektorgrafiken zu beschreiben.
  • Office Open XML, (.docx, .xlsx, .pptx), für Microsoft Office Dateien.
  • DocBook, zur Beschreibung von Dokumentation vor allem im technischen Umfeld.
  • TEI-XML (Text Encoding Initiative) zur Beschreibung von Texten, inzwischen ein Standard in den Geisteswissenschaften.

Von nun an verwenden wir das Schema TEI-XML, indem wir folgende Zeile am Anfang unserer Datei einfügen:

<?xml-model href="https://tei-c.org/release/xml/tei/custom/schema/relaxng/tei_all.rng"
schematypens="http://relaxng.org/ns/structure/1.0"?>

Damit können Visual Studio Code und dessen installierte XML-Erweiterung auf die Vorgaben des TEI-XML Schemas zugreifen, wie sie auf der Webseite des TEI-Konsortiums im RelaxNG-Format (REgular LAnguage for XML Next Generation) veröffentlicht sind. Dadurch erscheinen Fehlermeldungen, da unser bisheriges „Freestyle“ XML nicht den Vorgaben von TEI-XML entspricht. Eine detaillierte Dokumentation von TEI-XML befindet auf der Seite https://tei-c.org/release/doc/tei-p5-doc/en/html/index.html. Als Änderungen müssen wir minimal folgendes unternehmen:

  • Das Wurzelelement hat <TEI> zu heißen und muss anhand eines bestimmten Attributs auf die TEI-Webseite verweisen: <TEI xmlns="http://www.tei-c.org/ns/1.0">.
  • Vor dem eigentlichen Inhalt hat ein Kopfelement namens <teiHeader> zu stehen, welches wiederum bestimmte Pflichtangaben zu enthalten hat. In Visual Studio Code besteht die Möglichkeit, auf die Glühbirne zu klicken, um die nötigen Elemente einzufügen, die da wären:
    • Titel.
    • Verleger.
    • Beschreibung.
  • Der eigentliche Inhalt hat in einem Element namens <text> zu stehen, wobei der Titel innerhalb des Elements <front> stehen sollte, und der Haupttext innerhalb des Elements <body>.
  • Unsere bisherigen Tags <titel> und <rede> sind nicht zulässig und müssen ersetzt werden:
    • Der Titel gehört innerhalb eines gesonderten Elements namens <docTitle>, in dem wiederum Titelteile als Elemente namens <titlePart> gelistet werden sollen.
    • Textpassagen, die als Rede markiert werden sollen, können mit dem Tag <said who=""> markiert werden (wobei es hier mehrere korrekte Methoden gibt).

Nach diesen Korrekturen haben wir folgendes TEI-XML Dokument, das keine Fehler mehr vorweist:

<?xml-model href="https://tei-c.org/release/xml/tei/custom/schema/relaxng/tei_all.rng"
schematypens="http://relaxng.org/ns/structure/1.0"?>
<TEI xmlns="http://www.tei-c.org/ns/1.0">
    <teiHeader>
        <fileDesc>
            <titleStmt>
                <title>Kleine Fabel</title>
            </titleStmt>
            <publicationStmt>
                <publisher>Projekt Gutenberg-DE</publisher>
            </publicationStmt>
            <sourceDesc>
                <p>Kurzgeschichte von Franz Kafka</p>
            </sourceDesc>
        </fileDesc>
    </teiHeader>
    <text>
        <front>
            <docTitle>
                <titlePart>Kleine Fabel</titlePart>
            </docTitle>
        </front>
        <body>
            <p><said who="Maus">»Ach«</said>, sagte die Maus, <said who="Maus">»die Welt
            wird enger mit jedem Tag. Zuerst war sie so breit, daß ich Angst hatte, ich
            lief weiter und war glücklich, daß ich endlich rechts und links in der Ferne
            Mauern sah, aber diese langen Mauern eilen so schnell aufeinander zu, daß
            ich schon im letzten Zimmer bin, und dort im Winkel steht die Falle, in die
            ich laufe.«</said> – <said who="Katze">»Du mußt nur die Laufrichtung
            ändern«</said>, sagte die Katze und fraß sie.</p>
        </body>
    </text>
</TEI>

Einige genauere Angaben, die über die reine Fehlerkorrektur hinausgehen, können wir noch machen. Und zwar:

  • Die Quelle als URL mit dem Tag <idno>.
  • Das übergeordnete Werk innerhalb eines Tags namens <seriesStmt>.
  • Den Autor mit dem Tag <author> innerhalb eines <bibl> Tags.
  • Die Sprache des Textes mit einem entsprechenden Attribut im <text> Tag.
  • Den Titel als Haupttitel anhand eines entsprechenden Attributs im <titlePart> deklarieren.

Entsprechend dieser zusätzlichen Angaben sieht unser Dokument nun so aus:

<?xml-model href="https://tei-c.org/release/xml/tei/custom/schema/relaxng/tei_all.rng"
schematypens="http://relaxng.org/ns/structure/1.0"?>
<TEI xmlns="http://www.tei-c.org/ns/1.0">
    <teiHeader>
        <fileDesc>
            <titleStmt>
                <title>Kleine Fabel</title>
            </titleStmt>
            <publicationStmt>
                <publisher>Projekt Gutenberg-DE</publisher>
                <idno type="URI">
                    https://www.projekt-gutenberg.org/kafka/misc/chap002.html
                </idno>
            </publicationStmt>
            <seriesStmt>
                <title>Erzählungen II</title>
            </seriesStmt>
            <sourceDesc>
                <bibl>
                    <author>Franz Kafka</author>
                </bibl>
            </sourceDesc>
        </fileDesc>
    </teiHeader>
    <text xml:lang="de">
        <front>
            <docTitle>
                <titlePart type="main">Kleine Fabel</titlePart>
            </docTitle>
        </front>
        <body>
            <p><said who="Maus">»Ach«</said>, sagte die Maus, <said who="Maus">»die Welt
            wird enger mit jedem Tag. Zuerst war sie so breit, daß ich Angst hatte, ich
            lief weiter und war glücklich, daß ich endlich rechts und links in der Ferne
            Mauern sah, aber diese langen Mauern eilen so schnell aufeinander zu, daß
            ich schon im letzten Zimmer bin, und dort im Winkel steht die Falle, in die
            ich laufe.«</said> – <said who="Katze">»Du mußt nur die Laufrichtung
            ändern«</said>, sagte die Katze und fraß sie.</p>
        </body>
    </text>
</TEI>

Beauty-Tipp 💅

Der Text kann deutlich lesbarer gemacht werden, in dem wir einen Darstellungsstil definieren. Dies geschieht am einfachsten mit dem Format CSS (Cascading Style Sheets), mit dem gewöhnlich Webseiten gestylt werden. Wir erstellen eine Datei namens style.css und verweisen am Anfang unseres XML-Dokuments vor dem Wurzelelement <TEI> darauf:

<?xml-stylesheet href="./style.css" type="text/css"?>

Nun können wir in der CSS-Datei den Anzeigestil definieren, in dem wir z.B. eine Schriftart für das gesamte Dokument auswählen, den Titel größer darstellen als den Haupttext, und die Metadaten ausblenden.

text {
    font-family: Georgia, 'Times New Roman', Times, serif;
}

docTitle {
    font-size: 36pt;
    display: block;
}

p {
    font-size: 16pt;
    display: block;
}

teiHeader {
    display: none;
}

Die XML-Datei kann mit einem Webbrowser geöffnet werden, um den Inhalt dem CSS-Stil entsprechend anzusehen.

5 Wozu das Ganze?

Die konsequente Annotation eines Dokuments in einem gängigen XML-Schema (wie z.B. TEI-XML) erleichtert sowohl qualitative als auch quantitative Analysen und macht die erstellten Dateien zugänglicher für andere Forscherinnen und Forscher.

Für eine qualitative Analyse können wir den letzten Beauty-Tipp zu etwas Nützlicherem erweitern. Die zwei Figuren der Kurzgeschichte können farblich hervorgehoben werden, in dem wir in der CSS-Datei den Werten des Attributs im Tag <said who="WERT"> Hintergrundfarben zuordnen.

said[who="Maus"] {
    background-color: orange;
}

said[who="Katze"] {
    background-color: lightblue;
}

Für eine quantitative Analyse, wie z.B. dem automatischen Zählen von Wörtern der unterschiedlichen Figuren, bedarf es in der Regel fortgeschrittener Methoden, die Programmierkenntnisse voraussetzen. Empfohlene Tools für die Analyse von XML-Dateien sind Beautiful Soup für Python sowie xml2 für R. Diese werden wir aufgrund ihrer Komplexität hier nicht besprechen. Siehe jedoch den Bonusabschnitt 10 als Ausblick. Anstelle dessen befassen wir uns mit einer grundlegenderen Methode, nämlich den regulären Ausdrücken. Mit diesen können wir nicht nur rudimentär XML-Dateien analysieren, sondern auch effektiver bearbeiten.

6 Einführung in reguläre Ausdrücke

Reguläre Ausdrücke erlauben flexible und komplexe Suchanfrage dank einer Reihe von Sonderzeichen, die als Wildcards und Quantoren fungieren. Eine kurze Einführung (auf Englisch) befindet sich hier: https://anglistik-toolbox.uni-mannheim.de/app/searchtools/#regular-expressions

Bevor wir reguläre Ausdrücke in unserer XML-Datei anwenden, sollten wir deren Anwendung anhand einfacher Beispiele üben.

Wir können den Quelltext der verlinkten Seiten anhand mehrerer Methoden herunterladen, um darin zu suchen:

  • Im Browser den Quelltext anzeigen, um diesen dann in einen Texteditor wie Visual Studio Code hineinzukopieren.
  • Den Quelltext direkt mit einem Tool wie curl herunterladen, z.B. curl -o Vornamen_P.html https://de.wikipedia.org/wiki/Liste_von_Vornamen/P

Nun können wir mit folgenden regulären Ausdrücken unsere Übungsaufgaben lösen:

  • (?:F|Ph)[ie]l+ip+e?\b: Beginnend mit F oder Ph, gefolgt von einem i oder e, mit einem oder mehreren l, mit einem oder mehreren p, und am Ende optional ein e. Zur Erläuterung: Die Zeichenkette ?: in den Klammern erzeugt eine Gruppierung ohne Rückwärtsreferenz (eng. non-capturing group). Da wir auf den Inhalt der Klammern nicht zurückgreifen werden, ist es die beste Praxis, eine Rückwärtsreferenz zu vermeiden.
  • Paul\w+: Dem Text Paul sollen noch mindestens ein Wortzeichen folgen.
Visuelle Darstellung des regulären Ausdrucks zum Finden der Schreibvarianten von Philipp: (?:F|Ph)[ie]l+ip+e?\b (erstellt mit Debuggex.com)
Visuelle Darstellung des regulären Ausdrucks zum Finden der Vornamen, die von Paul abgeleitet sind: Paul\w+ (erstellt mit Debuggex.com)

7 Verwendung regulärer Ausdrücke zur effizienten Annotation von XML-Dokumenten

In unserem bisherigen Text können wir die alte Rechtschreibung normalisieren. Die Regeln von TEI-XML sehen dafür das Element <choice>, in dem <orig> die Schreibweise aus dem Original beinhaltet und <reg> die heutige Standardschreibweise beinhaltet. Z.B. können damit Verbindungen der alten Schreibweisen mußt und daß zu den aktuellen Varianten musst und dass hergestellt werden. In einem längeren Text wäre das Einfügen der XML-Elemente mit viel Handarbeit verbunden. Anhand von regulären Ausdrücken kann dies mit einem Befehl zum Suchen und Ersetzen von Text erfolgen.

Denkpause 🤔

  • Wie platzieren wir, möglichst effizient, die entsprechenden Tags um die Wörter mußt und dass?
  • Können wir beide Wörter möglicherweise in einem Durchgang behandeln?
  • Wenn wir die Wörter einzeln behandeln, können wir folgende Ersetzungen (ganz ohne reguläre Ausdrücke) vornehmen:
    • mußt<choice><orig>mußt</orig><reg>musst</reg></choice
    • daß<choice><orig>daß</orig><reg>dass</reg></choice
  • Um die Bearbeitung effizienter zu gestalten (auch in Hinblick auf längere Texte mit weiteren Wörtern wie muß, mußte, Fluß, Paß, usw.), können wir alle Wörter in einem Durchgang behandeln:
    • Wir können zu den Zeichen vor und nach dem Buchstaben ß Rückwärtsreferenzen (eng. capturing groups) mit () Klammern erzeugen und beim Ersetzen wiedergeben. Dabei ist zu beachten, dass Visual Studio Code hierfür das Format $1 erwartet, statt \1 wie in anderen „Dialekten“ regulärer Ausdrücke.
    • Wenn wir unseren regulären Ausdruck zu weit definieren, schließen wir auch ungewollte Treffer wie fraß mit ein: (\w+)ß(\w+)?.
    • Anstelle dessen können wir die Anfangsbuchstaben, die vor dem ß vorkommen, in der ersten Gruppe auflisten: (mu|da)ß(\w+)?.
    • Somit ergibt sich folgendes Muster zum Suchen und Ersetzen: (mu|da)ß(\w+)?<choice><orig>$1ß$2</orig><reg>$1ss$2</reg></choice>.
Visuelle Darstellung des zu weit gefassten regulären Ausdrucks zum Finden der Schreibvarianten nach alter Rechtschreibung: (\w+)ß(\w+)? (erstellt mit Debuggex.com)
Visuelle Darstellung des regulären Ausdrucks zum Finden der Schreibvarianten nach alter Rechtschreibung: (mu|da)ß(\w+)? (erstellt mit Debuggex.com)

8 Extrahieren von Information aus XML-Dokumenten

Anhand regulärer Ausdrücke können wir die durch Annotation vermerkte Information konsequent auswerten. In der Regel geschieht dies unter Verwendung von Programmiersprachen, jedoch können wir auch mit rudimentären Methoden ähnliche Ergebnisse erzielen. Wenn wir z.B. wissen möchten, wie viele Wörter jede der Figuren in der Kurzgeschichte spricht, können wir einfach die Wörter innerhalb der Elemente <said who="WERT"> isolieren, um darin Wörter zu zählen.

Die Funktionen zum Suchen und Ersetzen innerhalb von Visual Studio Code sind dafür zu begrenzt. Daher benutzen wir Kommandozeilenbefehle, um nach Suchkriterien Wörter zu zählen.1 Diese Befehle sind auf bestimmte Aufgaben spezialisiert, weshalb wir für jeden Schritt einen separaten Befehl verwenden:

  1. Zum Finden der relevanten Textabschnitte verwenden wir grep (global regular expression print) bzw. egrep (extended grep).
  2. Zum Ersetzen von Text verwenden wir sed (stream editor).
  3. Zum Zählen von Wörtern verwenden wir wc (word count).

Denkpause 🤔

  1. Welchen Suchbegriff können wir verwenden, um alle Passagen direkter Rede der Figur Maus zu finden?
  2. Wie löschen wir aus diesen Passagen die Tags, um nur den tatsächlichen Text zu erhalten?

Für Schritt 1 machen wir uns die Tags <said> zu Nutze, insbesondere diejenigen mit dem Attribut who="Maus". Nach dem Starttag und Endtag kann direkt gesucht werden, jedoch müssen wir überlegen, wie wir den Text dazwischen miteinbeziehen. Eine einfache Wildcard .* ist problematisch, da Quantoren wie * standardmäßig „gierig“ (greedy) sind. Konkret bedeutet dies, das mit dem Suchbegriff <said who="Maus">.*</said> die Suche nicht beim ersten Endtag </said> endet, sondern beim letztmöglichen Treffer, also dem zweiten Endtag in der Zeile. Das bedeutet, dass wir die ungewollte Passage , sagte die Maus, mit einbeziehen würden. Wir können den Quantor * „genügsam/zurückhaltend“ (non-greedy/lazy) machen, indem wir ein Fragezeichen anhängen. Somit erhalten wir folgenden Suchbegriff: <said who="Maus">.*?</said>.

Visuelle Darstellung des regulären Ausdrucks zum Finden der direkten Rede der Figur Maus: <said who="Maus">.*?</said> (erstellt mit Debuggex.com)

Mit dem Befehl grep können wir anhand dieses regulären Ausdrucks die relevanten Passagen extrahieren. In unserem Terminal wechseln wir mit dem Befehl cd zum Ordner, in dem sich unsere XML-Datei befindet. Standardmäßig gibt grep die komplette Zeile wieder, in der sich mindestens ein Treffer befindet. Da wir nur die tatsächlichen Treffer wollen, geben wir die Option -o (only) an. Da es sich beim Wechsel von einem „gierigen“ zu einem „genügsamen“ Quantor um einen erweiterten regulären Ausdruck handelt, geben wir entweder die Option -E (extended) an oder verwenden die Variante egrep. Unser Befehl sieht dann wie eine der folgenden Varianten aus:

grep -E -o '<said who="Maus">.*?</said>' Kafka_Kleine_Fabel_TEI.xml
egrep -o '<said who="Maus">.*?</said>' Kafka_Kleine_Fabel_TEI.xml

Dadurch sollten uns nur die Passage direkter Rede der Figur Maus, inklusive der Tags, angezeigt werden. Diesen angezeigten Text nutzen wir nun als Grundlage für unseren Schritt 2, in dem wir alle Tags entfernen. Dafür verwenden wir die Funktion zum Suchen und Ersetzen von sed, die folgende Syntax erwartet: sed 's/alt/neu/g'. Das s steht für substitution und das g für global. Da sed keine erweiterten regulären Ausdrücke unterstützt, können wir nicht analog zu vorhin <.*?> als Suchbegriff verwenden und müssen uns eine andere Strategie ausdenken, um die Tags zu entfernen. Anstatt ein beliebiges Zeichen als Wildcard zu verwenden, definieren wir unsere Wildcard negativ als alle Zeichen, die nicht > sind. Somit stellen wir sicher, dass unser Suchbegriff nie die Grenzen eines einzelnen Tags übertrifft. In Kombination mit dem vorherigen Schritt sieht unser Befehl nun wie eine der folgenden Varianten aus:

grep -E -o '<said who="Maus">.*?</said>' Kafka_Kleine_Fabel_TEI.xml |
    sed 's/<[^>]*>//g'

egrep -o '<said who="Maus">.*?</said>' Kafka_Kleine_Fabel_TEI.xml |
    sed 's/<[^>]*>//g'
Visuelle Darstellung des regulären Ausdrucks zum Finden von Tags: <[^>]*> (erstellt mit Debuggex.com)

Nun sollte die Passage direkter Rede der Figur Maus ohne Tags angezeigt werden. Zwar sind alte und neue Schreibweisen beide präsent und aneinandergeklebt, wie z.B. daßdass, jedoch ist dies für unseren Zweck des Wörterzählens kein Problem. Diesen neuen Text verwenden wir als Grundlage für unseren letzten Schritt 3, in dem wir den Befehl wc verwenden. Standardmäßig liefert dieser drei Werte, nämlich die Anzahl der Zeilen, Wörter und Zeichen. Da uns nur die Wörter interessieren, geben wir die Option -w an. Somit sieht unser endgültiger Befehl wie eine der folgenden Varianten aus:

grep -E -o '<said who="Maus">.*?</said>' Kafka_Kleine_Fabel_TEI.xml |
    sed 's/<[^>]*>//g' |
    wc -w
    
egrep -o '<said who="Maus">.*?</said>' Kafka_Kleine_Fabel_TEI.xml |
    sed 's/<[^>]*>//g' |
    wc -w

9 Annotation von Personen- und Ortsnamen

Als Übungsbeispiel speichern wir die Wikipedia Artikel zu Georg Friedrich Händel in mehreren Sprachen, z.B. Deutsch, Englisch und Französisch, und bereiten die Texte für TEI-XML vor.

Zum Herunterladen der Texte können wir curl verwenden, um den Quelltext der Artikel lokal als HTML-Dateien zu speichern:

curl -o Händel_DE.html https://de.wikipedia.org/wiki/Georg_Friedrich_H%C3%A4ndel
curl -o Händel_EN.html https://en.wikipedia.org/wiki/George_Frideric_Handel
curl -o Händel_FR.html https://fr.wikipedia.org/wiki/Georg_Friedrich_Haendel

Die Dokumente enthalten viel unerwünschtes Material, vor allem die Navigation innerhalb von Wikipedia betreffend. Daher behalten wir nur den tatsächlichen Inhalt der Artikel in Form der <p>-Elemente. Das ist insofern günstig, als dieser Tag auch in TEI-XML valide ist. Da reguläre Ausdrücke standardmäßig auf Muster innerhalb einer Zeile ausgelegt sind und das Suchen über mehrere Zeilen hinweg kompliziert ist, machen wir uns das Leben leichter, indem wir alle Zeilenumbrüche in der Datei mit Leerzeichen ersetzen. In Visual Studio Code erreichen wir dies, indem wir \n (Sonderzeichen für Zeilenumbrüche) mit (Leerzeichen) ersetzen.

Nun können wir anhand von grep bzw. egrep die <p>-Elemente extrahieren und in eine neue Datei speichern. Dadurch erhalten wir eine Version des Inhalts, in der jedes <p>-Element auf einer eigenen Zeile steht.

egrep -o '<p>.*?</p>' Händel_DE.html > Händel_DE_p.html

Innerhalb der <p>-Elemente haben wir noch weitere HTML-Elemente, die nicht Teil des TEI-XML Standards sind, wie z.B. <i> für kursiven Text, <b> für fettgedruckten Text oder <a> für Hyperlinks. Hier könnte man überlegen, diese Tags durch ihre TEI-XML-Entsprechungen zu ersetzen. Um das Beispiel möglichst einfach zu halten, werden wir sie zunächst löschen. Um alle Tags außer <p> zu löschen, haben wir zwei Möglichkeiten:

  1. Wir löschen zunächst alle Tags, um dann am Zeilenanfang und Zeilenende jeweils Starttags und Endtags wieder einzuführen. In Visual Studio Code erfassen wir und löschen alle Tags mit dem regulären Ausdruck <.*?>. Anschließend setzen wir an jeden Zeilenanfang ein Starttag: ^<p>. Am Zeilenende setzen wir die entsprechenden Endtags: $</p>.
  2. Wir löschen direkt alle Tags außer <p>. Dazu verwenden wir ein Konstrukt namens look-ahead assertion („vorausschauende Annahme“). Mit einer negativen look-ahead assertion können wir bestimmen, dass die öffnende Tagklammer nicht von p oder /p gefolgt werden darf: <(?!p|/p).*?>.2

Nun können wir die Datei im XML-Format speichern, z.B. als Händel_DE.xml. Zunächst erhalten wir in Visual Studio Code eine Fehlermeldung, da noch kein XML-Schema wie TEI-XML festgelegt ist und wir kein Wurzelelement haben. Wir können die Zeilen aus Abschnitt 4 oder aus unserer vorherigen XML-Datei kopieren.

Denkpause 🤔

Was müssen wir minimal der Datei hinzufügen, um alle Fehlermeldungen zu beseitigen?

Für die Annotation von Namen sehen die TEI-XML-Richtlinien das Tag <name> mit dem Attribut type vor, um zwischen Personen, Orten und Organisationen zu unterscheiden. Im vorliegenden Text könnten beispielsweise folgende Namen annotiert werden:

<name type="person">Georg Friedrich Händel</name>
<name type="place">London</name>
<name type="org">Georg-Friedrich-Händel-Gesellschaft</name>

Um nicht alle Namen per Hand annotieren zu müssen (z.B. kommt London über 30-mal vor) können wir per Suchen und Ersetzen die Annotation effizienter durchführen. Jedoch müssen wir bei Personennamen darauf achten, dass diese im Text nicht durchgehend gleich genannt werden: anfangs wird Georg Friedrich Händel mit ganzem Namen bezeichnet, später jedoch nur als Händel. Daher können wir uns nicht damit begnügen, den kompletten Namen anhand von Suchen und Ersetzen zu annotieren. Genauso wenig können wir alle Vorkommen des Namens Händel pauschal als Erwähnungen des Komponisten annotieren, da im Text auch dessen Eltern Georg Händel und Dorothea Händel erwähnt werden. Wir können Erwähnungen des Komponisten mit und ohne Vornamen unter Ausschluss seiner Eltern wie folgt effizient annotieren:

  • Suchbegriff: ((?:George? Frie?de?rich?)?\s?(?<!Georg |Dorothea )H(?:ä|a)ndels?)
  • Ersatz: <name type="person">$1</name>

Somit haben wir über 200 Instanzen des Namens annotiert und nur eine verfehlt, in der der zweite Vorname zu „Fr.“ abgekürzt wurde. Hier kann man abwägen, ob dieser Einzelfall einfach per Hand korrigiert wird, oder ob der reguläre Ausdruck umformuliert wird, um auch etwaige weitere Abkürzungen der Vornamen zu erfassen (wie sie z.B. in weiteren Texten vorkommen könnten). Ein aktualisierter regulärer Ausdruck als Suchbegriff könnte wie folgt aussehen:

((?:G[\w\.]+ F[\w\.]+)?\s?(?<!Georg |Dorothea )H(?:ä|a)ndels?)
Visuelle Darstellung des regulären Ausdrucks zum Finden aller Varianten des Namens von Georg Friedrich Händel: ((?:G[\w\.]+ F[\w\.]+)?\s?(?<!Georg |Dorothea )H(?:ä|a)ndels?) (erstellt mit Debuggex.com)

Idealerweise sollten wir vermerken, dass es bei sich diesen Namen um Referenzen zur selben Person handelt. Zu diesem Zweck können wir in Tags Referenzen einfügen, die auf ein zentrales Verzeichnis von erwähnten Personen, Orten und Organisationen verweisen. Dieses Verzeichnis kann im <teiHeader> des Dokuments stehen, genauer gesagt unter folgendem Konstrukt:

<teiHeader>
    ...
    <encodingDesc>
        <classDecl>
            <taxonomy>
                <desc>
                    <listPerson>
                        <person xml:id="gfhaendel">
                            <persName xml:lang="de">
                                Georg Friedrich Händel
                            </persName>
                            <persName xml:lang="en">
                                George Frideric Handel
                            </persName>
                            <birth when="1685-03-05" />
                            <death when="1759-04-14" />
                        </person> 
                    </listPerson>
                </desc> 
            </taxonomy>
        </classDecl>
    </encodingDesc>
</teiHeader>

In den Tags kann nun ein Verweis auf das Kürzel gfhaendel gesetzt werden. Dadurch wird das Attribut type="person" überflüssig, sodass wir folgende Ersetzung durchführen könnten:

  • Suchbegriff: <name type="person">
  • Ersatz: <name ref="#gfhaendel">

Jedoch ist diese Lösung nur sinnvoll, wenn das ganze Projekt aus nur einem XML-Dokument besteht. Realistischer ist eine Dokumentensammlung, sodass ein zentrales Verzeichnis in der Regel sinnvoller ist. Zu diesem Zweck können wir einen Ordner erstellen, z.B. mit dem Namen ref, in dem wir in XML-Dateien Verzeichnisse für Personen, Orte und Organisationen zentral erstellen, z.B. als personen.xml, orte.xml und organisationen.xml. Die Datei personen.xml kann z.B. wie folgt gestaltet werden:

<?xml-model href="https://tei-c.org/release/xml/tei/custom/schema/relaxng/tei_all.rng"
schematypens="http://relaxng.org/ns/structure/1.0"?>
<TEI xmlns="http://www.tei-c.org/ns/1.0">
    <teiHeader>
        <fileDesc>
            <titleStmt>
                <title>Verzeichnis erwähnter Personen</title>
            </titleStmt>
            <publicationStmt>
                <publisher>Name des Projekts</publisher>
            </publicationStmt>
            <sourceDesc>
                <p>Verzeichnis erwähnter Personen</p>
            </sourceDesc>
        </fileDesc>
    </teiHeader>
    <text>
        <body>
            <listPerson>
                <person xml:id="gfhaendel">
                    <persName xml:lang="de">Georg Friedrich Händel</persName>
                    <persName xml:lang="en">George Frideric Handel</persName>
                    <birth when="1685-03-05" />
                    <death when="1759-04-14" />
                </person>
                <person xml:id="ghaendel">
                    <persName>Georg Händel</persName>
                    <birth when="1622-09-24" />
                    <death when="1697-02-11" />
                </person>
                <person xml:id="dhaendel">
                    <persName>Dorothea Händel</persName>
                    <birth when="1651" />
                    <death when="1730" />
                </person>
                <person xml:id="jsbach">
                    <persName>Johann Sebastian Bach</persName>
                    <birth when="1685-03-31" />
                    <death when="1750-07-28" />
                </person>
            </listPerson>
        </body>
    </text>
</TEI>

Somit verlagern wir das Verzeichnis vom <teiHeader> des Dokuments in eine zentrale Datei. Somit kann die Referenz wie folgt ausgedrückt werden:

<name ref="ref/personen.xml#gfhaendel">Georg Friedrich Händel</name>

Diese Methode hat jedoch zwei Nachteile:

  1. Die Referenz ist lang, was das Lesen der Datei erschwert und fehleranfällig sein kann.
  2. Sollte die Ordnerstruktur des Projekts umorganisiert werden, müssten alle Referenzen aktualisiert werden. Mit Suchen und Ersetzen könnte die Aktualisierung leicht vollzogen werden, jedoch kann der Schritt komplett vermieden werden.

Stattdessen können abgekürzte Präfixe definiert werden, wie z.B. psn für Personen, loc für Orte und org für Organisationen, die dann zu den entsprechenden Dateien verweisen. Innerhalb des <teiHeader> können die Präfixe wie folgt definiert werden:

<teiHeader>
    ...
    <encodingDesc>
        <listPrefixDef>
            <prefixDef
                ident="psn"
                matchPattern="([a-z]+)"
                replacementPattern="ref/personen.xml#$1"
            />
            <prefixDef
                ident="loc"
                matchPattern="([a-z]+)"
                replacementPattern="ref/orte.xml#$1"
                />
            <prefixDef
                ident="org"
                matchPattern="([a-z]+)"
                replacementPattern="ref/organisationen.xml#$1"
            />
            </listPrefixDef>
        </encodingDesc>
</teiHeader>

Sind diese Präfixe definiert und die Details in den zentralen Dateien hinterlegt, können innerhalb mehrerer XML-Dateien Verweise zu denselben Entitäten gemacht werden, wie z.B.:

<!-- im deutschen Text: -->
<name ref="psn:gfhaendel">Georg Friedrich Händel</name>
<name ref="psn:gfhaendel">Georg Fr. Händel</name>
<name ref="psn:gfhaendel">Händel</name>
<name ref="psn:jsbach">Johann Sebastian Bach</name>
<name ref="loc:london">London</name>
<!-- im englischen Text: -->
<name ref="psn:gfhaendel">George Frideric Handel</name>
<name ref="psn:gfhaendel">Handel</name>
<name ref="psn:jsbach">Johann Sebastian Bach</name>
<name ref="loc:london">London</name>
<!-- im französischen Text: -->
<name ref="psn:gfhaendel">Georg Friederich Haendel</name>
<name ref="psn:gfhaendel">Haendel</name>
<name ref="psn:jsbach">Jean-Sébastien Bach</name>
<name ref="loc:london">Londres</name>

10 Bonus: Ausblick auf „Best Practice“-Methoden

Wir haben reguläre Ausdrücke sowohl zur effizienten Annotation von XML-Dokumenten als auch zu deren Analyse verwendet. Letzteres entspricht jedoch nicht den bewährten Methoden, da erstens die Verwendung regulärer Ausdrücke für diesen Zweck fehleranfällig sein kann und wir darüber hinaus die Struktur des XML-Dokuments nicht optimal nutzen. Idealerweise sollte ein XML-Dokument geparst werden, d.h. dass dessen Struktur analysiert und die Baumstruktur durchsuchbar gemacht wird. Dies bedarf in der Regel Programmierkenntnisse, weshalb wir diese Methode heute nicht verwendet haben.

Als einer der Standards gilt das Modul Beautiful Soup für die Programmiersprache Python. Als Beispiel wiederholen wir das Zählen von Wörtern in der direkten Rede einer Figur.

from bs4 import BeautifulSoup

with open("Kafka_Kleine_Fabel_TEI.xml", "r") as inp:
    content = inp.read()

tree = BeautifulSoup(content, "xml")

character = "Maus"

character_speech = tree.find_all("said", who=character)

word_count = 0

for match in character_speech:
    current_speech = match.get_text()
    current_word_count = len(current_speech.split())
    word_count = word_count + current_word_count

print(
    "The direct speech word count for the "
    f"character {character} is: {word_count}"
    )

Alternativ zu Beautiful Soup gibt es für die Programmiersprache R das Modul xml2. Wir bedienen uns des gleichen Beispiels wie vorhin und verwenden xml2 zusammen mit dem Modul tidyverse.

library(xml2)
library(tidyverse)

xml_data <- read_xml("Kafka_Kleine_Fabel_TEI.xml")

xml_ns(xml_data)
# d1 <-> http://www.tei-c.org/ns/1.0

maus_word_count <- xml_data %>%
  xml_find_all(".//d1:said[@who='Maus']", xml_ns(xml_data)) %>%
  xml_text() %>%
  str_flatten(collapse = " ") %>%
  str_count(pattern = "\\S+")

paste(
  "The direct speech word count for the character Maus is:",
  maus_word_count
)

11 Wie speichere ich dieses Skript auf meinen Computer?

curl -o xml_regex.html https://linguistics.percillier.eu/doc/xml_regex.html

Tagesworkshop: Einführung in XML und reguläre Ausdrücke (MaDaLi2 Zertifikat Data Literacy) © 2025 von Michael Percillier ist lizensiert unter: CC BY 4.0


  1. Diese Befehle sind standardmäßig auf macOS und Linux Betriebssysteme vorinstalliert. Für Windows empfiehlt sich die Installation des Windows-Subsystems für Linux (WSL).↩︎

  2. Eine entsprechende positive look-ahead assertion, wonach die öffnende Tagklammer von p oder /p gefolgt werden muss, wäre: <(?=p|/p).*?>. Es gibt ebenfalls positive und negative look-behind assertions. Damit bestimmt wann, welcher Ausdruck vorausgehen muss bzw. nicht vorausgehen darf. Um z.B. gezielt im Text nach Georg Händel, dem Vater von Georg Friedrich Händel zu suchen, kann folgender regulärer Ausdruck verwendet werden: (?<=Georg )Händel. Um genau diesen auszuschließen: (?<!Georg )Händel. Zusammengefasst werden look-ahead assertions und look-behind assertions als look-around assertions bezeichnet.↩︎