| von Danial Podjavorsek

Der Weg aus dem Labyrinth: XML-Denormalisierung mit Apache Spark

Einleitung

In der Welt der Datenverarbeitung und -analyse steht man oft vor Herausforderungen, die so komplex und verschachtelt sind wie die Daten selbst. Insbesondere beim Umgang mit XML-Strukturen kann die Tiefe der Verschachtelung zu einem wahren Labyrinth werden. Wagt man sich in die Tiefen dieses Themas vor, wird einem rasch bewusst, dass Theorie und Praxis oft weit auseinander liegen. Die meisten Blogs und Dokumentationen decken lediglich die grundlegendsten Anwendungsfälle ab, und sobald das Problem etwas komplexer wird, bleibt man auf sich selbst gestellt. Dieser Blogbeitrag soll dazu dienen, einen detaillierten Einblick in dieses Thema zu gewähren.

Was ist eine XML-Datei?

Die Extensible Markup Language (XML) ermöglicht die Definition und Speicherung von Daten auf eine Weise, die vom Frontend ebenso wie vom Backend genutzt werden kann und stellt damit eine Schnittstelle zwischen beiden Welten dar. XML unterstützt den Austausch von Informationen zwischen Computersystemen wie Websites, Datenbanken und Anwendungen von Drittanbietern. Dank vordefinierter Regeln wird die Übertragung von Daten als XML-Dateien über ein beliebiges Netzwerk erleichtert. Der Empfänger kann diese Regeln nutzen, um die Daten präzise und effizient zu lesen. Folgende Punkte fassen XML zusammen:

  • XML ist eine Markup-Sprache, ähnlich wie HTML.
  • XML wurde für die Speicherung und den Transport von Daten entwickelt.
  • XML wurde entwickelt, um weitestgehend selbsterklärend zu sein, d.h. die Datei ist ohne weiteren Kontext für dritte lesbar

Struktur

Im Bereich Big Data und Analytics werden Daten in drei Bereiche klassifiziert:

  • Strukturierte Daten
  • Unstrukturierte Daten
  • Semistrukturierte Daten

Strukturierte Daten sind Daten, die in einem vorgegebenen Format definiert wurden, bevor man sie im Datenspeicher abgelegt hat. Das wird häufig auch als Schema-on-Write bezeichnet. Strukturierte Daten sind vorteilhaft, da auch ein durchschnittlicher User, der sich mit dem Thema auskennt, sie nutzen kann, ohne ein tiefes Verständnis der verschiedenen Datenarten oder ihrer Zusammenhänge zu benötigen. Einer ihrer größten Vorteile liegt darin, dass sie leicht für maschinelles Lernen genutzt werden können, da sie organisiert, leicht bearbeitbar und abfragbar sind.  Ein Beispiel hierfür sind Finanzdaten in einer Datenbank, in der Informationen wie Kundenname, Transaktionsdatum, Betrag und Zahlungsmethode in tabellarischer Form strukturiert sind.

Unstrukturierte Daten sind Daten, die in einem nativen Format gespeichert und erst dann bearbeitet werden, wenn sie verwendet werden. Man spricht auch von Schema-on-Read. Unstrukturierte Daten bieten Flexibilität, da sie erst bei Bedarf definiert werden, was ihren Anwendungsbereich erweitert. Standard-Datenwerkzeuge sind in diesem Kontext weniger effizient, da spezialisierte Bearbeitungs-Tools benötigt werden. Dies bedeutet, dass ein Unternehmen, um unstrukturierte Daten optimal nutzen zu können, in spezielle Ressourcen und Fachwissen investieren muss. Ein Beispiel dafür sind Kundenbewertungen in sozialen Medien.

Semistrukturierte Daten sind eine Zwischenform von strukturierten und unstrukturierten Daten. Obwohl sie zunächst als unstrukturiert erscheinen, sind sie tatsächlich mit Metadaten versehen, die auf spezifische Merkmale verweisen. Diese Metadaten liefern ausreichend Informationen für eine effizientere Katalogisierung, Suche und Analyse im Vergleich zu komplett unstrukturierten Daten. Somit ermöglichen semistrukturierte Daten eine gewisse Organisation und sind flexibler als strukturierte Daten, aber bieten mehr Ordnung und durchsuchbare Informationen als unstrukturierte Daten.
XML-Dateien fallen in die Kategorie der semistrukturierten Daten. Das liegt zum einen am Schema, da es kein festes Schema einer Datei geben muss, damit diese als valide gilt. Es ist allerdings möglich eine XML Schema Definition Datei (XSD) anzulegen und beim Einlesen der Daten abzufragen und somit Dateien, die nicht diesem Schema entsprechen auszusortieren. Damit lässt sich eine Konsistenz in der Datenverarbeitung erreichen.
Zum anderen unterstützen XML-Dateien eine hierarchische Datenstruktur, die verschachtelte Informationen enthält. Im Gegensatz dazu werden bei strukturierten Daten die Daten einfach in einer flachen Tabelle dargestellt.

Schema

Eine XSD-Datei ist ein Dokument, das Regeln oder Einschränkungen für die Struktur einer XML-Datei beschreibt.

Beispiel einer XSD Datei
Abbildung 1: Beispiel einer XSD-Datei, welche das Schema der zugehörigen XML Dateien

Der Zweck eines XML-Schemas besteht darin, die formellen Bausteine eines XML-Dokuments zu definieren:

  • die Elemente und Attribute, die in einem Dokument erscheinen können
  • die Anzahl (und Reihenfolge) der untergeordneten Elemente
  • Datentypen für Elemente und Attribute
  • Standard- und Festwerte für Elemente und Attribute

Was sind verschachtelte Strukturen?

XML-Dateien begegnen einem aufgrund ihrer Vielseitigkeit in den unterschiedlichsten Kontexten. Ein typisches Szenario ist die tägliche Bereitstellung neuer Dateien auf einem Speicherort. Diese Dateien müssen tagtäglich abgerufen werden, um die darin enthaltenen Informationen zu extrahieren und in einer übersichtlichen Form zu speichern. Wie eine XML-Datei verarbeitet werden kann, wird in diesem Beitrag unter Nutzung von Notebooks der Unified Data und Analytics Plattform Databricks aufgezeigt.

Beispiel der XML-Datei
Abbildung 2: Beispiel der XML-Datei mit Informationen zu Autoren und Büchern

Abbildung 2 zeigt, wie eine XML-Datei aussehen könnte, welche Informationen zu Büchern, einer Bibliothek enthält.  Mit den in PySpark enthaltenen, nativen Bibliotheken ist es leider zu diesem Zeitpunkt nicht möglich, XML-Dateien zu lesen. Auf dem Cluster wird eine zusätzliche Library ("com.databricks:spark-xml_2.12:0.15.0"  gearbeitet) installiert, welche diese Funktionalität bereitstellt.

Ab der Databricks Runtime Version 14.1 wird es auch „builtin SQL functions“ geben, welche das Parsen von XML-Dateien ohne die externe Bibliothek ermöglichen. Da zum Zeitpunkt als der Artikel verfasst wurde die Version noch nicht veröffentlich wurde, wird davon ausgegangen, dass sich die eingelesenen DataFrames ähnlich verhalten zu den durch die externe Bibliothek eingelesenen.

In Spark SQL existieren dafür der StructType und das StructField. StructType ist eine Sammlung von StructFields. Mit StructField, oder auch einfach nur „Struct“, werden Spaltennamen, Spaltendatentyp, nullable column und Metadaten definiert. Ein StructField definiert in der internen PySpark Logik die Spalte eines DataFrames.

Eine weitere verschachtelte Struktur ist das Array. In Arrays lassen sich, ähnlich wie beim Struct, Informationen verschachtelt abspeichern. Ein Eintrag in einem Array entspricht in der internen Logik von PySpark einer Reihe. Mit diesen beiden Dimensionen (Spalte, Reihe) lassen sich mittels PySpark komplexe Konstrukte erstellen, welche in diesem Beitrag auf eine einfache Logik reduziert werden.

In diesem Beispiel einer XML-Datei, wird eine Tabelle erzeugt mit bekannten Buchtiteln, dem Genre der Bücher und dem Erscheinungsjahr. Außerdem existiert eine weitere Spalte, welche verschachtelte Informationen zum Autor und weitere bekannte Werke enthält. Die Problemstellung besteht darin, herauszufinden welche weiteren Werke die genannten Autoren veröffentlicht haben. Dafür muss die vorhandene verschachtelte Struktur aufgelöst werden, damit jedem Werk ein Autor zugeordnet werden kann. In diesem Beitrag wird dieser Zustand erreicht, wenn die Tabelle komplett „flach“ ist und somit unter die Kategorie strukturierte Daten fällt.

Schema XSD-Datei
Abbildung 3: Schema XSD-Datei für XML-Datei in Abbildung 2

Das Schema in Abbildung 3 beschreibt, wie Einträge in der Tabelle aussehen sollen. Mit Hilfe dieser Metadaten, können Semi-Strukturierte Daten in eine strukturierte Form gebracht werden. In diesem Fall ist dies nicht zwingend notwendig, aber falls man eine große Menge an Input Daten verarbeiten möchte, kann dies ein nützliches Werkzeug sein.

Wie werden in Databricks verschachtelte Strukturen navigiert?

Wurde die Library installiert, kann mit folgendem PySpark Code die XML-Datei eingelesen werden:

PySpark Code

Im Wesentlichen besteht der Code aus vier Elementen. Zunächst wird das Format definiert, wie das File eingelesen werden soll. Da es sich bei XML nicht um ein nativ unterstütztes Format handelt, muss man dafür den Pfad zur Bibliothek detailliert angeben.
Die Optionen „rootTag“ und „rowTag“ geben an wie das Dokument zu lesen ist. Der DataFrame beginnt ab dem „rootTag“ und jeder „rowTag“ bildet eine Reihe. Mit dem load-Befehl, wird der Pfad angegeben, unter dem die Datei zu finden ist.

Jedes XML-Dokument muss genau ein Root-Tag haben, das alle anderen Elemente im Dokument einschließt. Das Root-Tag definiert den Beginn und das Ende des XML-Dokuments und strukturiert somit die gesamte XML-Datei.

Im Kontext von XML bezieht sich der Begriff "rowTag" nicht auf eine spezifische XML-Terminologie. Es könnte sein, dass Sie sich auf Elemente innerhalb von XML-Dokumenten beziehen, die Daten in einer tabellarischen Struktur repräsentieren. In diesem Fall kann man allgemein von "Tags" sprechen, die Zeilen oder Datensätze in einer Tabelle darstellen.

Data-Frame
Abbildung 4: Schema und Inhalt des DataFrames, direkt nach dem Laden aus der Inputdatei.

Der DataFrame welcher in Abbildung 4 zu sehen ist, zeigt eine verschachtelte Struktur, in der Spalte „author“. Nicht nur die Informationen, die direkt den Autor beschreiben, sondern auch weitere Werke des Autors sind dort gespeichert. Als ersten Schritt konzentriert man sich darauf, die Daten des Autors von den Buchdaten zu trennen.

Da es sich bei der „author“ Spalte um ein StructField handelt, kann diese Spalte und die sich darunter befindenden Elemente mit einem 'Select'-Befehl erreicht werden. Für jedes Element, das sich im StructField befindet, wird eine eigene Spalte erzeugt.

Author Spalte

Das Ergebnis wird in einem neuen DataFrame gespeichert, welcher „df_author“ genannt wird. Das Schema dieses DataFrames unterscheidet sich nun von dem des initialen DataFrames. Hauptsächlich fallen die Spalten „genre“, „title“ und „year“ weg. Zusätzlich wird „author“ zum neuen „rowTag“ bestimmt. An dieser Stelle kann man die bereits bestehenden Spalten auch mitnehmen, aber um das Beispiel übersichtlicher zu halten, werden sie entfernt.

Data-Frame in oberster Ebene
Abbildung 5: DataFrame nach dem die oberste Ebene selektiert wurde.

Auf der obersten Ebene befinden sich nun zwei Elemente („address“ und „books“) vom Typ StructField. Die Elemente, welche den Autor beschreiben, wurden von den Elementen, welche die Bücher beschreiben, getrennt.
Um tiefer in die Struktur einzutauchen, werden im nächsten Schritt diese Elemente selektiert.

df author one

Dieses Mal wurden als die ersten beiden Parameter des SELECT-Statements, zwei Spalten mitgenommen ("first_name", "last_name"). Im Gegensatz zum vorherigen SELECT-Statement, bei dem alle nicht spezifisch genannten Spalten wegefallen sind, können so bei der Denormalisierung auch Spalten mitgenommen werden. Die Denormalisierung beschreibt ein Designkonzept, bei dem durch Erzeugen von redundanten Daten, eine verbesserte Leistung beim Abfragen der Daten erzielt werden soll. Bei Datenbanken dieser Art wird oft der Lesevorgang (Abfragen) höher priorisiert als der Schreibvorgang (Einfügen). Durch das Hinzufügen redundanter Daten, werden die Abfragen schneller und effizienter gestaltet.

Data Frame
Abbildung 6: DataFrame, nachdem die zweite Ebene selektiert wurde. Die Inhalte der anderen Spalten wurden explizit auch mitgenommen.

Nun befindet sich in der ersten Ebene eine Spalte „book“ vom Typ ArrayType. Jedes Buch wird in diesem Fall von einem Array repräsentiert, welches wiederum Elemente vom Typ StructType enthält, welche die Eigenschaft des Buches beschreiben.

Um diese Ebene nun zu denormalisieren, wird eine zusätzliche PySpark-Funktion benötigt: explode().

PySpark Funktion

Die explode-Funktion gibt für jedes Element im Array eine neue Reihe aus. Das ist auch der Unterschied zu den StructFields, bei denen für jedes Element eine neue Spalte erzeugt wird. Um die explode-Funktion aufrufen zu können, wird daher eine weitere Funktion benötigt. Die Funktion „withColumn“ erzeugt eine neue Spalte, in der die Ergebnisse abgespeichert werden können, damit die Informationen der bestehenden Spalte nicht verloren gehen.

Diese neue Spalte heißt "new_book" und enthält Werte in Form eines Structs:

Struct

Da die Informationen in der bestehenden Spalte allerdings nicht mehr benötigt werden und ein Befehl gespart werden soll, um die Spalte händisch zu entfernen, gibt es noch die Möglichkeit den bestehenden Spaltennamen als neuen Spaltennamen zu verwenden. Dadurch wird die bestehende Spalte überschrieben.

Data Frame nach Array
Abbildung 7: DataFrame nachdem das Array in der Spalte book "exploded" wurde.

Dieser Prozess lässt sich so lange wiederholen, bis alle verschachtelten Strukturen aufgelöst wurden. Ist diese Bedingung erfüllt, erhält man eine „flat table“, welche in die Kategorie der strukturierten Daten fällt und der sich in eine relationale Datenbank einbinden lässt.

Die explode-Funktion und das Select-Statement lassen sich auch kombinieren, um dies zu erreichen.

Data Frame
DataFrame nach Auflösung

Die schlussendlich erzeugte Tabelle, entspricht dem Grundgedanken der Denormalisierung. In Hinsicht auf die Speicherkapazität ist dies nicht der optimale Ansatz, da einige Reihen beinahe identische Einträge aufweisen. Die Lesevorgänge und damit die Geschwindigkeit bei Abfragen ist durch die redundanten Einträge aber höher.

Arbeiten mit verschachtelten Strukturen

In diesem Beitrag wurde erforscht wie verschachtelte Dateien im XML-Format, mithilfe von PySpark und Databricks, in eine strukturierte und lesbare Form gebracht werden können. Selbst stark verschachtelte Dateien können mit den oben gezeigten Schritten aufgelöst und übersichtlich dargestellt werden. An einem überschaubaren Beispiel wurde gezeigt, wie diese Schritte händisch ausgeführt werden können, um zum Beispiel die Struktur einer unbekannten Datei zu analysieren. Eine Möglichkeit auf dieser Analyse aufzubauen, wäre es diesen Prozess zu automatisieren. Im ersten Schritt werden alle verschachtelten Strukturen auf der Root Ebene aufgelistet. Diese werden dann aufgelöst, bis auf der obersten Ebene keine verschachtelten Strukturen mehr existieren. Im nächsten Schritt wiederholt man diesen Prozess eine Ebene tiefer. Alle verschachtelten Strukturen werden so lange aufgelöst, bis nur noch ein „Flat Table“ vorhanden ist.

In der kontinuierlich expandierenden Welt von "Big Data" begegnen einem fortwährend komplexe, verschachtelte Strukturen. Der hier gezeigte Leitfaden kann dem Nutzer zumindest in einem Teilgebiet, was XML-Dateien angeht, helfen, relevante Informationen zu extrahieren und für die Weiterverwendung entsprechend zu gestalten.

Quellen:

https://spark.apache.org/docs/3.2.2/api/python/index.html
https://www.snowflake.com/guides/semi-structured-data-101
https://www.w3schools.com/xml/schema_intro.asp
https://www.databricks.com/
https://docs.databricks.com/en/sql/language-manual/functions/from_xml.html

Teile diesen Artikel mit anderen

Über den Autor

Danial Podjavorsek ist seit September 2022 als Data Engineer bei Woodmark tätig. Im Rahmen von Kundenprojekten beschäftigt er sich hauptsächlich mit dem Aufbau und der Optimierung von ETL-Pipelines - insbesondere in Databricks.

Zur Übersicht Blogbeiträge