xml parser schreiben, wie Format und ausgabe gestalten?



  • nach meiner abstinenz zeit wollte ich mich nun wieder mal einem kleineren projekt zuwenden: einem minimalen xml parser um zb datensätze einzulesen/schreiben zu können.

    ich würde also gerne zb sowas auswerten können(ganz dummes Beispiel):

    <datensatz>
        <id>15</id>
        <name>foo</name>
        <list typ="float" separator="," name="werte">15.5,16.7,17.9</list>
    </datensatz>
    

    die implementation des Parsers selber wird wohl nicht das problem sein(dank boost::spirit 😃 ).

    worum ich mir sorgen mache ist folgendes:

    Result foo=parse(xmlDatei,Format);
    

    die frage ist, wie ich einerseits Format, andererseits Result gestalten soll. In Format sollen die infos darüber enthalten sein, wie der Datensatz aufgebaut ist, und Result soll das enthalten, was nach dem parsen übrigbleibt, also die Daten. Ich habe bisher für Format an eine spirit ähnliche syntax gedacht, vermute aber, dass das a) zu schwierig und b) zu komplex wäre. ein ähnliches problem gibt es auch bei Format, ein Baum wär wohl am einfachsten zu implementieren, und würde die struktur des datensatzes am besten wiedergeben, andererseits macht das die aufbereitung der Daten hinterher wieder schwerer(polymorphe knoten um die einzelnen typen unter einen hut zu bekommen). Ideal wär, dass in Result am ende einfach nur die Datensätze so wie sie hinterher im programm gebraucht werden enthalten sind, dh um das obige beispiel aufzugreifen sowas:

    struct Datensatz{
        unsigned int id;
        std::string name;
        std::vector<float> werte;
    };
    
    Result<Datensatz> foo=parse(xmlDatei,Format);
    

    aber das würde wahrscheinlich zwangsläufig Format wieder verkomplizieren...ach ich hab keine ahnung^^. Wie würdet ihr das regeln?



  • auch wenn niemand drauf geantwortet hat, schreib ich einfach mal meine fortgeschrittenen überlegungen weiter^^

    was das format angeht, hab ich mich doch für ein expression template entschieden, da es am einfachsten zu lesen und zu verstehen ist.

    Beim rückgabewert hab ich mir was ausgedacht:
    da ich jetzt doch nur statische formate parsen lassen können will(dh die tags sind immer in der selben reihenfolge in der datei), dachte ich mir, dass parse eine art tuple zurückgeben könnte.

    nehmen wir mal folgendes beispiel:

    <Kunde>
        <Nummer>1337</Nummer>
        <Vorname>Max</Vorname>
        <Nachname>Mustermann</Nachname>
    </Kunde>
    

    dann sähe das passende tuple(nicht boost::tuple, das wird den anforderungen nicht gerecht) wohl so aus:

    Tuple<Typlist<int,Typlist<std::string,Typlist<std::string,NullType> > >
    

    parse gibt nun dieses tuple zurück, und der user muss damit arbeiten.
    Die frage ist nun: kann man dem user sowas zumuten? Das problem ist, dass der user der das Format schreibt das problem hat, dass er so nicht sehen kann, wie das tuple nun am ende aussieht. er muss sich also sein Format die ganze zeit vor augen halten, um kein fehler hinterher beim auslesen der Typen aus dem Tuple zu machen. Bei diesem kleinen beispiel kann man zwar aus dem Format noch sehen, dass die typen int,std::string,std::string sind, aber wie siehts bei komplexeren Formaten aus?
    Nicht dass wir uns falsch verstehen: ich hätt keine probleme damit, einen Baum zurückzugeben, aber mich hats bei boost::spirit schon immer etwas genervt, dass man entweder die grammatik mit irgendwelchen aktors unlesbar machen musste, oder sich einen baum zurückgeben lies, bei dem man dann nochmal jeden Knoten einzeln durchgehen musste, um zu erkennen was fürn typ das jetzt sein soll. Aber andererseits will ich jetzt nicht von der einen Misere in die nächste Stapfen, indem ich undurchschaubare tuples zusammenbaue^^



  • will ja nichts sagen, aber einen xml parser gibt es schon (ausser du willst zum üben einen für dich schreiben ). aber wie gesagt einen prof. gibts schon 🙂



  • Natürlich will er einen zum üben schreiben. Hab auch schon mal vor 4 Jahren einen minimalen XML-Parser geschrieben... obwohl es schon XML-Parser zu Hauf gibt. Solange man nicht im Projektgeschäft ist, kann man sich vieles selber schreiben.



  • will einfach mal üben, aber ich mach das immer unter dem aspekt "wie würden fremde leute damit am einfachsten arbeiten können". macht die sache zwar nicht leichter, aber imho lernt man mehr dabei.



  • sorry, ich hab nicht alles gelesen.
    nur schoen vom anwenden faend ich sowas:

    XmlElement e = Parse("abc.xml");
    try
    {
      int id       = e["Datensatz"]["id"];
      string kunde = e["kunde"].find("nummer",1337);
    } 
    catch (ElementNotFoundException& e)
    {
      // ...
    }
    


  • so nicht zu machen, weil der op[] keine unterschiedlichen rückgabetypen haben kann.

    was vielleicht ginge wär sowas:

    int id=e.find("Datensatz").find<int>("id");
    

    zwar nichtmehr ganz soschön zu lesen, aber ginge.
    ist imho aber auch ein völlig anderer ansatz, weil parse dann einen Baum erstellt, der hinterher durchsucht wird.

    Mein ziel war es aber, dass das Parse ergebnis eben kein Baum ist, sondern wenn möglich schon die komplett ausgewerteten Datensätze ausspuckt(wenn der user tuple akzeptieren würde, könnte man ja noch weitergehen, und parse soweit ausbauen, dass die tuples gleich mithilfe einer vom user geschriebenen Funktion umgewandelt werden...oh das wär schön...).

    Ein Traum wär dann zb sowas:

    //abc xml enthält viele Datensätze
    std::vector<Datensatz> ergebnis=parse<Datensatz>("abc.xml",Format);
    


  • Wäre vielleicht ganz hilfreich die Rahmenbedingungen noch ein wenig besser darzustellen.

    Geht's nur um Datensatzdateien im XML-Format? Ohne Querverweise?
    Wie sieht's hiermit aus:

    <Kunde>
        <Nummer>1337</Nummer>
        <Vorname>Max</Vorname>
        <Nachname>Mustermann</Nachname>
        <Adresse>
            <Strasse>Musterstrasse</Strasse>
            <Plz>1337357</Plz>
            <Ort>Munster</Ort>
        </Adresse>
    </Kunde>
    

    Damit zusammenhängend natürlich die Frage wie's mit eigenen Datentypen in XML aussieht?

    Wenn's nur PODs und ein paar STL-Container gibt, was spricht dagegen aus dem struct Datensatz ein Objekt zu machen auf das man assoziativ zugreifen kann? Oder zu viel Overhead?

    Und wenn's tatsächlich nur um solche Datensätze geht würde ich das uU eher als SAX-Aufsatz oder so implementieren.

    (Ein Framework um simpel gleich richtige Objekte zu erzeugen wäre auch nicht schlecht 😉 )



  • otze schrieb:

    so nicht zu machen, weil der op[] keine unterschiedlichen rückgabetypen haben kann.

    Wäre eventuell eine Möglichkeit boost::any einzusetzen? Aber dann muss blöd rumgecastet werden...



  • die rahmenbedingungen sind folgende:

    Dein Beispiel ist das maximale was in der Version 1.0 geparst werden können soll. halt so primitiv wie möglich. was ich in einer version 2.0 mache steht noch in den sternen(wenns sie denn überhaupt gibt)^^

    was vielleicht noch dazukommt ist etwas, dass es erlaubt, dass bestimmte tags mehrfach vorkommen(zweit und drittwohnsitz oder mehrere telefonnummern etc), aber da würde das tuple prinzip wohl schon den geist aufgeben, weil die typen im rückgabewert einfach zu komplex werden(eine einzelne telefonnummer ist ein int, mehrere sind ein vector<int>,mehrere addressen währen dann ein

    vector<Tuple<Typlist<string,Typlist<int,Typlist<string,Nulltype> > > > >
    

    Jaa freude 😃

    Wenn's nur PODs und ein paar STL-Container gibt, was spricht dagegen aus dem struct Datensatz ein Objekt zu machen auf das man assoziativ zugreifen kann? Oder zu viel Overhead?

    versteh grad nicht, was du meinst.
    Datensatz aus meinen Beispielen soll ein vom user definierter typ sein, der dann auch hintrher im programm so benutzt wird.

    Walli schrieb:

    otze schrieb:

    so nicht zu machen, weil der op[] keine unterschiedlichen rückgabetypen haben kann.

    Wäre eventuell eine Möglichkeit boost::any einzusetzen? Aber dann muss blöd rumgecastet werden...

    dann könnte ich die zahlen etc auch gleich in ihrer rohform halten und hinterher durchn stringstream schicken, das problem ist bei allen möglichkeiten, dass man immer im blick haben muss, was welcher typ ist, und das behagt mir garnicht.



  • otze schrieb:

    Dein Beispiel ist das maximale was in der Version 1.0 geparst werden können soll. halt so primitiv wie möglich. was ich in einer version 2.0 mache steht noch in den sternen(wenns sie denn überhaupt gibt)^^

    Naja, wenn ein Datensatz einen Datensatz enthalten kann dann sollte dieser wohl auch recht problemlos noch einen weiteren enthalten können, oder? (Meine vom Aufwand her, da dürfte wohl hauptsächlich das ob überhaupt eine Rolle spielen.)
    Oder meintest du Adresse als "eigener" Datentyp?

    otze schrieb:

    was vielleicht noch dazukommt ist etwas, dass es erlaubt, dass bestimmte tags mehrfach vorkommen

    Eine Liste? 😃

    otze schrieb:

    eine einzelne telefonnummer ist ein int

    Eine Telefonnummer ist ein string oder vector<int> 😉 👍

    Was ich vorhin vergessen hab:

    otze schrieb:

    <list typ="float" separator="," name="werte">15.5,16.7,17.9</list>
    

    Das sieht ziemlich übel aus - wie wär's mit

    <list typ="float" name="werte">
      <e>15.5</e>
      <e>16.7</e>
      <e>17.9</e>
    </list>
    


  • also, ich fang nochmal von vorn an(hab euch leider mit meinen gedankensprüngen verwirrt)

    die version 1.0 wird folgenden Ausdruck unterstützen:

    <Foo>Bar</Foo>

    Foo ist ein beliebiger String
    Bar ist entweder ein Wert oder wieder so ein Ausdruck(also ist endlosverschachtelung erlaubt).

    mehr darf die XML datei nicht enthalten, damit mein Parser keine exception wirft ;).

    Dem Parser wird erstmal ein string oder ein iteratorpaar und ein Format übergeben. Dazu besitzt Parser noch einen template parameter(dazu später mehr).

    Dass Format soll die Form der XML datei angeben.

    zwar ist syntaxmäßig noch nichts entschieden, aber das ganze könnte zb so aussehen:

    Datei:

    <Kunde>
        <Nummer>1337</Nummer>
        <Vorname>Max</Vorname>
        <Nachname>Mustermann</Nachname>
    </Kunde>
    

    ausruck in C++

    //ich nenn den Ausdruck 
    <Foo>Bar</Foo> einfach mal Tag
    
    Tag("Kunde").inner(
        Tag("Nummer").inner(Integer()),
        Tag("Vorname").inner(String()),
        Tag("Nachname").inner(String())
    )
    

    wenn ein Datensatz mehrfach vorkommen kann, wird es vielleicht möglich sein durch benutzen des op * dies anzudeuten(anleihe aus spirit)

    eine liste die floats annimmt, könnte vielleicht dann mit folgendem ausdruck dargestellt werden:

    Tag("list").inner(
        *Tag("e").inner(Real())
    )
    

    Wenn die eingabe mit diesem Format nicht übereinstimmt, wird ne exception geworfen, sonst passiert folgendes:

    (der templateparameter der funktion wird hier T genannt)

    -es wird ein tuple erstellt und gefüllt. Das Tuple richtet sich nach dem Format

    -das Tuple wird einer convert<T> funktion übergeben.

    -die convert<T> funktion mach aus dem Tuple ein T

    -T wird zurückgegeben

    (und ab hier wirds gänzlich unausgegoren:)

    -wenn der "höchste" Parser(also der der ganz links im Format steht) ein op* ist, tritt ein sonderfall auf

    -der rückgabewert der funktion ist ein std::vector<T>

    -die funktion convert<T> wird für jeden verfügbaren "Datensatz" aufgerufen und T dann in einen vector inserted

    -der vector wird zurückgegeben

    im endeffekt ist die sache mit der rückgabe und konvertierung ziemlich mies, aber ich lass mir gerne da reinreden 😃



  • (Ohne mir das jetzt alles durchgelesen zu haben.)
    Warum nimmst du als Format nicht einfach einen ifstream, der auf einen dtd zeigen sollte? Validierung ist was feines, da kümmert sich der Endanwender unger drum...
    Als ausgabe könnte ich mir folgendes vorstellen:
    Erstmal legen wir folgendes fest:
    -es gibt elemente, diese können Attribute haben. Attribute speichern wir in stringform
    -Elemente können entweder PCDATA (strings) oder andere Elemnte enthalten (wen interessieren kommentare und processing instructions 😃 )
    Dann würden mir folgende Lösungen einfallen:
    1.: du machst einen sequenziellen Parser (also eine abstrakte Parserklasse, von der man ableiten und virtuelle Funktionen á la begin_element überschreiben muss)
    2.: du gibst das als ganzes Zurück:

    struct element
    {
        enum type{pcdata,element};
        type t;
        union
        {
            std::vector<element> v;
            std::string p;
        }data;
    };
    

    Ich schätze mal, das steht spirit beides im wege.
    Oder du machst einen auf 1337 und lässt ein statisches, templatebasiertes Format zu. Da dürfte die Benutzung dann aber mehr oder weniger unmöglich werden...



  • Oder du machst einen auf 1337 und lässt ein statisches, templatebasiertes Format zu. Da dürfte die Benutzung dann aber mehr oder weniger unmöglich werden...

    wieso unmöglich? wenn zur compilezeit alles über das format bekannt ist, ist das doch ok?

    Ich schätze mal, das steht spirit beides im wege.

    spirit kann das. spirit bietet 2 arten der verarbeitung: als baum nach dem parsen oder während des parsens mittels closures und lambda ausdrücken/functoren. da man relativ gut erstellen kann, was in den baum reinkommt bzw die lambda ausdrücke höchst mächtig sind, geht das klar.


Anmelden zum Antworten