Workshop - Arbeiten mit regulären Ausdrücken

Folie - Arbeiten mit regulären Ausdrücken Folie - Arbeiten mit regulären Ausdrücken

Ein wichtiges Werkzeug beim Arbeiten mit Daten sind reguläre Ausdrücke. Diese werden in OpenRefine von verschiedenen Funktionen unterstützt.

Einführung

Reguläre Ausdrücke in der OpenRefine Dokumentation.

Dieser Workshop wurde erstellt mit OpenRefine Version 3.5.0.
Dieser Workshop wurde zuletzt getestet mit OpenRefine Version 3.7.9.

Mit regulären Ausdrücken können wir Muster definieren, mit denen wir bestimmte Zeichenfolgen in einem Text finden, extrahieren und/oder ersetzen können. Dies funktioniert nicht nur in Programmiersprachen wie Python oder Programmen wie OpenRefine, sondern auch in frei verfügbaren Text Editoren wie Notepad++ oder Visual Studio Code.

[Perl Problems](https://xkcd.com/1171/) von Randall Munroe unter [CC BY-NC Lizenz](http://creativecommons.org/licenses/by-nc/2.5/).
Perl Problems von Randall Munroe unter CC BY-NC Lizenz.

Bei Memes zu regulären Ausdrücken wird wie in Abbildung 1 häufig der Eindruck erweckt, dass sie mehr Probleme erzeugen als sie tatsächlich lösen. Richtig dosiert sind sie jedoch ein hilfreiches Werkzeug bei der Datenextraktion.

Das Vorgehen beim Entwickeln von regulären Ausdrücken unterscheidet sich von Person zu Person. Wir zeigen hier unser bevorzugtes Vorgehen, von dem gerne abgewichen werden darf.

Zum Entwickeln, Testen und Verstehen von regulären Ausdrücken, nutzen wir RegEx101 (Alternative: RegExr). Hier bekommen wir wie in Abbildung 2 visuelles Feedback, auf welche Textzeilen unsere regulären Ausdrücke passen und was die einzelnen Bestandteile bedeuten.

Bildschirmfoto von [RegEx101](https://regex101.com/) mit Art *ECMAScript*, einem regulären Ausdruck zur Extraktion von **Datumsangaben** in der Mitte oben, mehreren Beispielen direkt darunter und hilfreichen Informationen auf der rechten Seite.
Bildschirmfoto von RegEx101 mit Art ECMAScript, einem regulären Ausdruck zur Extraktion von Datumsangaben in der Mitte oben, mehreren Beispielen direkt darunter und hilfreichen Informationen auf der rechten Seite.

Die Verwendung eines visuell unterstützenden Werkzeuges wie RegEx101 hat den Vorteil, dass wir einen übersichtlichen regulären Ausdruck entwickeln können, den wir dann in unsere Ursprungsanwendung, wie zum Beispiel OpenRefine, übertragen können. Ein Nachteil ist, dass zum Beispiel in OpenRefine nicht alle Features von regulären Ausdrücken unterstützt werden und der reguläre Ausdruck nachträglich ggf. angepasst werden muss.

Vorbereitung

Wir erstellen uns eine kleine Sammlung von Beispielen für Daten, die wir mit einem regulären Ausdruck bearbeiten wollen. Dazu ergänzen wir einige Negativbeispiele, also Beispiele, die von unserem regulären Ausdruck nicht extrahiert werden sollten.

Die Beispiele für dieses Tutorial sind im Folgenden gelistet und wir kopieren sie in das Testfenster von RegEx101.

2021-11-25 Einsteigerworkshop 1
2021-12-08 Einsteigerworkshop 2
2022-04-07 Einsteigerworkshop 3
2022-04-13 Einsteigerworkshop 4
2022-05-03 Fortgeschrittenenworkshop
1970-01-01
2005-1-1
- - - - - - - - - - - - - - - - - - -
Do not match!
1234-56-78
2022-13-01
2022-01-32
12-34-56
abcd-ef-gh
Wir werden bei den folgenden Aufgaben teilweise keine “perfekten” regulären Ausdrücke entwickeln, also teilweise Treffer in der Liste der Negativbeispiele haben!

Reguläre Ausdrücke werden von verschiedenen Programmiersprachen und Werkzeugen unterstützt. Dabei unterscheidet sich die Art, wie reguläre Ausdrücke als solche markiert werden. Außerdem werden je nach Programmiersprache, Version der Programmiersprache oder Werkzeug nicht alle Features von regulären Ausdrücken unterstützt.

In RegEx101 können wir daher ein so genanntes Flavor für die Eingabe der regulären Ausdrücke auswählen. Wir verwenden für unser Tutorial “ECMAScript (JavaScript)”. Das hat den Hintergrund, dass sich GREL an der Syntax für JavaScript orientiert, auch wenn es im Hintergrund Java zur Auswertung verwendet. Reguläre Ausdrücke werden hier durch zwei Schrägstriche /.../ markiert.

Hinter dem regulären Ausdruck gibt es optional noch Optionen. Wir verwenden für RegEx101 die Optionen g und m. Das sind die Standardoptionen, die bewirken, dass ein regulärer Ausdruck auf den kompletten Text (g für global) und auch Zeilen übergreifend (m für multiline) angewendet werden kann.

Wir haben sie in den Beispielen explizit gesetzt, damit sie beim Kopieren und Einfügen von regulären Ausdrücken hier aus dem Tutorial nicht ungewollt deaktiviert werden. In OpenRefine benötigen wir diese Optionen für unser Tutorial nicht.

Aufgabe 1: Zeichenauswahl (Character Classes)

Wir wollen die Datumsangaben extrahieren. Diese haben das Muster Jahreszahl-Monat-Tag bzw. ZahlZahlZahlZahl-ZahlZahl-ZahlZahl. Ein erster Ansatz ist es, dies direkt zu formulieren.

/(0|1|2|3|4|5|6|7|8|9)(0|1|2|3|4|5|6|7|8|9)(0|1|2|3|4|5|6|7|8|9)(0|1|2|3|4|5|6|7|8|9)-(0|1|2|3|4|5|6|7|8|9)(0|1|2|3|4|5|6|7|8|9)-(0|1|2|3|4|5|6|7|8|9)(0|1|2|3|4|5|6|7|8|9)/gm

Mit dem Pipe-Symbol | können wir in regulären Ausdrücken Alternativen formulieren. Also zum Beispiel 0|1. Mit den runden Klammern ( und ) können wir die Alternativen gruppieren.

Dies ist etwas umständlich und unleserlich. Daher gibt es in regulären Ausdrücken auch ein Muster zur Zeichenauswahl, welches die eckigen Klammern [ und ] verwendet. Mit den Character Classes lässt sich der obige Ausdruck vereinfachen zu:

/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/gm

Die Zeichenauswahl lässt sich kombinieren und limitieren. So lassen sich Zahlen im Hexadezimalsystem identifizieren mit [0-9A-F].

Aufgabe: Passen Sie den obigen regulären Ausdruck so an, dass das erste Zeichen der Jahreszahl des Datums nur eine 1 oder 2 sein darf.

Lösung:

/[1-2][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/gm
/(1|2)[0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/gm

Aufgabe 2: Zeichenklassen (Shape Matching)

Alternativ zu der recht flexiblen Zeichenauswahl gibt es vordefinierte Zeichenklassen, die es einfacher machen, die Form des Musters zu beschreiben.

KlasseSteht fürZeichenauswahlBeschreibung
\ddigit[0-9]Beliebige Zahl
\wword[a-zA-Z0-9_]Buchstaben in Groß- oder Kleinschreibung, Zahlen und Unterstrich.
\sspaceLeerraum: Leerzeichen, Tabulator, …
\bboundaryWortgrenze, erfasst kein separates Zeichen.

Mit der Klasse \d für Zahlen sieht der reguläre Ausdruck aus Aufgabe 1 wie folgt aus:

/\d\d\d\d-\d\d-\d\d/gm

Aufgabe: Passen Sie den obigen regulären Ausdruck so an, dass er (nur) auf das Datumsformat 2005-1-1 passt.

Lösung:

/\d\d\d\d-\d-\d/gm

Aufgabe 3: Verneinungen

Die Zeichenauswahl oder Zeichenklasse lässt sich auch negieren. Bei der Zeichenauswahl funktioniert das mit dem Sonderzeichen ^, bei den Zeichenklassen durch Großschreibung.

  • Negierte Zeichenauswahl für Zahlen: [^0-9]
  • Negierte Zeichenklasse für Zahlen: \D

Aufgabe: Negieren Sie die regulären Ausdrücke für das Datum aus Aufgabe 1 und Aufgabe 2.

Lösung:

/[^0-9][^0-9][^0-9][^0-9]-[^0-9][^0-9]-[^0-9][^0-9]/gm
/\D\D\D\D-\D\D-\D\D/gm

Aufgabe 4: Quantoren (Quantifiers)

Anstatt Zeichen, Zeichenauswahl oder Zeichenklassen wiederholt zu schreiben, lassen sich Wiederholungen auch durch Quantoren definieren.

QuantorAlternativeBeschreibung
{2,}Mindestens zweimal
{1,2}Mindest einmal, höchstens zweimal
?{0,1}Kann vorkommen
+{1,}Kann sich wiederholen
*{0,}Kann vorkommen und sich wiederholen

Ein möglicher regulärer Ausdruck für unsere Daten wäre also:

/\d{4}-\d\d?-\d\d?/gm

Aufgabe: Schreiben Sie den obigen regulären Ausdruck so um, dass er den ? Quantor nicht mehr verwendet.

Lösung:

/\d{4}-\d{1,2}-\d{1,2}/gm

Aufgabe 5: Gruppierungen (Group Constructs)

Mit den runden Klammern ( und ) lassen sich Gruppen in regulären Ausdrücken definieren. Das ist nicht nur hilfreich, wenn man mit dem Pipe Symbol | Alternativen definieren möchte, sondern auch um einzelne Bestandteile zu markieren.

Setzen wir die einzelnen Bestandteile des regulären Ausdrucks in runde Klammern, dann zeigt uns RegEx101 wie in Abbildung 3 in Gruppe 1 die Jahreszahl, in Gruppe 2 den Monat und in Gruppe 3 den Tag. Auch bei der Verwendung von regulären Ausdrücken in OpenRefine können wir auf die einzelnen Gruppen zugreifen.

/(\d{4})-(\d{1,2})-(\d{1,2})/gm
Bildschirmfoto von [RegEx101](https://regex101.com/) mit den extrahierten Gruppen.
Bildschirmfoto von RegEx101 mit den extrahierten Gruppen.

Die einzelnen Gruppen können auch benannt werden um dann wie in Abbildung 4 über den Gruppennamen aufgerufen werden.

/(?<Jahr>\d{4})-(?<Monat>\d{1,2})-(?<Tag>\d{1,2})/gm
Bildschirmfoto von [RegEx101](https://regex101.com/) mit den benannten Gruppen.
Bildschirmfoto von RegEx101 mit den benannten Gruppen.

Aufgabe: Erstellen Sie einen regulären Ausdruck mit den zwei benannten Gruppen Datum und Event. Das erwartete Ergebnis ist in Abbildung 5 gezeigt.

Bildschirmfoto von [RegEx101](https://regex101.com/) mit den erwarteten Ergebnissen.
Bildschirmfoto von RegEx101 mit den erwarteten Ergebnissen.
Lösung:

/(?<Datum>\d{4}-\d{1,2}-\d{1,2}) (?<Event>[a-zA-Z0-9 ]+)/gm
/(?<Datum>\d{4}-\d{1,2}-\d{1,2}) (?<Event>.+)/gm

In OpenRefine 3.5, 3.6 und 3.7 werden benannte Gruppierungen nicht überall unterstützt. Beispielsweise wird bei find() immer nur der vollständige Treffer zurückgegeben und bei match() die Gruppen ausschließlich durchnummeriert. Lediglich bei replace() lässt sich auf die benannten Gruppen zugreifen.

Der folgende GREL Ausdruck nutzt benannte Gruppen um mit replace() die Reihenfolge des Datums umzudrehen:

value.replace(/(?<Jahr>\d{4})-(?<Monat>\d{1,2})-(?<Tag>\d{1,2})/, "${Tag}-${Monat}-${Jahr}")

Aufgabe 6: Look-around (Look-ahead/Look-behind)

Manchmal möchte man die Daten weiter einschränken.

Hier ein einfaches Beispiel um nur Daten aus dem Jahr 2022 zu extrahieren:

/2022-\d{1,2}-\d{1,2}/gm

Aufwendiger wird es, wenn ein Datum nur extrahiert werden soll, wenn ein bestimmter Begriff dahinter steht. Dies funktioniert mit so genannten Look-around Anweisungen. Man kann via positive Look-ahead (?=...) definieren, dass ein Begriff nach einem Muster vorkommen soll und via positive Look-behind (?<=...) kann definiert werden, dass ein Begriff vor einem Muster vorkommen soll.

So findet der folgende reguläre Ausdruck alle Daten unserer Einsteigerworkshops, ohne den Begriff “Einsteigerworkshop” mit in das Ergebnis aufzunehmen:

/\d{4}-\d{1,2}-\d{1,2}(?= Einsteigerworkshop)/gm

Die Look-around Anweisungen lassen sich auch verneinen, indem man das Gleichheitszeichen = durch ein Ausrufezeichen ! austauscht. So findet der folgende reguläre Ausdruck mit einem negative Look-ahead (?!...) alle Daten von nicht Einsteigerworkshops:

/\d{4}-\d{1,2}-\d{1,2} (?!Einsteigerworkshop)/gm

Aufgabe: Extrahieren Sie die Titel aller drei Workshops im Jahr 2022. Nutzen Sie dazu eine positive Look-behind Anweisung (?<=...).

Hinweise:

  • Quantoren wie +, *, ?, {1,9} werden in Look-around Gruppen nicht überall unterstützt! Das kommt auf die benutzte Programmiersprache, deren Version und/oder verwendeten Zusatzbibliotheken an. Daher im Zweifel erst einmal testen!
  • Look-around Anweisungen verlangsamen die Auswertung eines regulären Ausdrucks deutlich und sollten daher mit Bedacht eingesetzt werden.

Lösung:

/(?<=2022-\d\d-\d\d )(?<Titel>[a-zA-Z0-9 ]+)/gm

Aufgabe 7: Datums Einschränkungen

Mit einer Kombination von Alternativen, Gruppierungen, Zeichenauswahl und Quantoren lässt sich auch ein regulärer Ausdruck entwickeln, der ausschließlich valide Datumsangaben zwischen 1900 und 2029 extrahiert.

/(?<Jahr>19\d\d|20[0-2]\d)-(?<Monat>0?\d|1[0-2])-(?<Tag>[0-2]?\d|3[0-1])(?=\s)/gm

Auch wenn er fachlich und technisch richtig ist, welche Probleme treten bei diesem regulären Ausdruck eventuell auf?

Hinweise:

  • Der reguläre Ausdruck ist sehr komplex und schwer zu verstehen.
  • Überflüssig da es ggf. gar keine invaliden Datumsangaben gibt?
  • Ggf. möchte man falsch geschriebene Daten explizit mit in die Ergebnisse aufnehmen und nachkorrigieren?
  • Ggf. Datumsangaben vor 1900 oder nach 2029 übersehen?
  • Unterscheidet nicht zwischen Monaten mit 28/30 und 31 Tagen.

Aufgabe 8: Spezielle Zeichen

Es gibt eine Liste mit speziellen Zeichen, die bei Vorkommen in einem regulären Ausdruck mit einem Backslash \ escaped werden müssen um sie zu deaktivieren.

ZeichenBedeutung
.Wildcard, beliebiges Zeichen
^Zeilenbeginn oder Verneinung
$Zeilenende
\Escapezeichen
+Quantor
*Quantor
?Quantor
{, }Quantor
(, )Gruppe
[, ]Zeichenklasse

Der folgende reguläre Ausdruck passt zum Beispiel auf Zeilen, in denen nur ein Datum steht.

/^\d{4}-\d{1,2}-\d{1,2}$/gm

Aufgabe: Schreiben Sie einen regulären Ausdruck, der in dem folgenden Text ausschließlich Dezimalzahlen in englischer Schreibweise mit Punkt . als Trennzeichen extrahiert.

Der Literpreis für Diesel ist erstmals über 2.00 Euro gestiegen.

Siehe auch Code AFE2A00F.
Lösung:

/\d+\.\d+/gm

Aufgabe 9: Reguläre Ausdrücke in OpenRefine

In OpenRefine kann anstelle von Text auch häufig ein regulärer Ausdruck verwendet werden. Zum Beispiel beim Suchen via Textfilter, dem Aufteilen von Spalten oder dem Replace-Dialog. Eine Liste der GREL Funktionen mit Unterstützung von regulären Ausdrücken gibt es in der OpenRefine Dokumentation.

Für diese Aufgabe wechseln wir zu OpenRefine und laden die Datei 08_kurzbiographien.csv in das Projekt “Kurzbiographien”.

💾 Wir benötigen die folgende Datei (Rechtsklick und “Ziel speichern unter…”):

Kurzbiographien als CSV

Die Datei 08_kurzbiographien.csv verwendet Texte von verschiedenen und in der Datei referenzierten Wikipedia Artikeln, die unter einer CC BY-SA 3.0 Lizenz veröffentlicht wurden. Die Datei 08_kurzbiographien.csv ist entsprechend ebenfalls lizenziert unter einer CC BY-SA 3.0 Lizenz.

Die Datei enthält aus Wikipedia gesammelte Kurzbiographien einiger Autoren einer bestimmten Literaturströmung.

Aufgabe: Wir wollen aus den Kurzbiographien mit Hilfe von regulären Ausdrücken vier neue Spalten erstellen: Geburtsdatum, Geburtsort, Sterbedatum und Sterbeort.

Bonusfrage: zu welcher Literaturströmung gehören die Autoren?

Hinweise:

  • Um mit einem regulären Ausdruck nur einen Teil der Daten aus einer Spalte in eine neue zu überführen bietet sich der “Add column based on this column…” Dialog und die GREL Funktion find() an.
  • Der GREL Ausdruck value.find(/\d{4}/)[0] findet beispielsweise alle Jahreszahlen in Form einer Liste (technisch Array) zurück und wir extrahieren mit [0] das erste Element.
  • Reguläre Ausdrücke werden in OpenRefine Funktionen mit /.../ eingefasst.

Lösung:

Es gibt unterschiedliche Lösungswege, hier ist eine recht einfache Variante aufgezeigt. Hierbei verzichten wir auf komplexe reguläre Ausdrücke und formulieren verständliche Einzelschritte.

  1. Neue Spalte “Geburtsdatum” erstellen via “Kurzbiographie” “Edit column” “Add column based on this column…” und dem GREL Ausdruck value.find(/\*[^;]+/)[0].
  2. Wiederhole Schritt 1 mit “Sterbedatum” und dem GREL Ausdruck value.find(/†[^\;)]+/)[0].
  3. Die Spalte “Geburtsdatum” in zwei Spalten aufteilen via “Geburtsdatum” “Edit column” “Split into several columns…” und dem Seperator \s+in\s+ (Häkchen bei regular expression nicht vergessen).
  4. Wiederhole Schritt 3 auf der Spalte “Sterbedatum”.
  5. Spalten umbenennen
  6. Unnötige Zeichen in Spalte “Geburtsdatum” ersetzen via “Geburtsdatum” “Edit cells” “Transform…” und dem GREL Ausdruck value.replace(/(\*\s*|†\s*)/, "") (alternativ geht das auch über den “Replace” Dialog).
  7. Wiederhole Schritt 6 auf der Spalte “Sterbedatum”.

Alternative Lösung(en):

Wir verwenden match() und einen komplexen regulären Ausdruck um alle Datenbestandteile gleichzeitig zu extrahieren:

value.match(/.+\* (?<Geburtsdatum>\d{1,2}\. [a-zA-Zä]+ \d{4}) in (?<Geburtsort>[^;]+); † (?<Sterbedatum>\d{1,2}\. [a-zA-Zä]+ \d{4}) in (?<Sterbeort>[^;|\)]+).+/)

Diesen GREL Ausdruck können wir dann viermal mit “Kurzbiographie” “Edit column” “Add column based on this column…” anwenden und jeweils via value.match(...)[0] (Geburtsdatum) bis value.match(...)[3] (Sterbeort) die einzelnen Bestandteile für die jeweilige Spalte extrahieren.

Oder wir verwenden anstatt value.match(...)[...] die Funktion value.match(...).join(";") und teilen die neue Spalte anschließend an dem Trennzeichen ; in vier Spalten auf.

Oder wir extrahieren die Daten zuerst gemeinsam und transponieren sie anschließend, wie in Daten mit OpenRefine Transponieren beschrieben. Vor dem Transponieren müssen wir die Daten anhand von einem Zeilenumbruch \n in mehrere Zeilen aufteilen, die Spalte anhand von : in zwei Spalten aufteilen und mit “Edit cells” “Fill down” die restlichen Spalten auffüllen.

with(
    value.match(/.+\* (?<Geburtsdatum>\d{1,2}\. [a-zA-Zä]+ \d{4}) in (?<Geburtsort>[^;]+); † (?<Sterbedatum>\d{1,2}\. [a-zA-Zä]+ \d{4}) in (?<Sterbeort>[^;|\)]+).+/),
    v,
    "Geburtsdatum: "  + v[0] +
    "\nGeburtsort: "  + v[1] +
    "\nSterbedatum: " + v[2] +
    "\nSterbeort: "   + v[3]
)

Aufgabe 10: Vorsicht bei Umlauten!

Wie schon erwähnt basiert OpenRefine auf Java und in der hier im Tutorial eingesetzten OpenRefine Version 3.7.9 wird Java in Version 11 mitgeliefert. Eine Übersicht über alle von Java in dieser Version unterstützen Features von regulären Ausdrücken findet sich in der Java 11 Dokumentation zu RegEx Pattern. Die Unicode Unterstützung ist bei den Mustern in Java 11 optional und muss daher separat aktiviert werden. Das hat zur Folge, dass “Sonderzeichen” wie unsere Umlaute äöü, oder die in der französischen Sprache gebräuchlichen Buchstaben mit Akzenten éêè, nicht von den Mustern [a-z] oder \w berücksichtigt werden.

Um zum Beispiel auch Umlaute zu berücksichtigen gibt es mehrere Möglichkeiten:

  1. Umlaute explizit auflisten: [a-zäöüß]
  2. Spezielle Klasse für Buchstaben (letter) verwenden: \p{L}
  3. Spezielle Klasse für alphabetische Zeichen verwenden: \p{IsAlphabetic}
  4. Unicode Modus mit inline Flag aktivieren: (?U)\w

Aufgabe: Kopieren Sie den folgenden GREL Ausdruck in einen GREL Dialog und ersetzen Sie ... mit den oben genannten Möglichkeiten auch Unicode-Zeichen zu berücksichtigen.

"abcöüßéêè€$123!-".find(/.../)
Hinweise:

  • In RegEx101 wird bei der Auswahl des Flavors Java 8 die Unicode Unterstützung über das U Flag nach dem regulären Ausdruck aktiviert. Inline Flags werden dort nicht unterstützt.
  • In OpenRefine wird U nur als inline Flag und nicht als Regex Flag unterstützt.

Fazit

Bei der Verwendung von regulären Ausdrücken sollte stets die Balance zwischen Genauigkeit und Trefferquote im Hinterkopf behalten werden. Liegt die Priorität darauf nur genau die Daten zu finden, die einem sehr präzisen Muster folgen? Oder formulieren wir das Muster eher unpräzise und filtern die überflüssigen Treffer in einem separaten Schritt?

Wie bei der Lösung zur Aufgabe 9 angesprochen, ist es manchmal einfacher und verständlicher die Arbeiten in mehrere einfache Schritte zu teilen, anstatt sie in einem komplexen Schritt zu formulieren. Also zum Beispiel Geburtsdatum und Geburtsort zusammenhängend zu extrahieren und dann aufzusplitten, anstatt komplexe reguläre Ausdrücke für ein Geburtsdatum und den Geburtsort separat zu formulieren.


Im nächsten Teil beschäftigen wir uns mit der Programmiersprache GREL.

Benjamin Rosemann
Benjamin Rosemann
Data Scientist

Ich evaluiere KI- und Software-Lösungen und integriere sie in den Archivalltag.

Ähnliches