Parsen mit boost::spirit macht Probleme.



  • N'abend Leute,

    ich habe drei Anforderung an die Grammatik eines textfile bzw. eines string mit dem Inhalt des textfile.

    1. Es gibt beliebig viele Pflichteinträge.
    2. Es gibt beliebig viele optionale Einträge.
    3. Die Reihenfolge der Einträge aus 1. und 2. ist beliebig.

    Ein Bsp.:

    Es seien die Pflichteinträge pe0, pe1 und pe2 gegeben.
    Weiter seien die optionalen Einträge oe0 und oe1 gegeben.

    Damit ergibt sich:

    3! Einträge bei Verwendung von pe0, pe1, p2
    +
    4! Einträge bei Verwendung von pe0, pe1, p2, oe0
    +
    4! Einträge bei Verwendung von pe0, pe1, p2, oe1
    +
    5! Einträge bei Verwendung von pe0, pe1, p2, oe0, oe1

    3! + 4! + 4! + 5! = 174 mögl. Variationen eines textfiles

    Damit hat eine Variation des textfiles folgenden Inhalt:

    PflichtEintrag0
    PflichtEintrag2
    PflichtEintrag1

    Eine weitere Variation hat folgenden Inhalt:

    PflichtEintrag2
    OptionalerEintrag1
    PflichtEintrag1
    PflichtEintrag0

    Bei dem o.g. Bsp. habe ich bisher Punkt 3. meiner Anforderung ausgelassen. D.h. die Reihenfolge ist fest vorgegeben.

    rule<> pe0 = strlit<>("PflichtEintrag0");
        rule<> pe1 = strlit<>("PflichtEintrag1");
        rule<> pe2 = strlit<>("PflichtEintrag2");
        rule<> oe0 = strlit<>("OptionalerEintrag0");
        rule<> oe1 = strlit<>("OptionalerEintrag1");
        rule<> textfile = pe0 >> *space_p >> pe1 >> *space_p >> pe2
                >> *space_p >> *oe0 >> *space_p >> *oe1 >> *space_p;
    

    Und jetzt zum Schluss meine Frage:

    Wie zum Teufel kann ich spirit beibringen das die Reihenfolge meiner Einträge egal ist?

    Besten Dank für jeden Tip!
    Goran



  • ich weiß nicht, ob boost::spirit dafür die beste wahl ist.
    Was hältst du von folgendem:

    std::set<string> mgl_eintraege;
    
    // mgl_eintraege befüllen
    mgl_eintraege.insert("Pflichteintrag0");
    mgl_eintraege.insert("Pflichteintrag1");
    mgl_eintraege.insert("Pflichteintrag2");
    mgl_eintraege.insert("optional0");
    
    vector<string> vorhanden;
    
    // Die datei schrittweise/Zeilenweise/wortweise durchgehen und jedes Wort im 
    // set suchen. Wenn es im set vorhanden ist, in den vector vorhanden einfügen.
    // Wenn du die Datei dann durchgegeangen bist, steht im vector, welche 
    // Einträge in der Datei waren.
    // Und wenn du willst, kannst du den ja dann auch noch sortieren oder so.
    


  • Ich habe das Bsp. absichtlich vereinfacht. Die einzelnen Einträge sind komplexer bzw. flexibler in ihrer Gestaltung.

    Die verschiedenen Einträge werden mit komplexeren rules abgebildet. U.a. werden Regexe genutzt. Um die Komplexität zu kapseln würde ich einen eigenen, sehr flexiblen Parser schreiben müssen. Den Aufwand will ich mir ersparen.

    Weiter wäre auch eine Möglichkeit per semantic actions zu prüfen ob der jeweilige Eintrag schon einmal geprüft wurde. Die Prüfung findet dann aber außerhalb von spirit statt und ich weiß dann nicht an welcher Position der Inhalt des textfile falsch ist.



  • Du könntest dir noch eine eigene Parser-Subklasse ableiten, die sich merkt wie oft (z.B. relativ zu einem Schlüssel in einer Map) sie aufgerufen wurde und beim zweiten Mal kein Match liefert.



  • oder du schreibst dir für jeden Eintrag eine rule auf und überprüfst dann zeile für zeile, welcher parser gerade passt? ODer sind die Einträge leider nciht durch zeilenumbrüche getrennt?



  • Die Notwendigkeit, dass Pflichteinträge komplett vorhanden sein müssen und jeder Eintrag nur einmal vorhanden sein darf, scheint schon keine wirklich kontextfreie Grammatik mehr zu sein (bin mir nicht ganz sicher, die Theorie-Gurus mögen mir verzeihen falls ich mich irre).
    Ich würd deshalb die pure Grammatik für den Aufbau und den Vollständigkeitscheck trennen:

    Mach dir an deine einzelnen Eintrags-rules semantic actions, die den jeweiligen Eintrag in eine Tabelle eintragen und bei doppelten Einträgen einen Eintrag in einen Fehlerlog machen. Die start-rule, die das ganze Dokument parsen soll, schließt du mit einer weiteren semantic action ab, die überprüfut ob jeder Pflichteintrag geliefert wurde und ggf. eine weitere Fehlermeldung ins Log ausgibt.
    Wie du das Log gestaltest ist natürlich dir überlassen - ich würds als list<string> implementieren wo jede Meldung einzeln angehängt wird. Die Aufrufende Funktion des Parsers muss dann natürlich nicht nur das Ergebnis des Parsers berücksichtigen sondern auch ob die Liste gefüllt ist oder nicht.



  • Ich hatte schon semantic actions probiert und dann wieder verworfen. Der Grund war das ich innerhalb einer semantic action nicht weiß an welcher Stelle im textfile der Parser steht (keine Verfügbarkeit eines parse_info Objekts).

    Die Stelle (ein unsigned int) ist wichtig um im Falle eines Abbruchs (ein doppelter Eintrag ist entdeckt worden) eine Ausgabe zu bauen die besagt an welcher Stelle das textfile fehlerhaft ist.

    Kann ich an die besagte Stelle irgendwie rankommen? Vielleicht ist es möglich den Parser zur Laufzeit zu ändern?



  • Ich habe mal meinen Ansatz etwas ausgearbeitet:

    #include <iostream>
    #include <string>
    using namespace std;
    
    #include <boost/spirit.hpp>
    using namespace boost::spirit;
    
    template<int Key, int Max>
    struct TestMax {
    	static int counter;
    
    	template<class ResultT, class ScanT>
    	static match<> test(ResultT result, ScanT scan) {
    		if (counter < Max) {
    			counter++;
    			return match<>(result.length());
    		}
    		return scan.no_match();
    	}
    };
    
    template<int Key, int Max>
    int TestMax<Key, Max>::counter = 0;
    
    template<int Key>
    struct TestMax<Key, 0> {
    	template<class ResultT, class ScanT>
    	static match<> test(ResultT result, ScanT scan) {
    		return match<>(result.length());
    	}
    };
    
    template<int Key, class RuleT, int Max = 0>
    class CounterParser : public parser<CounterParser<Key, RuleT, Max> > {
    	RuleT rule; // must be default-constructible
    	string keyStr;
    public:
    	typedef CounterParser<Key, RuleT, Max> self_t;
    
    	CounterParser(string keyStr) : keyStr(keyStr) { };
    
    	template <typename ScannerT>
    	typename parser_result<self_t, ScannerT>::type
    	parse(ScannerT const& scan) const {
    		using namespace boost::spirit;
    
    		typedef typename ScannerT::iterator_t Iterator;
    		typedef typename parser_result<self_t, ScannerT>::type ResultType;
    
    		Iterator save = scan.first;
    
    		ResultType result = (str_p(keyStr.c_str()) >> rule).parse(scan);
    
    		if (result) {
    			return TestMax<Key, Max>::test(result, scan);
    		}
    
    		scan.first = save;
    		return scan.no_match();
    	}
    };
    
    typedef uint_parser<unsigned> pe0_t;
    // 0     - Key
    // pe0_t - Typ der Regel, die nach dem Text kommt
    // 1     - Maximale Anzahl.
    typedef CounterParser<0, pe0_t, 1> pe0;
    
    int main() {
    	pe0 parser("Pflichteintrag0");
    	parse_info<> r = parse("Pflichteintrag0 123 Pflichteintrag0 456", *parser, space_p);
    	if (!r.full) {
    		cout << r.stop << endl;
    	} else {
    		cout << "Erfolgreich geparst." << endl;
    	}
    	return 0;
    }
    

    Jetzt wird erfolgreich der 2. Pflichteintrag als Fehler gemeldet.

    Anmerkungen:
    - String-Parameter werden von Templates scheinbar nicht so recht unterstützt, deswegen der Integer.
    - Den space_p brauchst du nicht immer mit anzugeben.
    - Die Initialisierung des Zählers erfolgt nur einmal zum Programmstart. Sollte bei mehrmaligem Parsen wieder zurückgesetzt werden.
    - "0" als Maximalwert (siehe Kommentar) bedeutet "ohne Beschränkung"



  • goran schrieb:

    Die Stelle (ein unsigned int) ist wichtig um im Falle eines Abbruchs (ein doppelter Eintrag ist entdeckt worden) eine Ausgabe zu bauen die besagt an welcher Stelle das textfile fehlerhaft ist.

    Ich würd den Parsvorgang nicht abbrechen beim ersten Fehler sondern soweit wie möglich durchlaufen lassen. Damit hast du dann gleich so viele Fehlermeldungen wie möglich (im besten Fall alle) und kannst gleich alle Fehlerstellen in der Datei beheben statt eine zu beheben und beim nächsten Versuch die zweite Fehlermeldung zu bekommen etc.

    Kann ich an die besagte Stelle irgendwie rankommen? Vielleicht ist es möglich den Parser zur Laufzeit zu ändern?

    Da gibts viele Möglichkeiten. Es ist relativ einfach einen Functor zu bauen den du als semantic action an jeden Parser einer gewissen Granularität dranhängst und der einfach die Zeichenzahl im geparsten Teil mitnimmt und auf einen Zähler aufaddiert. Damit kannst du genau sagen bei welchem Zeichen in der gesamten Sequenz der Fehler auftritt.
    Alternativ (fast genauso einfach) kannst du einen Zeilenumbruch-Zähler einbauen und den Zeichenzähler jedesmal wieder auf 0 setzen, schon hast du ne Möglichkeit, Zeilen- und Spaltenzahl in deinen Fehlermeldungen anzugeben.



  • Vielen Dank für eure Unterstützung, insbesondere an "Dasd".

    Ich habe folgenden Vorschlag ausgearbeitet:

    #include <iostream>
    #include <boost/bind.hpp>
    #include <boost/spirit.hpp>
    #include <boost/spirit/dynamic/stored_rule.hpp>
    
    using namespace std;
    using namespace boost;
    using namespace boost::spirit;
    
    class ConfParser
    {
    public:
    	ConfParser(const std::string& f)
    	{
    		comment  = chlit<>('#') >> *(anychar_p - eol_p) >> eol_p;
    		space    = *(space_p | comment);
    
    		pe0Unit  = space >> strlit<>("pe0") >> space >> strlit<>("=") >> space;
    		pe0Value = int_p >> space;
    		oe0Unit  = space >> strlit<>("oe0") >> space >> strlit<>("=") >> space;
    		oe0Value = int_p >> space;
    
    		stored_rule<> r = repeat_p(2)
    				  [
    					(pe0Unit >> pe0Value[bind(&ConfParser::setPe0, this)])
    					|
    					!(oe0Unit >> oe0Value[bind(&ConfParser::setOe0, this)])
    				  ];
    
    		parse_info<> p = parse(f.c_str(), r);
    
    		if(p.full)
    			cout << "File is OK.\n";
    		else
    			cout << "File is not OK.\n";
    	}
    
    private:
    	void setPe0()
    	{
    		pe0Unit = ~anychar_p;
    	}
    	void setOe0()
    	{
    		oe0Unit = ~anychar_p;
    	}
    
    private:
    	stored_rule<> comment;
    	stored_rule<> space;
    	stored_rule<> pe0Unit;
    	stored_rule<> pe0Value;
    	stored_rule<> oe0Unit;
    	stored_rule<> oe0Value;
    };
    
    int main()
    {
    	string textfile = "###\npe0 = 35 ";
    	ConfParser cp(textfile);
    
    	return 0;
    }
    

    Zur Beschreibung des Progs:
    1. um externe Funktoren zu umgehen nutzt das Programm boost::bind (Zeile 25 u. 27)
    2. wird ein Eintrag gefunden wird seine rule auf "das Nichts" gesetzt (Zeile 41 u. 45)
    3. die komplette rule ist ab Zeile 23 zu finden

    Was noch zu tun ist:
    1. ein "echtes" textfile einlesen statt eines provisorischen string
    2. eine parse-error Behandlung mittles position_iterator
    3. die privaten set Methoden erweitern (speichern der gefundenen Werte)

    Gruß, Goran



  • Ich möchte nur nochmal darauf hinweisen, dass du das wiederholte Auftreten von "space" vermeiden kannst, indem du ihn stattdessen dem parse-Aufruf mitgibst.


Anmelden zum Antworten