Texte parsen mit Spirit (2)

Die Gleitkommazahlen beim letzten Mal waren ja doch etwas einfacher. Daher heute ein etwas komplexeres Beispiel: ein JSON-Parser mit Spirit.

Video

Quelltext

#include <iostream>
#include <string>

#include <boost/spirit/include/qi.hpp>

namespace qi = boost::spirit::qi;

int main()
{
    std::string json_text = R"json(
        {
            "number" : 123.456,
            "string" : "Eine Zeichenkette",
            "ein array" : [ 1, 5, 8.9, "Noch eine Zeichenkette"],
            "object" : {
                "name" : "Ein Objekt"
            }
        }
    )json";

    typedef qi::rule<std::string::iterator, qi::space_type> rule;

    rule object, members, pair, array, elements, value, string;

    object   = qi::char_('{') >> -members >> '}';
    members  = pair >> ',' >> members | pair;
    pair     = string >> ':' >> value;
    array    = qi::char_('[') >> *elements >> ']';
    elements = value >> ',' >> -elements | value;
    value    = string | qi::double_ | object | array | "true" | "false" | "null";
    string   = qi::lexeme[ qi::char_('"') >> *(qi::char_ - qi::char_('"')) >> '"'];

auto it = json_text.begin(); if (qi::phrase_parse(it, json_text.end(), value, qi::space) && it == json_text.end()) { std::cout << "JSON erfolgreich erkannt\n"; } else { std::cout << "Der Text ist kein gültiges JSON\n"; } }

Erklärung

Meist ist es sinnvoll beim parsen komplexerer Quelltexte die Leerzeichen automatisch zu überspringen. Oft sind diese ja lediglich überflüssige Trenner, die nur die Grammatik unnötig aufblähen. Daher bietet boost::spirit die Möglichkeit, das automatisch zu tun und sich auf die eigentlichen Symbole zu konzentrieren. Die erste Änderung an der Grammatik betrifft die Typdefinition für einzelne Regeln. Die brauchen nämlich den sogenannten Skipper-Typ, der genutzt wird, um Leerzeichen zu überspringen. Hier im Beispiel ist das boost::spirit::qi::space_type, der sich eben für Leerzeichen zuständig fühlt. Der eigentliche Skipper-Parser (das Element, was zum Überspringen genutzt wird), wird als vierter Parameter an die Funktion phrase_parse übergeben. Im Gegenzug zu parse() kann diese nämlich einen Skipper übernehmen.

So vorbereitet wird die Grammatik deutlich vereinfacht, da sie sich nur noch auf die eigentlichen Symbole konzentrieren muss. Um nochmal Tippaufwand zu sparen, kann man auf die automatischen Konvertierungsregeln von C++ zurückgreifen: So wird zum Beispiel in Zeile 25 das letzte Literal ('}') einfach als Character-Literal angehängt, statt einen Konstruktoraufruf von qi::char_ einzufügen. Da der Compiler weiß, dass dort eine rule erwartet wird, kann er selbst den entsprechenden automatischen Konstruktoraufruf einsetzen und man spart wieder etwas Rauschen.

Will man in einer Grammatik mehrere Alternativen ausdrücken, so bietet Spirit den operator|. Dieser prüft zuerst seine linke und dann seine rechte Seite auf ein Match und ist erfolgreich, wenn eins der beiden matcht. Hierbei muss man allerdings aufpassen. Wäre beispielsweise in Zeile 26 die Reihenfolge der beiden Seiten vertauscht, dann würde unser Parser fehlschlagen. Er würde nämlich zuerst auf ein einzelnes pair prüfen, dabei natürlich den entsprechenden Textteil verarbeiten, dann zur übergeordneten Produktion in object zurückkehren und als nächstes im String ein einzelnes Komma vorfinden, mit dem er nichts anfangen kann. Daher muss hier die längere Regel zuerst kommen, damit er gleich versucht, das Komma zu verarbeiten und erst wenn dass fehlschlägt zum einzelnen pair übergeht.

Immer wieder kommt es in Grammatiken vor, dass ein Element beliebig oft wiederholt werden kann. In formalen Grammatiken wird das mit dem sogenannten Kleene-Star ausgedrückt. Der kommt in der eigentlichen Notation zwar hinter dem Symbol, aber die C++-Grammatik lässt ihn nun mal nur als operator*() zu. *symbol ist daher die beliebige Wiederholung (inklusive 0) von symbol.

Da der Parser im Beispiel standardmäßig Leerzeichen überspringt, entsteht natürlich ein Problem beim Parsen des string-Symbols. Innerhalb der Anführungszeichen müssen Leerzeichen mit aufgenommen werden. Ein Programmierer wäre vermutlich ziemlich verwirrt, wenn ein String keine Leerzeichen enthalten könnte. Daher muss also das Überspringen von Leerzeichen kurzzeitig deaktiviert werden. Zu diesem Zweck wird die betreffende Teilgrammatik komplett in ein qi::lexeme eingefasst. Dieser Parser überspringt alle Leerzeichen bis zum ersten Match der inneren Grammatik und liest sie ab da alle mit ein.

Das Beispiel string zeigt noch ein anderes Problem auf: Der String sollte definiert sein als Anführungszeichen - Alles außer Anführungszeichen - Anführungszeichen. Wenn man das naiv umsetzt, dann kommt man (wie ich) auf die Idee, *(!qi::char_('"')) zu verwenden und stellt fest, dass der Parser dann in eine Endlosschleife läuft. Die Erklärung: operator!() matcht immer erfolgreich, ggf. halt mit einem String der Länge 0. operator*() wartet allerdings darauf, dass die innere Grammatik ein non-Match liefert, um weiterzumachen. Daher ruft operator* den operator! immer wieder auf, ohne im String vorwärts zu kommen. Will man bestimmte Zeichen ausschließen, dann muss man diese von der Grundmenge quasi "abziehen": qi::char_ - qi::char_('"') matcht alles, außer Leerzeichen, da dieser Ausdruck eine Regel produziert, die alles matcht, was auf der linken, aber nicht auf der rechten Seite ein Match liefert.