Texte parsen mit Spirit (1)

Regex sind für komplexere Anwendungsfälle nicht mächtig genug. Dafür gibt es formale Grammatiken, mit denen man Sprachen beschreiben kann. Der C++-Standard bietet zwar nichts passendes, um so eine Grammatik in C++ direkt auszudrücken, aber hier eilt dann boost::spirit zu Hilfe. Mittels einer in C++ definierten Domain Specific Language kann man Grammatiken direkt in passende Regelobjekte umwandeln.

Video

Quelltext

#include <iostream>
#include <string>
#include <list>

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

using namespace std;
namespace qi = boost::spirit::qi;

int main()
{
    list<string> texts { "-123.456", "123", "+987.000", "-", "123.", "abcd.efg" };

    typedef qi::rule<string::iterator> rule;
    
    rule number;
    rule float_;
    
    float_ = -qi::char_("+-") >> number >> -( qi::char_('.') >> number);
    number = qi::repeat(1, qi::inf)[ qi::char_("0-9") ];
    
    
    for (auto t : texts) {
        auto it = t.begin();
        
        if (qi::parse(it, t.end(), float_) && it == t.end()) {
            cout << '"' << t << "\" erfolgreich geparst.\n";
        }
        else {
            cout << '"' << t << "\" ist keine gültige Gleitkommazahl.\n";
        }
    }
}

Erklärung

Grammatiken in der Erweiterten Backus-Naur Form definieren formale Sprachen und lassen sich nutzen, um Zeichenketten nach den Regeln dieser Sprachen zu parsen. Eine Grammatik besteht hierbei aus zwei Grundlegenden Elementen: Terminalsymbolen (letztlich: Zeichenketten, die so, wie beschrieben, vorkommen und nicht weiter aufgelöst werden) und Produktionsregeln, mit denen einfachere Strukturen zu komplexeren verbunden werden. Im Beispiel oben sind die Ziffern 0-9, der Punkt oder Plus und Minus Terminalsymbole. Diese Zeichenketten werden letztlich im zu parsenden Text gesucht. Ein Beispiel für eine Produktionsregel ist der Nachkommaanteil der Zahl: ein Punkt gefolgt, von einer number (die ihrerseits wiederum eine Produktionsregel auf Basis der Ziffern darstellt). 

Diese Grammatiken lassen sich mit der DSL von boost::spirit direkt in C++ ausdrücken. Die Syntax ist dabei eng an der Syntax der EBNF angelehnt. Aus Gründen der C++-Syntax ist es nicht möglich, direkt den gleichen Aufbau zu verwenden, aber wer schonmal eine EBNF-Grammatik gesehen hat, der erkennt die in der Struktur der entsprechenden C++-Ausdrücke wieder. Spirit verwendet hierbei massiv Operatorüberladung, um dieses Ziel zu erreichen. So wird bspw. aus dem operator>>() der Ausdruck für eine Folge aus zwei Elementen. operator-() wird zur Darstellung einer optionalen Regel (bspw. der Nachkommaanteil im Beispiel oben, der komplett in -() eingefasst ist), während operator[] herhalten muss, um die zu wiederholende Regel für die repeat-Produktion einzufassen (im Beispiel von 1 bis unendlich viele Wiederholungen für Ziffern in einer Zahl). Jeder dieser Ausdrücke liefert ein von boost::spirit::qi::rule abgeleitetes Objekt zurück, welches eine Bestimmte Sub-Grammatik erkennt. Werden zwei Grammatiken mit einem Operator verbunden, so ist das Ergebnis wiederum eine Grammatik, die beide Grammatiken und den Operator erkennt (bspw. bei operator>>() erst die linke Seite und bei erfolgreichem Match die rechte Seite).

Um die Grammatik tatsächlich auf einen String anzuwenden, übergibt man sie als Parameter an boost::spirit::qi::parse(). Diese Methode nimmt den Anfangs- und Enditerator des zu parsenden Bereiches, sowie das Regelobjekt, welches die Wurzel des Regelbaumes darstellt, und versucht die Zeichenkette zwischen den Iteratoren anhand der Regeln zu zerlegen. Wird überhaupt etwas erkannt, so liefert diese Funktion true zurück und schreibt die letzte erfolgreich erkannte Stelle in ihren ersten Parameter. Daher sollte dort eine lokale Kopie des Anfangsiterators übergeben werden (siehe Zeilen 24 und 26). Sind Anfangs- und Enditerator nach dem Parsen gleich, so wurde der komplette String gelesen (was in unserem Gleitkomma-Beispiel oben als Erfolg gilt). Diesen Erfolg oder Mißerfolg gibt das Programm dann noch entsprechend aus.