Workshop - Arbeiten mit GREL
In diesem Tutorial beschäftigen wir uns mit der “General Refine Expression Language” (GREL).
Einführung
GREL in der OpenRefine Dokumentation.
GREL Rezepte im OpenRefine Wiki.
Dieser Workshop wurde zuletzt getestet mit OpenRefine Version 3.8.2.
Mit der General Refine Expression Language (GREL) werden in OpenRefine die einzelnen Operationen auf den Daten ausgeführt. Die meisten Funktionen und Dialoge in OpenRefine sind quasi nur ein Eingabeformular für GREL Ausdrücke, die in den Facets angezeigten Daten basieren auf GREL Ausdrücken und in verschiedenen Dialogen können wir auch direkt GREL Ausdrücke verwenden, um zum Beispiel Daten zu transformieren.
Die Syntax von GREL ist dabei an die Programmiersprache JavaScript angelehnt, die unterliegende Technologie basiert jedoch auf der Programmiersprache Java. Dadurch entstehen Verständnisprobleme, wenn zum Beispiel reguläre Ausdrücke im Stil von JavaScript geschrieben werden, dann jedoch in OpenRefine intern von Java ausgewertet werden und entsprechend die Funktionalität von regulären Ausdrücken in Java unterstützen.
Die Eingabe von GREL Ausdrücken erfolgt (meistens) über Dialogfenster wie in Abbildung 1. Hier sehen wir nicht nur eine Vorschau der Umwandlung, die wir gerade anstreben, sondern können auch vorherige oder gespeicherte (Starred) GREL Ausdrücke laden.
Werte, Variablen, Objekte
In Programmiersprachen gibt es verschiedene Konzepte um mit Daten zu arbeiten.
Ein Konzept ist die Typisierung von Daten. So wird bei Werten zum Beispiel zwischen Text und Zahlen unterschieden. Mit diesen Werten können dann verschiedene Operationen und Berechnungen durchgeführt werden.
5 > 2
> true
Diesen Werten können via Variablen auch Namen zugewiesen werden.
laenge = 5
breite = 2
laenge > breite
> true
Oder es gibt spezielle Funktionen für Werte eines bestimmten Typs:
text = "Lorem ipsum"
length(text)
> 11
Werte und Funktionen lassen sich zu Objekten zusammenfassen. Via Variable kann ein bestimmtes Objekt gespeichert und darauf zugegriffen werden.
Rechteck(laenge, breite):
laenge = leange
breite = breite
flaeche()
return laenge * breite
rechteck1 = Rechteck(5, 2)
rechteck2 = Rechteck(4, 2)
rechteck1.flaeche > rechteck2.flaeche
> true
In GREL verwenden wir hauptsächlich schon existierende Variablen, Funktionen und Objekte.
Datentypen
In der OpenRefine Oberfläche arbeiten wir mit vier Datentypen: boolean, date, number und string. In GREL gibt es noch die Datentypen array, object und regex. Texte (string) können zusätzlich in eine Repräsentation für HTML, JSON oder XML geparsed werden. Das bedeutet vereinfacht, dass die (Text-)Daten intern von OpenRefine in eine Repräsentation überführt werden, die zum Beispiel XML versteht und dafür passende Funktionen anbietet. Also zum Beispiel die Funktion über alle XML-Knoten mit einem bestimmten Namen zu iterieren.
Typ | Kürzel | GUI | Beispiel(e) |
---|---|---|---|
array | a | ["a", "b", "c"] | |
boolean | b | Ja | true , false |
date | d | Ja | [date 2022-05-03T15:00:00Z] |
number | n | Ja | 1 , 3.1415 |
object | o | [object Cell] | |
regex | p | /\d{4}/ | |
string | s | Ja | Lorem ipsum... |
Variablen
Beim Arbeiten mit GREL stehen uns zusätzlich einige Umgebungsvariablen zur Verfügung.
Variable | Alternative | Beschreibung |
---|---|---|
cell | row.cells[columnName] | Aktuelle Zelle der ausgewählten Spalte. |
cell.recon | Reconciliation Daten für die Zelle. | |
cell.recon. … | Dokumentation zu Recon. | |
cell.value | value | Wert der Zelle. |
cell. …. | Dokumentation zu Cell. | |
columnName | Name der ausgewählten Spalte. | |
row | Aktuelle Zeile. | |
row.cells | cells | Zellen in der aktuellen Zeile. |
row.cells. … | Dokumentation zu Cells | |
row.index | rowIndex | Index der aktuellen Zeile. |
row.record | Record der aktuellen Zeile. | |
row.record. … | Dokumentation zu Record | |
row. …. | Dokumentation zu Row. |
Die Variablen beinhalten teilweise einfache Werte (value
, rowIndex
) wie einen Text oder eine Zahl.
Teilweise verweisen sie auf komplexe Objekte, worin die Werte gesammelt oder verschachtelt gespeichert sind (row
).
Manche Variablen stellen auch Abkürzungen dar. So kann auf den Wert der aktuellen Zeile in der ausgewählten Spalte direkt mit value
zugegriffen werden, anstatt dies via row.cells[columnName].value
machen zu müssen.
Hier eine Übersicht der Objekte und Variablen.
Fortsetzung für Reconciliation Daten einer Zelle:
Auf Attribute eines Objekts kann punktnotiert row.cells
oder via Klammern row["cells"]
zugegriffen werden.
Prinzipiell empfiehlt es sich aus Gründen der Lesbarkeit die Punktnotation zu verwenden und die Variante mit Klammern nur, wenn der Name in einer Variablen steht cells[columnName]
oder wir mit Spaltennamen arbeiten, die potentiell “Sonderzeichen” beinhalten können cells["GND-ID"]
.
row.record.cells[columnName]
.Kontrollstrukturen
Etwas verwirrend ist das Definieren von eigenen Variablen in GREL.
Das geschieht mit einer with Anweisung.
Im folgenden Beispiel setzen wir die Variable jahr
auf den Wert 1988
und berechnen dann den Ausdruck jahr < 2000
.
with(1988, jahr, jahr < 2000) == true
== ...
an, so dass das Ergebnis auch abgelesen werden kann, ohne den Ausdruck in OpenRefine oder im Kopf auswerten zu müssen.Eine der essentiellen Kontrollstrukturen in der Informatik ist die Verzweigung.
In GREL wird dies mit if realisiert.
Im folgenden Beispiel setzen wir wie zuvor die Variable jahr
auf den Wert 1988
, geben aber abhängig von dem Ergebnis einer Prüfung unterschiedliche Werte zurück. Die if-Anweisungen können auch ineinander verschachtelt werden.
with(1988, jahr, if((jahr >= 1980).and(jahr < 1990), "80er", "Sonstiges")) == "80er"
Anstatt einer Verzweigung ist es manchmal auch nötig eine komplette Liste (technisch Array) von Werten zu filtern.
Dafür gibt es in GREL eine filter() Funktion.
Im folgenden Beispiel erstellen wir einen Array aus einem Text indem wir ihn mit split() am Trennzeichen ,
trennen.
Anschließend wandeln die einzelnen Elemente mit toNumber() in Zahlen um und filtern anschließend nur die geraden Zahlen via mod(number, 2) == 2
.
Um das Ergebnis in OpenRefine ausgeben zu können, wandeln wir die Daten im Array anschließend mit join() wieder in einen Text um, da OpenRefine in der GUI keine Arrays anzeigen kann.
filter("1,2,3,4,5,6,7,8,9,10".split(","), v, mod(v.toNumber(), 2) == 0).join(",") == "2,4,6,8,10"
Um alle Werte einer Liste (technisch Array) durchzugehen, gibt es in GREL die
forEach Anweisung.
Im folgenden Beispiel erstellen wir mit split()
einen Array aus einem Text und fügen mit +
zu jedem Element die Endung “er” hinzu.
Anschließend fügen wir die Daten im Array mit join()
wieder zu einen Text zusammen.
forEach("70,80,90".split(","), v, v + "er").join(",") == "70er,80er,90er"
Zusätzlich gibt es noch weitere Helfer. Um den Typ eines Wertes zu prüfen gibt es verschiedene Varianten von is…, um einen bestimmten Bereich aufzuzählen forRange oder um eine Liste (technisch Array) aufzuzählen forEachIndex.
Extra: forNonBlank
Da sie etwas verwirrend ist, geben wir der Funktion forNonBlank hier noch einen separaten Absatz.
Angenommen wir wollen eine Liste (technisch Array) von Werten durchgehen und dabei nur Werte bearbeiten, die einen Inhalt haben.
Also zum Beispiel die ["1", "2", "", "4", "5"]
umwandeln in 1,2,0,4,5
.
forNonBlank(["1", "2", "", "4", "5"], v, v, 0).join(",") == "1,2,,4,5"
Das klappt so nicht, da forNonBlank(v, ...)
eine andere Schreibweise für if(isNonBlank(v), ...)
ist.
Der korrekte Weg ist also einer der beiden folgenden Ausdrücke.
forEach(["1", "2", "", "4", "5"], v, if(isNonBlank(v), v, 0)).join(",") == "1,2,0,4,5"
forEach(["1", "2", "", "4", "5"], v, forNonBlank(v, v, v, 0)).join(",") == "1,2,0,4,5"
Standardbibliothek
Üblicherweise haben Programmiersprachen eine Standardbibliothek von Funktionen zum Arbeiten mit den zur Verfügung gestellten Datentypen.
Diese ist bei GREL im Vergleich zu anderen Programmiersprachen zwar recht übersichtlich, kann hier aber trotzdem nicht vollständig besprochen werden. Wir empfehlen daher, die Liste der in OpenRefine verfügbaren Funktionen als Referenz verfügbar zu halten.
Wir gehen hier auf ein paar Besonderheiten von GREL ein.
Syntax für Funktionsaufrufe
Eine Besonderheit in GREL ist, dass Funktionen zur besseren Lesbarkeit auch in Punktnotation aufgerufen werden können.
Also length(value)
auch geschrieben werden kann als value.length()
.
Aliase
Es gibt Funktionen, die unter mehreren Namen verfügbar sind.
Beispielsweise sind trim() und strip() bis auf ihre Namen identisch und wurden auch gleichzeitig in OpenRefine eingeführt. Vermutlich sollte das Quereinsteigern von anderen Programmiersprachen den Einstieg vereinfachen.
Vorbereitung: Projekt erstellen
Wir laden die folgende Datei in ein OpenRefine Projekt.
Kretschmann Kabinett III💾 Wir benötigen die folgende Datei (Rechtsklick und “Ziel speichern unter…”):
Wie in Abbildung 2 beinhaltet die Datei das Kabinett Kretschmann III mit den Berufen.
Aufgabe 1: GREL Variablen nutzen
Das Geburtsdatum ist in drei Spalten aufgeteilt, wir wollen das Geburtsdatum jedoch in einer Spalte haben und dabei den Monat auch noch ausgeschrieben haben. Das haben wir in 03 Transformieren: Aufgabe 5 so ähnlich schon einmal mit Funktionen in der Benutzeroberfläche gelöst. Hier wollen wir GREL Funktionen verwenden, um die Bearbeitung in einem Schritt durchzuführen.
Dafür verwenden wir “Geburtsjahr" "Edit column" "Add column based on this column” und entwickeln schrittweise den in Abbildung 3 gezeigten GREL Ausdruck.
Schritt 1: Geburtsmonat umwandeln
Um die Spalte “Geburtsmonat” in einen Namen umzuwandeln verwenden wir den Zugriff über row.cells["Geburtsjahr"]
und die Funktionen toDate() und toString().
row.cells["Geburtsmonat"].value.toDate("M").toString("MMMM")
Schritt 2: Spalten zusammenfügen
Um die Spalten zusammenzufügen verwenden wir den Operator +
.
row.cells["Geburtstag"].value + ". "
+ row.cells["Geburtsmonat"].value + " "
+ row.cells["Geburtsjahr"].value
Schritt 3: Verzweigung einbauen
Für manche Zeilen wird entweder eine Fehlermeldung oder null
angezeigt.
Das sind die Zeilen, wo kein Geburtstag und kein Geburtsmonat vorhanden sind.
Diese behandeln wir separat mit einer Verzweigung und passen dabei den Typ der Inhalte der Spalte “Geburtsjahr” an.
if(
isBlank(row.cells["Geburtstag"]),
row.cells["Geburtsjahr"].value.toString(),
...
)
Aufgabe
Fügen Sie die drei obigen GREL Ausdrücke zu einem GREL Ausdruck zusammen.
Lösung:
if(
isBlank(row.cells["Geburtstag"]),
row.cells["Geburtsjahr"].value.toString(),
row.cells["Geburtstag"].value + ". "
+ row.cells["Geburtsmonat"].value.toDate("M").toString("MMMM") + " "
+ row.cells["Geburtsjahr"].value
)
Aufgabe 2: Werte aufräumen und sortieren
Die Werte in der Spalte “Beruf oder Beschäftigung” sind nicht sortiert, haben teilweise Duplikate und teilweise numerische Referenzen hinter den Berufsbezeichnungen. Diese Spalte wollen wir daher in einem Schritt mit einem GREL Ausdruck aufräumen und dabei die numerischen Referenzen entfernen, Duplikate entfernen und die Werte alphabetisch sortieren.
Dafür verwenden wir den Dialog “Beruf oder Beschäftigung" "Edit cells" "Transform” und erarbeiten uns einen GREL Ausdruck.
Schritt 1: Text in Liste umwandeln
Um die einzelnen Werte im Text in eine Liste (technisch Array) umzuwandeln, verwenden wir split() mit ,
als Trennzeichen.
"Politiker, Abgeordneter, Lehrer, Regierungschef, Politiker (5), Lehrer (3)".split(", ")
Schritt 2: Numerische Referenzen entfernen
Um die numerischen Referenzen zu entfernen verwenden wir replace() mit einem regulären Ausdruck und entfernen zusätzliche Leerzeichen.
"Politiker (5)".replace(/\(\d+\)/, "").trim()
Schritt 3: Funktion auf alle Elemente anwenden
Um die Ersetzung der numerischen Referenzen auf alle Elemente in der Liste (technisch Array) anzuwenden nutzen wir forEach.
forEach(
["Politiker, Abgeordneter, Lehrer, Regierungschef, Politiker (5), Lehrer (3)"],
v,
...
)
Schritt 4: Werte vereinheitlichen und sortieren
Um die Werte zu vereinheitlichen und zu sortieren nutzen wir uniques() und sort().
["Politiker, Abgeordneter, Lehrer, Regierungschef, Politiker, Lehrer"].uniques().sort()
Schritt 5: Text aus Liste erstellen
Um aus der Liste (technisch Array) wieder einen Text zu erzeugen, verwenden wir join() mit ,
als Trennzeichen.
["Abgeordneter", "Lehrer", "Politiker", "Regierungschef"].join(", ")
Aufgabe
Fügen Sie die fünf obigen GREL Ausdrücke zu einem GREL Ausdruck zusammen. Das Ergebnis sollte in etwa wie in Abbildung 4 aussehen.
Lösung:
forEach(
value.split(", "),
v,
v.replace(/\(\d+\)/, "").trim())
.uniques()
.sort()
.join(", ")
Fazit
Viele Umwandlungen lassen sich in Einzelschritten über die graphische Oberfläche von OpenRefine ansteuern und umsetzen.
So kommt man auch ohne in GREL zu programmieren in OpenRefine recht weit.
Das Wissen um den Zugriff auf die Umgebungsvariablen und nützliche Funktionen wie find()
, replace()
oder Datumsumwandlungen ersetzt viele manuelle Schritte.
Das explizite Programmieren mit Verzweigungen und Schleifen ergänzt Arbeitsschritte oder reduziert mehrere Einzelschritte in der graphischen Oberfläche zu einem Schritt. Beim Export von XML in OpenRefine (Templating) sind die Kontrollstrukturen noch einmal relevant.
Im nächsten Teil behandeln wir das Konzept von “Records” in OpenRefine.