C++ XML Parser
Die Wahl des richtigen C++ XML Parser ist gar nicht so einfach. Je nach Anwendungsfall gibt es eine andere „optimale“ Bibliothek. Vom schnellen Parsen bis zur vollständigen Unterstützung des XML Standards reicht das Spektrum einer ganzen Reihe von XML Bibliotheken für C++.
C++ XML Parser
Ich lehne mich einmal weit aus dem Fenster und behaupte die meisten Projekte verwenden XML Dateien als Konfigurationsdateien. Der XML Standard hat schon vor vielen Jahren die in C/C++ Projekten früher üblichen *.ini Dateien abgelöst. In so einem Anwendungsfall reicht ein schneller und einfacher XML Parser.
Einfacher XML Parser
Ich habe mich für mein Projekt für TinyXML entschieden, da es recht einfach eingebaut werden kann (nur eine Header und eine Source Datei). TinyXML ist einfach zu benutzen aber im vergleich zu beispielsweise RapidXML extrem langsam. Das mag bei kleinen Konfigurationsdateien nicht auffallen, will man aber große XML Strukturen parsen ist man mit RapidXML besser bedient. Man soll beide Parser verwenden können, deshalb baue ich das Interface mit diesem Gedanken.
Ansatz
Mein erster Ansatz war ein Interface, welches von einer TinyXML und einer RapidXML Klasse verwendet wird. Dabei kommt man aber gleich zu einem Problem: Methoden geben spezielle Objekte zurück. Das sind je nach dem XMLNode, XMLElement, XMLAttribute oder vergleichbares. Diese Objekte gibt es sowohl bei TinyXML als auch RapidXML mit einigen unterschieden. Was tun? Weitere Wrapper schreiben? Basierend auf einer der beiden Bibliotheken und dann Anpassungen für die andere Bibliothek dazubauen?
Offenbar war mein erster Ansatz keine gute Idee. Man sollte noch einmal einen Schritt zurück gehen und sich die Anforderungen vom Projekt ansehen. Ich möchte XML Dateien mit Daten und Konfigurationen laden und deren Werte auslesen. Ein Testdriven Development Ansatz ist offenbar zielführender, weshalb ich mir im Testmodul eine test.xml Datei mit einigen üblichen XML Konstrukten angelegt habe:
<?xml version="1.0" encoding="utf-8"?> <root> <testsettingint>17</testsettingint> <testsettingstring>version 1.1.2</testsettingstring> <samplecategory> <testsettingfloat>8.98273</testsettingfloat> <settingarray><![CDATA[***************** ++++++++++++++ ************** ääääääääääääää öööööööööööööö üüüüüüüüüüüüüü]]></settingarray> </samplecategory> <tree1> <tree2> <tree3> <deepdata name="test123" value="98"> Some Text to demonstrate this. </deepdata> </tree3> </tree2> </tree1> </root>
Testdriven Development
Basierend auf diesem Dokument entwickle ich nun Tests, die ich im Interface und den beiden Implementierungen für TinyXML und RapidXML umsetze. Erst wenn jeder Testfall von beiden Implementierungen unterstützt wird kommt es zum nächsten Testfall. Die mit Google Test implementierten Tests sehen so aus:
#include "../Core/TinyXMLParser.h" #include "../Core/RapidXMLParser.h" // Test XML Parser TEST(TinyXMLParser, init) { TinyXMLParser p; p.loadFile("test.xml"); EXPECT_STREQ(p.getValue("root/testsettingint").data(), "17"); EXPECT_STREQ(p.getValue("root/testsettingstring").data(), "version 1.1.2"); EXPECT_STREQ(p.getValue("root/samplecategory/testsettingfloat").data(), "8.98273"); EXPECT_STREQ(p.getAttributeValue("root/tree1/tree2/tree3/deepdata", "name").data(), "test123"); } TEST(RapidXMLParser, init) { RapidXMLParser p; p.loadFile("test.xml"); EXPECT_STREQ(p.getValue("root/testsettingint").data(), "17"); EXPECT_STREQ(p.getValue("root/testsettingstring").data(), "version 1.1.2"); EXPECT_STREQ(p.getValue("root/samplecategory/testsettingfloat").data(), "8.98273"); EXPECT_STREQ(p.getAttributeValue("root/tree1/tree2/tree3/deepdata", "name").data(), "test123"); }
Der Testdriven Development Ansatz zahlt sich wirklich aus. Nach einigen Entwicklungen in falsche Richtungen habe ich nun einen einfach erweiterbaren XML Parser für meine Konfigurationsdateien.
#pragma once #include #include class XMLParserFacility { public: virtual ~XMLParserFacility() = default; // read XML virtual void loadFile(const std::string filename) = 0; virtual std::string getValue(const std::string path) = 0; virtual std::string getAttributeValue(const std::string path, const std::string name) = 0; // write XML // currently not needed }; using XMLParser = std::shared_ptr;
Wie man sehen kann sehr Basic. Man kann eine XML Datei laden und von dort einen Wert laden (unter Angabe vom Pfad) und ein Attribut. Mehr ist für meine Konfigurationsdateien nicht nötig, das Interface könnte man so aber jederzeit erweitern. Implementiert wurden davon TinyXML und RapidXML. Die Header Dateien sehen wie folgt aus:
#pragma once #include #include "XMLParserFacility.h" #include "tinyxml2.h" class TinyXMLParser : public XMLParserFacility { public: TinyXMLParser() : doc() {} virtual void loadFile(const std::string filename) override { doc.LoadFile(filename.data()); } virtual std::string getValue(const std::string path) override; virtual std::string getAttributeValue(const std::string path, const std::string name) override; private: std::vector splitPath(std::string path); tinyxml2::XMLElement* findElement(std::string path); tinyxml2::XMLDocument doc; };
und
#pragma once #include #include #include "XMLParserFacility.h" #include "rapidxml_utils.hpp" class RapidXMLParser : public XMLParserFacility { public: RapidXMLParser() : doc() {} virtual void loadFile(const std::string filename) override { this->filename = filename; } virtual std::string getValue(const std::string path) override; virtual std::string getAttributeValue(const std::string path, const std::string name) override; private: std::vector splitPath(std::string path); rapidxml::xml_node<>* findElement(std::string path); rapidxml::xml_document<> doc; std::string filename; };
Komplexer XML Parser
In Zeiten von Microservices werden Schnittstellen immer wichtiger. Der XML Standard hat sich dafür bewährt (obwohl Web-Schnittstellen meist einfachere Formate wie JSON verwenden). In den meisten Fällen kommt man auch in diesen Anwendungsfällen mit einem einfachen XML Parser aus. Leider gibt es aber manchmal besonders pingelige Systeme die Abweichungen vom Standard nicht tolerieren. In diesen Fällen empfiehlt sich der Einsatz von beispielsweise Xerces.
Wie auch schon bei meiner Definition der Schnittstelle des professionellen Loggings macht der Einsatz der richtigen Design Patterns Sinn. Über ein Interface und einer Factory kann man auch bei weit fortgeschrittenem Projekt die verwendete XML Bibliothek einfach austauschen.
Fazit
Kaum ein Projekt kommt ohne einen XML Parser aus. Das Format ist heute Standard bei jeglicher Schnittstelle in Filesystem oder zu anderen Systemen. Die Wahl eines C++ XML Parser ist gar nicht so einfach, es gibt so viele Bibliotheken. Je nach Anwendungsfall ist man mit dem einen oder anderen besser bedient. In jedem Fall empfiehlt es sich nicht das Rad neu zu erfinden. Mit kluger Architektur lässt sich die verwendete Bibliothek leicht austauschen.
Hi, gibts den kompletten code irgendwo zu sehen? Github oder ähnliches?
Ja den gibt es. In folgendem Projekt habe ich den TinyXML und den RapidXML Parser implementiert: https://github.com/Ziagl/A3Editor/tree/master/Core