Dump-sicheres Parsen



  • Eisflamme schrieb:

    Zu einem bestimmten Zeitpunkt in meinem Parsevorgang suche ich nur nach Zeilen, die mit "Beat" anfangen. Kommt keines vor, würde ich gerne den Rest der Zeile ignorieren (da hätte ich jetzt so was wie stream >> firstWord; if(firstWord != Beat) getline(stream, &dummy); gemacht)

    Könnte damit gehen:

    std::istream& search_beat( std::istream& in )
    {
        for( std::string token; in >> token && token != "Beat"; )
            in.ignore( std::numeric_limits< std::streamsize >::max(), '\n' );
        return in;
    }
    

    setzt aber voraus, dass sich hinter "Beat" garantiert ein Leerzeichen befindet. Falls nicht, macht man sich std::setw zu nutze.

    #include <istream>
    #include <limits>
    #include <string>
    #include <iomanip> // std::setw
    
    std::istream& search_beat( std::istream& in )
    {
        for( std::string token; in >> std::setw(4) >> token && token != "Beat"; )
            in.ignore( std::numeric_limits< std::streamsize >::max(), '\n' );
        return in;
    }
    

    Und falls es nicht immer "Beat" ist, kann man das Suchwort auch mitgeben.

    // includes wie oben
    struct search_text_at_beginline
    {
        explicit search_text_at_beginline( const std::string& key )
            : key_( key )
        {}
        friend std::istream& operator>>( std::istream& in, search_text_at_beginline st )
        {
            for( std::string token; in >> std::setw(st.key_.length()) >> token && token != st.key_; )
                in.ignore( std::numeric_limits< std::streamsize >::max(), '\n' );
            return in;
        }
    private:
        std::string key_;
    };
    

    Anwendung

    cin >> search_text_at_beginline("Beat")
    

    Eisflamme schrieb:

    Dann möchte ich gerne in der aktuellen Zeile so viele Zeichen überspringen, bis das Zeichen ':' kommt. Der zu überspringende Teil soll auch Leerzeichen enthalten. Kommt jedoch kein ':' in der aktuellen Zeile vor, möchte ich das auch gerne irgendwie erfahren.

    siehe Posting oben mit skip_to_char_in_line.

    Gruß
    Werner



  • Okay, das sieht super aus, herzlichen Dank. 🙂



  • Kann mir jemand erklären, warum Meep Meep skip_char als Template-Funktion deklariert hat? Was spricht dagegen den Buchstaben 'C' nicht einfach als Argument zu übergeben?



  • Qute schrieb:

    Kann mir jemand erklären, warum Meep Meep skip_char als Template-Funktion deklariert hat? Was spricht dagegen den Buchstaben 'C' nicht einfach als Argument zu übergeben?

    weil die Syntax eines Manipulators ohne Parameter vom Standard vorgegeben ist und keine weiteren Parameter außer std::istream& erlaubt. Gibt es eine Funktion

    std::istream& egal( std::istream& in );
    

    so kann diese Funktion mit operator>> an einen istream übergeben werden. Z.B.:

    std::cin >> egal; // ruft die Funktion egal mit std::cin als Parameter auf
    

    Möchte man Parameter übergeben muss man sich eine eigene struct oder class nebst eigenen inserter/extractor (also hier operator>>) bauen. So wie ich in meinem Posting oben - search_beat ist ohne Parameter; mit fest codiertem "Beat" und bei search_text_at_beginline kann man den Suchtext als Parameter mitgeben.

    Gruß
    Werner



  • Ah, auch das ist eine super Erkenntnis!

    Was, wenn ich mehrere Zeichen alternativ lesen möchte und rausfinden möchte, welche gelesen wurde? Ich brauche dann ja einen return type, der mir auch sagt, dass nichts gefunden wurde.

    Ich habe mir jetzt so was gebastelt, aber ich bin unsicher, welcher Wert Sinn macht, wenn es eben char ist. Ich könnte auch std::istream::int_type nehmen, dann müsste ich es aber jedes Mal konvertieren.

    template<char C1, char C2, char C3>
    	char skipToCharInLine(std::istream& in)
    	{
    		typedef std::istream::traits_type Traits;
    		for(std::istream::int_type m; (m = in.get()) != Traits::eof();)
    		{
    			const char c = Traits::to_char_type(m);
    			if(c == C1)
    				return C1;
    			if(c == C2)
    				return C2;
    			if(c == C3)
    				return C3;
    			if(c == '\n')
    				return '\n';
    		}
    		return 0; // eof
    	}
    

    Ist das so ok? Sonst fiele mir noch ein std::pair<bool, char> ein.

    Ach und eigentlich brauchen wir hier doch gar keine Templates, oder? Das ist ja kein Manipulator!

    Edit:
    Sorry, ich bin ein Nimmersatt und hier die Infos zu erfragen ist einfach so viel komfortabler als irgendwann wieder Code zu zeigen, den dann alle zerreißen. 🙂

    Ich möchte gerne auch chars skippen, die erst zur Laufzeit bekannt sind. Dann nehme ich so eine struct. Ich hätte aber gerne beides. Wie bezeichnet ihr das dann? Beides als skipChar geht ja nicht, aber mir fallen keine vernünftigen Bezeichner ein, außer so was wie: skipFixedChar und skipChar oder so, aber na ja



  • viellicht solltest du dir so langsam parser frameworks ansehen. boost::spirit oder boost::regex, je nachdem was du brauchst. Stringstreams sind für relativ einfach Datenformate geschrieben, nicht für komplexe Parseraufgaben.



  • Ja, das verstehe ich. Mein Datenformat ist aber gar nicht so komplex, wie man hier vermuten könnte. Ich hatte das ja in relativ kurzer Zeit schon mit stream + string gelöst. Ich will es nur noch auf Streams umbauen und dumpsicher machen, dann bin ich froh und glücklich.

    Zudem ist es eine großartige Übung. Selbst wenn man hier eleganter mit boost arbeiten könnte, so brauche ich die Streams doch ständig. Und ich verwende stets so ein String-Gefrickel. Das will ich mir jetzt ein für alle Mal abgewöhnen. Was ist dafür besser als eine recht komplexe Übung, die man am besten gar nicht erst mit Streams anfassen würde? 🙂

    Edit:
    Wenn ich zu viel frage, sagt mir das. Jetzt habe ich eine Zeile, in der ich prüfen will, ob sie mit einem Wort beginnt. Tut sie das nicht, möchte ich sie aber unversehrt lassen, d.h. das gelesene Wort nicht extrahieren. Ich muss also entweder einen ganzen String peeken (geht wohl nicht?), oder ich muss wieder etwas zurückstecken. Ich fand unget(), aber kann ich das beliebig häufig aufrufen? Dann müsste ja der Stream alles Vergangene noch beinhalten, löscht der das nicht?

    Folgender Code geht, aber das muss doch einfacher gehen?

    bool stringInLine(istream& in, const string& stringToFind)
    	{
    		typedef istream::traits_type Traits;
    		int charsExtracted = 0;
    
    		bool result = false;
    
    		for(Traits::int_type i; (i = in.get()) != Traits::eof();)
    		{
    			++charsExtracted;
    
    			char c = Traits::to_char_type(i);
    			if(c == '\n')
    				break;
    
    			const int charsExtractedBefore = charsExtracted;
    
    			if(c == stringToFind[0]) // first char fits!
    			{
    				string::const_iterator it;
    				for(it = stringToFind.begin() + 1; it != stringToFind.end(); ++it)
    				{
    					if((i = in.get()) == Traits::eof())
    						break;
    
    					++charsExtracted;
    
    					c = Traits::to_char_type(i);
    
    					if(*it != c)
    						break; // not correct word
    				}
    
    				if(it == stringToFind.end())
    				{
    					result = true;
    					break;
    				}
    
    				for(; charsExtracted > charsExtractedBefore; --charsExtracted)
    					in.unget();
    			}
    		}
    
    		for(; charsExtracted > 0; --charsExtracted)
    			in.unget();
    
    		return result;
    	}
    


  • Eisflamme schrieb:

    Wenn ich zu viel frage, sagt mir das.

    Hallo Eisflamme,

    frag' ruhig. Im schlimmsten Fall antwortet keiner mehr. Bei mir kann es auch mal vorkommen, dass ich erst zwei Tage später antworte.

    Eisflamme schrieb:

    Jetzt habe ich eine Zeile, in der ich prüfen will, ob sie mit einem Wort beginnt. Tut sie das nicht, möchte ich sie aber unversehrt lassen, d.h. das gelesene Wort nicht extrahieren.

    da kommen wir dann an die Grenzen des Streams. Garantiert ist nur, dass Du genau ein Zeichen weit nach vorn 'sehen' kannst.

    Eisflamme schrieb:

    Ich muss also entweder einen ganzen String peeken (geht wohl nicht?), oder ich muss wieder etwas zurückstecken. Ich fand unget(), aber kann ich das beliebig häufig aufrufen?

    Nein - unter Umständen kannst Du es nicht mal einmal aufrufen. Bei std::cin geht sicher eine Zeile und bei Dateien vielleicht die ganze Datei, aber zugesichert wird das nicht.

    Der Stream geht nach 'bad', wenn Du ungetc öfter aufrufst als der darunter liegende Streambuf es verträgt. Stell' dir vor es wäre eine RS232-Schnittstelle, die kann auf ungetc das gerade empfangene Zeichen nicht einfach zurück schicken.

    Das ist im Allgemeinen aber auch gar nicht nötig. Wenn die Zeichenfolge mit dem Schlüssel passt, darf sie auch überlesen werden und was soll denn passieren, wenn sie nicht passt? Wahrscheinlich willst Du dann auf eine andere Zeichenfolge prüfen oder?
    Stattdessen könntest Du auf mehrere Zeichenfolgen gleichzeitig prüfen - wäre das eine Lösung? Dazu muss man wieder nur ein Zeichen weit nach vorne schauen.

    Gruß
    Werner



  • Okay, danke fürs weitere Antworten 🙂

    Auf mehrere gleichzeitig, hmm... das könnte klappen.

    Es ist halt so: Entweder, es gibt ein gewisses Schlüsselwort in der Zeile, dann ist nur noch der Zeilenrest relevant, genau. Falls nicht, dann muss das erste Wort der Zeile extrahiert werden. Geht so was noch elegant oder sprengt das die Grenzen des "Schön Machbaren". Wobei ein getline in einen string auch nicht schön ist, wirkt nur irgendwie zweckmäßiger. Dann jedoch würde ich vermutlich einen zweiten Stream nutzen und den auf den string hetzen, nun ja...

    Und ist unget() für istream in derselben Zeile nun zugesichert oder nicht? Ich habe es jetzt so implementiert und es funktioniert, aber wenn das nicht sicher ist, begebe ich mich ja in Teufels Küche. Ich meine, der zu Grunde liegende string (keine Benutzereingabe und auch keine Datei) existiert ja noch komplett. Iteriert istream nicht einfach über irgendeine Zeiger/Iteratorkapselung durch den string?



  • Eisflamme schrieb:

    Was, wenn ich mehrere Zeichen alternativ lesen möchte und rausfinden möchte, welche gelesen wurde? Ich brauche dann ja einen return type, der mir auch sagt, dass nichts gefunden wurde.
    ...
    Sorry, ich bin ein Nimmersatt und hier die Infos zu erfragen ist einfach so viel komfortabler als irgendwann wieder Code zu zeigen, den dann alle zerreißen. 🙂

    Hi Eisflamme,

    stimmt das ist viel einfacher, aber weil heute irgendwie "Tag des Streams" ist, muss ich noch was loswerden.

    Werner Salomon schrieb:

    template< char C >
    bool skip_to_char_in_line( std::istream& in )
    {
        typedef std::istream::traits_type Traits;
        for( std::istream::int_type m; (m = in.get()) != Traits::eof(); )
        {
            const char c = Traits::to_char_type( m );
            if( c == C )
                return true;
            if( c == '\n' )
                break;
        }
        return false;
    }
    

    Der Code, den ich Dir ursprünglich posten wollte, sah völlig anders aus. Erst nach Meep Meeps Beitrag von 23:41:00 02.10.2012 bin ich auf diese Lösung umgeschwenkt.

    Wenn sich jetzt einer von den Performance-Junkies ranmacht und diesen Code einem Benchmark unterzieht, wird er wahrscheinlich feststellen, dass der ziemlich lahm ist. Das liegt daran, dass in einer Schleife Zeichen für Zeichen die relativ teure Methode 'istrem::get()' gerufen wird. Die ist deshalb relativ teuer, weil u.a. jedes mal so ein sentry-Objekt angelegt wird und überprüft wird, ob noch alle iO ist. Das habe ich Dir schon weiter oben erklärt (mein Beitrag 19:58:05 02.10.2012).

    deshalb lohnt es sich in solchen Fällen eine Stufe tiefer auf die streambuf-Ebene abzusteigen. Der streambuf stellt für die Zeichen-für-Zeichen-Analyse drei Methoden zur Verfügung.
    sgetc - liefert das aktuelle Zeichen, ohne Änderung des Lesezeigers
    snextc - schiebt den Lesezeiger um 1 nach vorn und liefert das dort gefunden Zeichen
    sbumpc - liefert das aktuelle Zeichen und schiebt danach den lesezeiger um 1 nach vorn
    Alle drei Methoden liefern einen int_type dessen Wert EOF sein kann und - falls nicht - erst in einen char konvertiert werden muss. Diese Konvertierung ist aber billig.

    Wenn man direkt auf den streambuf zugreift, muss man allerdings einiges selber machen. Dazu gehört zum einen das Setzen der Flags (fail, eof, bad) und das Abfangen von Exceptions.

    Dazu ein Beispiel, was Du oben angesprochen hast. Angenommen Du hast folgende Funktion zu implementieren:

    char search_in_line( std::istream& in, const std::string& chars );
    

    Diese Funktion soll alle Zeichen überlesen, bis sie entweder auf eines der Zeichen in 'chars' trift oder auf '\n' (EOL) oder eben auf EOF. Findet die Datei eines der Zeichen gibt sie es zurück in jedem anderen Fall ein char(0).

    Wie implementiert man das mit Zugriff aus den streambuf. Der Rahmen sieht immer so aus:

    char search_in_line( std::istream& in, const std::string& chars )
    {
        char result = char(0);
        std::istream::sentry ok( in, true ); // false := skip white space character
        if( ok )
        {
            // ... hier geht's weiter
        }
        return result;
    }
    

    Wie schon mehrfach erwähnt ist erst ein sentry-Objekt zu erzeugen. Das stellt sicher, dass der Stream noch good ist, und würde wir hier als 2.Paramter ein 'false' oder nichts (false ist default) übergeben, so würde das sentry-Objekt auch gleich alle White Space Character überlesen. Wichtig ist, dass im Ok-Fall die Methode istream::rdbuf() ein gültiges Streambuf-Objekt zurück liefert. Und das brauchen wir später.

    Vorher muss man sich aber noch um den Zustand kümmern.

    char search_in_line( std::istream& in, const std::string& chars )
    {
        char result = char(0);
        std::istream::sentry ok( in, true );
        if( ok )
        {
            std::ios_base::iostate state = std::ios_base::goodbit;
            try
            {
                // ... hier geht's weiter
            }
            catch(...)
            {
                state |= std::ios_base::badbit;
                if( in.exceptions() & std::ios_base::badbit )
                    throw;
            }
            in.setstate( state );
        }
        return result;
    }
    

    Für den Zustand des Streams wird am Anfang ein state auf good gesetzt. Kommt man jetzt an einen Punkt, bei dem eine Zustandsänderung notwendig wird, so wird nicht unmittelbar der Zustand des Streams geändert (istream::setstate) sondern nur die lokale Variable state verändert. Der Grund dafür liegt darin, dass mit jedem Setzen des Zustands eine Exception ausgelöst werden kann (s. ios_base::exceptions). Wirft der unterliegende konkrete Streambuf eine Exception, wrd diese zunächst gefangen und nur weitergegeben, wenn das badbit gesetzt ist. Ansonsten wird das badbit gesetzt - danach ist der streambuf unbrauchbar.
    Bis hier sind alle diese Lesefunktionen irgendwie gleich; z.B.: operator>>string müsste genauso aussehen.

    Und nach dem ganze Vorgeplänkel kommen wir jetzt zum eigentlichen Lesen. Da jedes Zeichen hier auf jeden Fall gelesen wird und kein gelesenes Zeichen später noch benötigt wird, reicht hier die Methode streambuf::sbumpc() (s.o.).

    char search_in_line( std::istream& in, const std::string& chars )
    {
        char result = char(0);
        std::istream::sentry ok( in, true );
        if( ok )
        {
            std::ios_base::iostate state = std::ios_base::goodbit;
            try
            {
                typedef std::istream::traits_type Traits;
                for(;;)
                {
                    const std::istream::int_type m = in.rdbuf()->sbumpc();
                    if( Traits::eq_int_type( m, Traits::eof() ) )
                    {
                        state |= std::ios_base::eofbit;
                        break;
                    }
                    const char c = Traits::to_char_type( m );
                    if( chars.find( c ) != std::string::npos )
                    {
                        result = c;
                        break;
                    }
                    if( c == '\n' )
                        break;
                }
            }
            catch(...)
            {
                state |= std::ios_base::badbit;
                if( in.exceptions() & std::ios_base::badbit )
                    throw;
            }
            in.setstate( state );
        }
        return result;
    }
    

    Das Lesen geschieht in Zeile 13. Zunächst wird auf EOF geprüft und auf EOF die Zustandsvariable verändert und die Schleife unterbrochen.
    Danach kann dann sicher nach char konvertiert werden (char_traits::to_char_type) und dann werden die einzelnen Checks durchgeführt.
    Fertig ist die Funktion.

    Eisflamme schrieb:

    Ich möchte gerne auch chars skippen, die erst zur Laufzeit bekannt sind. Dann nehme ich so eine struct. Ich hätte aber gerne beides. Wie bezeichnet ihr das dann? Beides als skipChar geht ja nicht, aber mir fallen keine vernünftigen Bezeichner ein, außer so was wie: skipFixedChar und skipChar oder so, aber na ja

    Vielleicht skip, search oder ignore mit entsprechenden Anhängen.

    Gruß
    Werner



  • Eisflamme schrieb:

    Es ist halt so: Entweder, es gibt ein gewisses Schlüsselwort in der Zeile, dann ist nur noch der Zeilenrest relevant, genau. Falls nicht, dann muss das erste Wort der Zeile extrahiert werden.

    Was spricht dann gegen ein einfaches

    string erstesWort;
        if( datei >> erstesWort ) {
            if( erstesWort == dies ) // oder 'das' oder was anderes ...
    

    Eisflamme schrieb:

    Und ist unget() für istream in derselben Zeile nun zugesichert oder nicht? Ich habe es jetzt so implementiert und es funktioniert, aber wenn das nicht sicher ist, begebe ich mich ja in Teufels Küche.

    Probiere es auf dem Zielsystem(en) aus. Wenn es funktioniert, so funktioniert es.

    Eisflamme schrieb:

    Ich meine, der zu Grunde liegende string (keine Benutzereingabe und auch keine Datei) existiert ja noch komplett. Iteriert istream nicht einfach über irgendeine Zeiger/Iteratorkapselung durch den string?

    Es muss unter einem stream oder streambuf keinen string oder Zeichenbuffer geben. Wie die interne Implementierung aussieht ist völlig frei. Die Vorstellung, dass ein Lesezeiger über ein Memory mit char iteriert ist zwar hilfreich für das Verständnis, aber streng genommen gilt das nur für genau das eine Zeichen auf den der Lesezeiger gerade zeigt. Sicher werden irgendwo Blöcke von Zeichen liegen, aber die Blockgrenzen können aus Gründen, die nach außen völlig absurd erscheinen auch sonst wo sein.

    Stell Dir mal vor das darunter liegende Programm ist vor zig Jahren in C geschrieben worden mit Blöcken von 40 Zeichen, die in einer verketteten Liste liegen und der Programmierer hatte keine Lust/Zeit einen ungetc über eine Blockgrenze zu realisieren. Und schon funktioniert es fast(!) immer, bis auf diesen blöden Fall wo der ungetc vom 41. auf das 40. Zeichen erfolgt. Da ist alles möglich.

    Gute Nacht
    Werner



  • Wow, vielen Dank für deinen riesigen Input. Verstanden habe ich alles, Du hast es ja wie üblich auch wieder hervorragend erklärt. Ich habe mir diesen Thread jetzt in den Favoriten abgelegt und werde mir das alles noch genauer anschauen als ich es jetzt tue. Spätestens schaue ich mir das ganz genau an, wenn das nächste Mal was mit Streams nutze, vermutlich mehrfach. (aber jetzt schaue ich mir das ebenfalls an, nicht dass jetzt ein falscher Eindruck entsteht :)).

    Probiere es auf dem Zielsystem(en) aus. Wenn es funktioniert, so funktioniert es.

    Das klingt gewagt. Ist es denn "sicher", wenn ich es kompiliert habe und mit meinen Beispielen ausprobiert? Wenn da irgendwo undefiniertes Verhalten rumflattert, dann kann es ja jederzeit explodieren.



  • Eisflamme schrieb:

    Okay, aber prüft ihr bei jedem get, bei jedem getline und bei jedem Stringparsen, ob auch an dieser Stelle das Format korrekt ist? Oder lasst ihr das Programm auch einfach Mal crashen, wenn der einzige Inputfehler sein könnte, dass der Benutzer absichtlich manipuliert hat?

    Absichtlich craschen lassen ist in manchen Fällen OK, nämlich dann wenn es überhaupt keine Wichtigkeit hat dass das Programm an der Stelle "besser" reagiert, und es ein "sicherer" crash ist.
    Also ein Crash mit garantiertem Verhalten.
    Also in C++ wohl nur etwas wo eine C++ Exception fliegt -- die dann ggf. nicht (bzw. erst von der Runtime) gefangen wird.

    Also Zugriff über vector::at mit "falschem" Index ist z.B. OK.
    Zugriff über vector::operator[] ist nicht OK, Zugriff über Zeiger die Null sein könnten etc. auch nicht.


Anmelden zum Antworten