XML und .NET Tuan Duc Nguyen [email protected] Abstract: XML ist heutzutage eine sehr verbreitete Technologie, um Daten zu beschreiben, zu speichern und im Internet zu transportieren. Daher gibt es in der .NET Framework drei unterschiedliche Möglichkeiten mit XML-Dateien zu arbeiten. Hier werden wir über die Vor- und Nachteile dieser Methoden diskutieren, um herauszufinden für welche praktische Situation welche Methode am besten geeignet ist. 1 1.1 Einleitung Überblick über XML XML steht für Extensible Markup Language, eine vereinfachte Version vom Standard General Markup Language. Das Ziel von XML ist eine einfache Methode für Beschreibung, Sicherung und Transportierung von Dateien zwischen verschiedenen Systemen und Applikationen zu entwickeln. Im Jahr 1998 wurde die erste Spezifikation von XML von W3C veröffentlicht. Heute ist XML das wichtigste Datei-Format, nicht nur im Desktop-Bereich sondern auch im Internet. Ein Grund für die schnelle Verbreitung von XML ist die Fähigkeit, Informationen über Informationen zu geben. Mit anderen Worten können XML-Daten sich selbst beschreiben. Unten ist ein Beispiel einer einfachen XML-Datei: <?xml version="1.0" encoding="UTF-8" ?> <book> <title>C# 4.0 in a Nutshell</title> <author>Joseph Albahari</author> <isbn>978-0596800956</isbn> <price currency="EURO">34,95</price> </book> Aus dem Beispiel können wir sofort erkennen, worum es sich bei dieser Datei handelt. Mit Hilfe der Auszeichner (Tag) wie book, title, author, usw. erkennen wir die Bedeutung der einzelnen Informationen. Die Schachtelung dieser Auszeichner beschreibt die Strukturen und Beziehungen zwischen diesen Informationen. XML definiert aber keine feste Anzahl von Auszeichnern. Je nach Bedarf können mehr oder weniger Auszeichner deklariert werden. Das ist der größte Vorteil von XML: Erwei- terungsfähigkeit. Man kann daher XML nutzen, um fast alle Arten von Daten zu beschreiben und zu speichern. Eigentlich ist XML jedoch nur einfacher Text. Das bedeutet, man kann ein bestehendes XML-Dokument einfach lesen und untersuchen, wie die Daten in dem Dokument strukturiert und definiert sind. Danach ist es möglich, die gewünschten Informationen aus der XML-Datei zu extrahieren. Seit der ersten Version wurde XML von vielen Softwares und Systemen genutzt um Daten zu speichern. Eine große Menge von Daten wird auch im Internet als XML-Format transportiert. Fast jede Software-Plattform unterstützt das Lesen und Schreiben von XML. 1.2 XML-Klassen in .NET Die .NET-Plattform von Microsoft hat in der aktuellen Version 4.0 drei unterschiedliche Methoden um XML-Dateien zu bearbeiten: Die leichtgewichtige XmlReader/XmlWriter, das standardisierte DOM (Document Object Model) und die neue LINQ to XML. In den folgenden Kapiteln werden wir diese drei Möglichkeiten genauer betrachten. Schon seit der ersten Version von .NET gibt es 2 Familien von Klassen, die das Lesen und Schreiben von XML-Datei in der .NET-Umgebung realisieren: XmlReader/XmlWriter und DOM. Mit .NET 3.5 wurde LINQ to XML zusammen mit LINQ eingeführt und als empfohlene Methode vorgestellt, denn LINQ to XML hat nicht nur ein einfaches Programmier-Konzept wie DOM, sondern auch eine akzeptable Performanz im Vergleich zu XmlReader. Die Abbildung 1 zeigt einen Überblick über die Hierarchie von XML-Klassen in der .NETPlattform. 2 XmlReader XmlReader bildet die Grundlage für alle Lese-Aktivitäten in der .NET-Plattform und definiert den grundlegenden Leistungsumfang anderer abgeleiteten Klassen. Im Vergleich zu den zwei anderen Möglichkeiten in .NET auf XML-Dateien zuzugreifen, sind die wesentlichen Vorteile von XmlReader seine Einfachheit, seine gute Performanz und sein geringer Speicherbedarf. Natürlich hat XmlReader auch seine eigenen Nachteile. 2.1 Pull-Model vs. Push-Model XmlReader implementiert den Pull-Model-Parser, während einige sehr populäre APIBibliotheke wie zum Beispiel SAX (Simple API for XML) in Java das Push-Model ver- XmlReader System.XML XmlWriter XmlDocument XmlNode XmlAttribute XmlCharacterData XmlLinkedNode XmlElement XDocument XNode XContainer System.XML.Linq (ab .NET 3.5) XElement XAttribute Abbildung 1: Struktur der XML-Klassen in .NET wenden. Der Push-Parser liest die Datei und löst beim Auftreten eines bestimmten Elements des XML-Dokuments, zum Beispiel eines Auszeichners oder eines Kommentares, das entsprechende Event aus. Die Applikationen müssen dauerhaft und passiv auf Events des Parsers warten und die Callback-Funktion aufrufen. Die Applikationen haben daher keine Kontrolle über den Ablauf des Lesens und müssen für die Bearbeitung der XML-Datei eine kleine Zustandsmaschine aufbauen, um den aktuellen Zustand des Lesens zu verwalten. Im Gegensatz zum Push-Model löst der Pull-Parser kein Event aus. Stattdessen hält er selbst die aktuellen Informationen über den Ablauf des Lesens der XML-Datei. Die Applikationen fragen den Parser aktiv nach seinem aktuellen Status ab und können dann selbst entscheiden, ob der Parser das momentan gelesene Element detaillierter untersuchen oder zum nächsten Knoten überspringen sollte. Eine Zustandsmaschine und einen EventHandler aufzubauen ist daher unnötig für einen Pull-Parser. Ein Parser mit Pull-Model ist aus diesem Grund für die meisten Entwickler angenehmer als ein mit Push-Model. 2.2 Parsen von XML-Dateien mit XmlReader XmlReader ist ein Pull-Parser, das bedeutet XmlReader bietet eigene Methoden und Eigenschaften, um das aktuelle Element des Dokuments mitzuteilen. Die zwei wichtigsten Eigenschaften von XmlReader sind Name und NodeType, welche den Titel und den Knotentyp des aktuellen Elements zurückliefern. Es gibt verschiedene Knotentypen, die in der Aufzählung XmlNodeType definiert werden. Die am häufigsten betrachteten Knotentypen sind Element, EndElement, Text und Comment. Mit den Eigenschaften Name und NodeType kann man feststellen, ob das aktuelle Element des Dokuments die relevante Information enthält. Der Inhalt dieses Elements kann dann mit Hilfe der Eigenschaft Value als Zeichenketten zurückgeliefert werden. Diese Eigenschaft ist aber nur für Text-Knoten, Kommentare oder Attribute verfügbar. Für Knoten aus anderen Typen bietet XmlReader die Methode ReadElementContentAsXXXX() oder ReadContentAsXXXX() um den Inhalt des Knotens zu lesen. Falls das momentane Element für die Applikation uninteressant ist, kann XmlReader mit Hilfe der Methoden Skip() oder Read() den aktuellen Knoten überspringen. Die Methoden ReadToFollowing(string nodeName), ReadToDescendant(string nodeName) oder ReadToNextSibling(string nodeName) können den LeserCursor zum nächsten Knoten mit dem gegebenen Name verschieben. Leider ist Navigation mit XmlReader nur vorwärts möglich. XmlReader ist daher nur geeignet für die Situation, in der man die XML-Datei nur einmal durchlesen sollte. Ein weiterer Nachteil von XmlReader ist das Fehlen von der Fähigkeit, ein bestehendes Dokument zu manipulieren. 2.3 Beispiel Folgendes ist ein Beispiel, das demonstriert, wie man Informationen in einer XML-Datei mit Hilfe XmlReader einlesen kann. Die Test-Datei Book.xml: <?xml version="1.0" encoding="UTF-8" ?> <book> <title>C# 4.0 in a Nutshell</title> <author>Joseph Albahari</author> <isbn>978-0596800956</isbn> <!-- Some comment --> <price currency="EURO">34,95</price> </book> Der Code-Abschnitt: XmlReader reader = XmlReader.Create("book.xml"); while (reader.Read()) { switch (reader.NodeType) { case XmlNodeType.Element: if (reader.Name == "title") { reader.ReadStartElement(); Console.WriteLine("title: " + reader.ReadContentAsString()); } if (reader.Name == "price") { if (reader.MoveToAttribute("currency")) Console.WriteLine(reader.Name + " = " + reader.Value); reader.ReadStartElement(); Console.WriteLine("price: " + reader.ReadContentAsFloat()); } break; case XmlNodeType.Comment: Console.WriteLine("comment: " + reader.Value); break; } } 3 Das Document Object Model (DOM) Eine weitere Möglichkeit, XML-Datei in .NET zu bearbeiten, ist DOM. Im Vergleich zu XmlReader hat DOM schlechtere Performanz und hohen Speicherbedarf. Trotzdem ermöglicht DOM beliebige Navigation in der Baum-Struktur des Dokuments und kleine Veränderungen der bestehenden Daten, welche unmöglich bei XmlReader sind. 3.1 Was ist DOM DOM (Document Object Model) ist eine von W3C definierte Schnittstelle, die beschreibt, wie man ein XML-Dokument einlesen, bearbeiten und wieder speichern sollte. Die Idee von DOM ist es, ein XML-Dokument im Zwischenspeicher als Baum von Objekten verschiedener Klassen zu modellieren. Wenn wir mit diesen Baum-Strukturen und Objekten arbeiten, verändern wir auch die Strukturen und den Inhalt des Dokuments. DOM in .NET wird durch die Klasse XmlDocument und ihre Familie unter dem Namespace System.Xml implementiert. Da DOM eine Schnittstelle ist, ist die Standardisierung der Implementierung von DOM in verschiedenen Programmiersprachen ein Vorteil. Wenn man DOM-API schon in einer Entwicklerplattform kennt, kann man sich schnell an die Implementierung vom DOM in einer anderen Sprache gewöhnen. 3.2 XML-Dateien mit DOM einlesen und arbeiten Der Kern von DOM in .NET ist die Klasse XmlNode und ihre geerbten Klassen wie XmlElement, XmlAttribute und XmlDocument. Um ein bestehendes XML-Dokument in den Speicher zu laden bietet eine Instanz der Klasse XmlDocument die Methode Load(). Diese Methode ist vielfach überladen, da- mit das Dokument aus einer angegebenen URL, aus einem Stream oder mit Hilfe eines bestehenden XmlReader-Objekts gelesen werden kann. Falls die XML-Daten bereits in der Form eines Strings sind, kann die Methode LoadXml(string xml) verwendet werden. Nach dem Laden des Dokuments kann das Wurzelelement mit Hilfe der Eigenschaft DocumentElement abgerufen werden. XmlNode ist die wichtigste Klasse des DOM, denn sie bietet die nötige Methode, um im DOM-Baum zu navigieren, um nach einem Knoten zu suchen oder um die Struktur des Baumes zu ändern. Die Navigation im XML-Baum wird von den Eigenschaften FirstChild, LastChild, NextSibling, PreviousSibling und ParentNode ermöglicht. Die Eigenschaft ChildNodes einer XmlNode-Instanz liefert die Menge aller untergeordneten Knoten zurück. Die Eigenschaften Name und NodeType von XmlNode haben die gleiche Funktionalität wie die von XmlReader. Um nach einem Element mit dem gegebenen Auszeichner zu suchen bieten die Klassen XmlDocument und XmlElement die Methode GetElementsByTagName(string nodeName). Außerdem ist es auch möglich, Knoten mit Hilfe eines XPath-Ausdrucks zu lokalisieren. Diese Funktionalität wird in der Klasse XmlNode von den Methoden SelectSingleNode(string xpathAusdruck) und SelectNodes(string xpathAusdruck) implementiert. XPath ist ein mächtiges Werkzeug, um nach einem Knoten mit gegebenen Eigenschaften zu suchen. Trotzdem fordert XPath auch großen Rechenaufwand wie wir später im Abschnitt über Performanz zeigen werden. XPath sollte daher nicht übermäßig gebraucht werden. Nachdem wir die relevanten Knoten erhalten, kann der Inhalt des Knotens mit Hilfe von den Eigenschaften Value oder InnerText als Zeichenketten zurückgeliefert oder festgelegt werden. Der XML-Code lässt sich einfach verändern, indem man die Eigenschaft InnerXml einstellt. Wenn ein neues Element des Dokuments erzeugt werden soll, stehen die Methode CreateAttribute(), CreateComment(), CreateElement() oder CreateTextNode() einer XmlDocumentInstanz zur Verfügung. Der neu erzeugte Knoten kann danach mit Hilfe der Methoden AppendChild(), PrependChild() oder ReplaceChild() zu einem bestehenden Knoten hinzugefügt werden. Die Methoden RemoveChild() und RemoveAll() ermöglichen dagegen das Entfernen von einem bzw. allen Kindknoten eines Knotens. 3.3 Beispiel Unten ist ein Code-Abschnitt, der das Lesen eines XML-Dokuments mit Hilfe von DOM in .NET beschreibt. In diesem Beispiel wird XPath verwendet um die Knoten mit relevanten Informationen schnell zu finden. Die Test-Datei ist dieselbe Book.xml wie im Beispiel mit XmlReader. XmlDocument doc = new XmlDocument(); doc.Load("book.xml"); Console.WriteLine(doc.SelectSingleNode("book/title").InnerText); Console.WriteLine(doc.SelectSingleNode("book/price/@currency").Value); float price = float.Parse(doc.SelectSingleNode("book/price")); 4 LINQ to XML Zusammen mit der Version 3.5 von .NET hat Microsoft die neue Technologie LINQ vorgestellt. Um die Vorteile von LINQ auch in der XML-Bearbeitung zu nutzen wurde eine neue Familie von Klassen unter dem Namespace System.Xml.Linq entwickelt, die ein ähnliches Konzept wie DOM implementieren. Diese Klassen sind nicht nur LINQ freundlich sondern wurden auch sehr gut optimiert und sie liefern bessere Performanz als die alten DOM-Klassen. 4.1 Vorteil zum alten DOM LINQ to XML hat im Vergleich zum DOM ein ähnliches Konzept. Für die Bearbeitung von XML-Daten mit LINQ to XML wird eine Hierarchie von Objekten aus verschiedenen LINQ to XML-Klassen auch im Zwischenspeicher aufgebaut. LINQ to XML bietet trotzdem einige stärke Vorteile zum DOM. Der erste Vorteil ist die wesentlich bessere Performanz. LINQ to XML wurde neu entwickelt und muss den DOM-Standard nicht entsprechen, deshalb kann sie sehr intensiv optimiert werden. Einige veraltete oder uninteressante Komponenten des DOM-Konzepts wurden nicht in LINQ to XML eingeplant. Außerdem ist es mit LINQ to XML möglich, nur mit einem Teil des Dokuments zu arbeiten, deshalb ist der Speicherbedarf und der Rechenaufwand von LINQ to XML geringer als von DOM. Eine weitere Stärke von LINQ to XML ist die Möglichkeit, einfacheren Code zu schreiben. Die Klassen von LINQ to XML implementieren einige neue Programmier-Konzepte und ermöglichen eine sauberere Codierung und einfachere Test- und Debuggen-Verfahren. Daraus folgt eine bessere Produktivität beim Entwicklungsprozess. Obwohl die Klassen von LINQ to XML auch ohne LINQ verwendet werden können, ist der größte Vorteil von LINQ to XML die Möglichkeit, LINQ-Abfrage direkt mit XMLDaten zu nutzen. Diese Abfragefunktion von LINQ to XML ist von ihrer Funktionalität her mit der von XPath und XQuery vergleichbar, die Syntax ist allerdings eine andere. Die Integration von LINQ mit dem Entwicklerwerkzeug Visual Studio bietet eine stärkere Typisierung, Syntaxüberprüfung bei der Kompilierung und verbesserte Debuggerunterstützung. 4.2 Bearbeitung von XML-Datei mit LINQ to XML In LINQ to XML kann nicht nur die Klasse XDocument sondern auch die Klasse XElement eines XML-Dokuments zum Bearbeiten verwendet werden. Die beiden Klassen verfügen über die mehrfach übergeladene Methode Load(), die das Lesen eines Dokuments aus einer URL, aus einem Stream oder aus einem XmlReader-Objekt realisiert. Im Gegensatz zu DOM ist es in LINQ to XML nicht verpflichtet, eine XML-Datei immer mit XDocument zu laden, denn XElement ist allein für die Bearbeitung von XML-Dateien ausreichend. Zur Navigation im Dokument-Baum bieten XNode und ihre abgeleiteten Klassen die Eigenschaften NextNode, PreviousNode und Parent. Außerdem verfügen die Klassen XElement und XDocument über einige Methoden, die die Suche nach einem oder mehreren untergeordneten Knoten mit einem gegebenen Namen ermöglichen: Descendants (XName name) und Elements(XName name). Der Inhalt des Knotens kann danach einfach mit “Type-Casting” erhalten werden. Dieses Design der LINQ to XML-Klassen ermöglicht die Verwendung von LINQ-Abfragen. Folgende ist Beispiel einer LINQ-Abfrage, die den Titel aller Bücher zurückliefert, deren Preis kleiner oder gleich 10 ist: var buecher = XElement.Load("buecher.xml"); var billige_buecher = from buch in buecher.Descendants("buch") where (float)buch.Element("preis") <= 10 select (string)buch.Element("titel"); 4.3 LINQ to XML mit riesiger Datei Ein wichtiger Vorteil von LINQ to XML zum DOM ist die Möglichkeit, ein großes Dokument nur mit geringer Speicherbeanspruchung zu bearbeiten, indem man das Dokument mit XmlReader liest und teilweise mit LINQ to XML verarbeitet. Die statische Methode ReadFrom(XmlReader reader) der abstrakten Klasse XNode kann das momentan von XmlReader gelesene Element aus dem Dokument extrahieren und ein XNode-Objekt davon aufbauen. Diese Fähigkeit ist nicht verfügbar in DOM. Unten ist der Beispiel-Code mit dem normalen DOM-Ansatz: XDocument doc = XDocument.Load("beispiel.xml"); // Hier wird das gesamte Dokument in den Zwischenspeicher geladen. Foreach (XElement e in doc.Root.Elements("item")) ItemBearbeiten(e); Die Datei beispiel.xml kann wegen einer großen Anzahl von den Kind-Elementen item so riesig sein, dass der Zwischenspeicher nicht ausreichen kann, um den gesamten DOM-Baum zu lagern. Hier ist es empfehlenswert, die Datei als Stream mit XmlReader zu lesen und jedes einzelnes item-Element mit LINQ to XML zu bearbeiten. XmlReader reader = XmlReader.Create("beispiel.xml"); while (reader.Read()) if (reader.Name == "item" && reader.NodeType == XmlNodeType.Element) { XElement e = XNode.ReadFrom(reader); // Hier wird nur das momentan gelesene "item"-Element geladen. ItemBearbeiten(e); }; 5 5.1 Vergleichen von Performanz Implementierung des Tests Der Test lässt sich einfach konstruieren. Die Test-Datei wird automatisch generiert und hat einen Wurzelknoten root mit verschiedenen Kindknoten child. Die Kindknoten haben keinen Inhalt, sondern nur eine zufällig erzeugte Zeichenkette als id-Attribut. Verschiedene Methoden werden dann genutzt, um dieses id-Attribut aller Kindknoten zu lesen. Für jede Methode wird der Test 100-mal durchgeführt und die durchschnittliche Laufzeit wird in Millisekunden zurückgegeben. Das Test-Ergebnis wird in der Tabelle 1 angezeigt. Unten ist ein Beispiel der Testdatei mit zwei Kindknoten: <root> <child id=’1259287450’/> <child id=’2334165534’/> </root> 5.2 Das Test-Ergebnis Vom Test-Ergebnis können wir ausgehen, dass XmlReader die beste Performanz hat und DOM die schlechteste Performanz hat. Ein Grund dafür ist, bei DOM und LINQ to XML muss die gesamte Datei erst fertig eingelesen und in den Zwischenspeicher gelegt werden. Außerdem lässt sich auch erkennen, dass Suchen und Navigation zu einem bestimmten Knoten bei der Bearbeitung eines XML-Dokuments sehr teuer ist, insbesondere wenn man XPath verwendet. Mit der Zunahme der Größe der Test-Datei geht der Zuwachs der Laufzeit der Varianten mit DOM und mit LINQ to XML schneller einher als mit XmlReader. Hier wird die Memory-Belastung nicht abgeschätzt, aber trotzdem lässt sich leicht erkennen, dass DOM und LINQ to XML für große Dokumente sehr viel Zwischenspeicher verbrauchen. Anzahl von Kindknoten XmlReader XmlDocument XmlDocument mit XPath XDocument XDocument mit Navigation 1 0,074830 0,095433 0,111558 0,089083 0,125736 10 0,071740 0,104943 0,104753 0,082286 0,096984 100 0,130494 0,208655 0,230719 0,178986 0,171954 1.000 0,590924 1,316731 1,477319 0,937331 1,047099 10.000 6,324870 13,803712 15,511574 10,170527 10,232822 100.000 57,057659 389,614392 421,267870 156,225456 159,162281 Tabelle 1: Laufzeit verschiedener Methoden in Millisekunde 6 Zusammenfassung Die .NET-Plattform von Microsoft bietet eine große Unterstützung um mit XML-Dateien zu arbeiten. XmlReader und XmlWriter sind sehr effektiv für große Dateien oder für Dateien, deren Strukturen schon vorher bekannt sind. Wenn Performanz und Speichereffizienz eine große Rolle spielen, sollten wir uns für XmlReader und XmlWriter entscheiden. Man muss trotzdem damit rechnen, den Code oft komplizierter schreiben zu müssen und die Applikationen schwieriger zu debuggen und zu testen. Im Gegensatz zu XmlReader ist LINQ to XML sehr gut für kleine Dateien oder Dateien mit komplexeren Strukturen geeignet. Diese Methode hilft auch, die Produktivität zu erhöhen, denn das Schreiben von Code ist mit LINQ to XML sehr leicht und überschaubar. Es wird häufig eine Kombination von LINQ to XML und XmlReader verwendet werden. XmlReader wird zuerst genutzt, um die Datei-Ströme einzulesen und die XML-Stücke werden dann teilweise mit LINQ to XML manipuliert. DOM hat die gleichen Vor- und Nachteile wie LINQ to XML, trotzdem ist die Performanz von DOM am schlechtesten. LINQ to XML und XmlReader/XmlWriter bieten außerdem genügend Werkzeug, um effizient mit XML-Dateien zu arbeiten. Daher ist es nicht empfehlenswert DOM weiter zu verwenden. Allerdings kann man DOM auch weiter nutzen, wenn Kompatibilitätsprobleme mit älterer Version der .NET-Plattform vorliegen oder wenn nicht alle Mitglieder des Entwicklerteams die andere Alternative kennen. Literaturverzeichnis 1. Joseph, A., Ben, A., C# 4.0 in a Nutshell, O‘Reilly, 2010. 2. David, H., Jeff, R., Joe, F., Eric, V., Danny, A., Jon, D., Andrew, W., Linda, M., Beginning XML, 4. Edition, Wrox, 2007. 3. MSDN, XML-Dokumente und XML-Daten, http://msdn.microsoft.com/ de-de/library/2bcctyt8(v=VS.100).aspx. 250.000 143,452533 997,281160 1070,537216 584,460105 597,603840 4. MSDN, LINQ to XML, http://msdn.microsoft.com/de-de/library/ bb387098.aspx. 5. Joe Ferner, Performance: LINQ to XML vs XmlDocument vs XmlReader, http: //www.nearinfinity.com/blogs/joe_ferner/performance_linq_ to_sql_vs.html, Near Infinity‘s blog, 05/2008. 6. James Newton-King, LINQ to XML over large documents, http://james.newtonking. com/archive/2007/12/11/linq-to-xml-over-large-documents. aspx, 12/2007. 7. Michael Jervis, DTDs vs XML Schema, http://articles.sitepoint.com/ article/xml-dtds-xml-schema, Sitepoint,11/2002.