Workshop - Arbeiten mit GREL

Folie - Arbeiten mit GREL Folie - 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 erstellt mit OpenRefine Version 3.5.0.
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.

Bildschirmfoto eines Dialog Fensters zur Transformation von Daten in OpenRefine.
Bildschirmfoto eines Dialog Fensters zur Transformation von Daten in OpenRefine.

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.

TypKürzelGUIBeispiel(e)
arraya["a", "b", "c"]
booleanbJatrue, false
datedJa[date 2022-05-03T15:00:00Z]
numbernJa1, 3.1415
objecto[object Cell]
regexp/\d{4}/
stringsJaLorem ipsum...

Variablen

Beim Arbeiten mit GREL stehen uns zusätzlich einige Umgebungsvariablen zur Verfügung.

VariableAlternativeBeschreibung
cellrow.cells[columnName]Aktuelle Zelle der ausgewählten Spalte.
cell.reconReconciliation Daten für die Zelle.
cell.recon. …Dokumentation zu Recon.
cell.valuevalueWert der Zelle.
cell. ….Dokumentation zu Cell.
columnNameName der ausgewählten Spalte.
rowAktuelle Zeile.
row.cellscellsZellen in der aktuellen Zeile.
row.cells. …Dokumentation zu Cells
row.indexrowIndexIndex der aktuellen Zeile.
row.recordRecord 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.

flowchart LR columnName row.index[index] row.cells[cells] row.columnNames[columnNames] row.starred[starred] row.flagged[flagged] row.record[record] row.cells.cell[cell...] row.cells.cell.value[value] row.cells.cell.recon[recon] row.cells.cell.recon.properties[...] row.cells.cell.errorMessage[errorMessage] row.cells.cell.errorMessage.value[value] row.record.index[index] row.record.cells[cells] row.record.fromRowIndex[fromRowIndex] row.record.toRowIndex[toRowIndex] row.record.rowCount[rowCount] row --> row.index & row.cells & row.columnNames & row.starred & row.flagged & row.record row.cells --> row.cells.cell row.cells.cell --> row.cells.cell.value & row.cells.cell.recon & row.cells.cell.errorMessage row.cells.cell.errorMessage --> row.cells.cell.errorMessage.value row.cells.cell.recon --> row.cells.cell.recon.properties row.record --> row.record.index & row.record.cells & row.record.fromRowIndex & row.record.toRowIndex & row.record.rowCount cell -.-> row.cells.cell cells -.-> row.cells rowIndex -.-> row.index value -.-> row.cells.cell.value

Fortsetzung für Reconciliation Daten einer Zelle:

flowchart LR row.cells.cell.recon[row.cells.cell....recon] row.cells.cell.recon.judgment[judgment] row.cells.cell.recon.judgmentAction[judgmentAction] row.cells.cell.recon.judgmentHistory[judgmentHistory] row.cells.cell.recon.matched[matched] row.cells.cell.recon.match[match] row.cells.cell.recon.best[best] row.cells.cell.recon.features[features] row.cells.cell.recon.candidates[candidates] row.cells.cell.recon.features.typeMatch[typeMatch] row.cells.cell.recon.features.nameMatch[nameMatch] row.cells.cell.recon.features.nameLevenshtein[nameLevenshtein] row.cells.cell.recon.features.nameWordDistance[nameWordDistance] match.id[id] match.name[name] match.type[type] match.score[score] row.cells.cell.recon --> row.cells.cell.recon.judgment & row.cells.cell.recon.judgmentAction & row.cells.cell.recon.judgmentHistory & row.cells.cell.recon.matched & row.cells.cell.recon.match & row.cells.cell.recon.best & row.cells.cell.recon.features & row.cells.cell.recon.candidates row.cells.cell.recon.match --> match.id & match.name & match.type row.cells.cell.recon.best --> match.id & match.name & match.type & match.score row.cells.cell.recon.candidates --> match.id & match.name & match.type & match.score row.cells.cell.recon.features --> row.cells.cell.recon.features.typeMatch & row.cells.cell.recon.features.nameMatch & row.cells.cell.recon.features.nameLevenshtein & row.cells.cell.recon.features.nameWordDistance

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"].

Aktuell kann nicht auf alle Werte einer Spalte zugegriffen werden. Man kann aber auf alle Werte einer Spalte eines Records zugreifen: 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
Wir geben das erwartete Ergebnis der GREL Ausdrücke in den Beispielen zusätzlich via == ... 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.

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

Kretschmann Kabinett III

Wie in Abbildung 2 beinhaltet die Datei das Kabinett Kretschmann III mit den Berufen.

Bildschirmfoto des Projektes direkt nach dem Laden.
Bildschirmfoto des Projektes direkt nach dem Laden.

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.

Bildschirmfoto des Dialogs zur Transformation.
Bildschirmfoto des Dialogs zur Transformation.

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.

Bildschirmfoto des Projektes nach der Bearbeitung.
Bildschirmfoto des Projektes nach der Bearbeitung.
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.

Benjamin Rosemann
Benjamin Rosemann
Data Scientist

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

Ähnliches