Komplizierten String umwandeln und splitten



  • Hallo liebe Mitglieder.

    Ich stehe gerade vor einem kleinen Problem. Ich bekomme von einer Web-Anfrage eine Datei zurück bei der ich die wichtigen Inhalte in einen String speichere.

    Darunter ist z.B. soein String zu finden:

    string xy = "[['1.81592098644987','52.5487429714954'],['-1.81592290792183','52.5487234624632'],...]";
    

    Die länge dieses Strings ist je nach Anfrage verschieden groß.
    Bei den innerhalb der "[]" stehenden Zahlen handelt es sich um Koordinaten eines Punktes die ich wiederum extrahieren möchte und in eine double variable legen möchte.

    Meine Frage ist nun, wie schaffe ich es diesen String so zu splitten, dass ich z.B. mit einer Schleife jedesmal die beiden werte in eine double variable packe?

    Mein erster Ansatz war die Anzahl der Speziellen Zeichen durch ersetzen zu verringern, sodass ich zum Schluss nur noch beim "," splitte.

    String xy;
    for(auto iter = xy.begin(); iter != xy.end(); iter++){
    if(*iter == '[')
    *iter = ' ';
    if(*iter == ']')
    *iter = ' ';
    if(*iter = '\'')
    *iter = ' ';
    }
    

    Die Whitespaces würde ich dann in einem späteren Zug rausschmeißen und danach wie erwähnt nach "," splitten.
    Bestimmt haben nun einige von euch bemerkt dass das nicht klappen kann. Und zwar werden die "'" nicht durch Whitespace ersetzt.
    Gibt es da evtl. eine bessere Methode?

    PS: Den Rücklauf des Webdienstes kann ich nicht beeinflussen.


  • Mod

    Evtl. reicht es schon, überflüssige Zeichen direkt zu überlesen?
    http://ideone.com/zWsxfj

    #include <locale>
    #include <iostream>
    #include <sstream>
    #include <iomanip>
    
    struct skip_extra : std::ctype<char> {
        skip_extra()
            : std::ctype<char>( get_table() )
        {}
        static const mask* get_table() {
            static const std::array<mask, table_size> table = [&]() {
                std::array<mask, table_size> table;
                std::copy( classic_table(), classic_table() + table_size, table.begin() );
                table['"'] |= space;
                table['\''] |= space;
                table['['] |= space;
                table[']'] |= space;
                table[','] |= space;
                return table;
            }();
            return table.begin();
        }
    };
    
    int main() {
        std::istringstream s( "\"[['1.81592098644987','52.5487429714954'],['-1.81592290792183','52.5487234624632']]\"" );
        s.imbue( std::locale( s.getloc(), new skip_extra ) );
        double x;
        while ( s >> x ) {
            std::cout << std::setprecision( 15 ) << x << "\n";
        }
    }
    

    Unser Streams-Guru Werner Salomon hat aber bestimmt noch eine bessere Lösung.



  • Für sowas haben die (C)Götter sscanf erschaffen:

    void split(const char *s)
    {
    	int n = 0;
    	double d, e;
    	while (2 == sscanf(s += n, "%*[^']'%lf','%lf'%n", &d, &e, &n))
    	{
    		printf("%f %f\n", d, e);
    	}
    }
    
    int main()
    {
    	const char *s = "[['1.81592098644987','52.5487429714954'],['-1.81592290792183','52.5487234624632'],['-99.88','77.66'],['-0.55','44.33']]";
    	split(s);
    	return 0;
    }
    


  • Vielen dank @wutz und @camper!
    Super das ihr so schnell antworten konntet.
    Auf die Idee die einfach zu überlesen bin ich überhaupt nicht gekommen! Aber danke für diesen Tipp!

    Jetzt muss ich diese noch in eine Klasse reinschmeißen die Point heißt, genauer gesagt geht es um die OpenCV Point Klasse. Da es sich immer um Koordinatenpaare handelt muss ich also immer 2 in den Konstruktor schieben und dann bestenfalls eine Liste mit den Point Objekten anlegen. Da bin ich mir noch nicht sicher wie ich das erreichen soll.

    Meine Idee war nun mit einer Schleife die Zahlen in die Klasse zu geben, aber wie ich das sehe geht das immer nur einer nach dem anderen?
    Und die Points in einen vector zu geben mach ich dann wohl am besten mit push_back?

    Sinn&Zweck:
    Es handelt sich bei dem String um ein Polygon, dass in ein Bild eingezeichnet werden soll, die Methoden zum einzeichnen und transformieren sind bereits fertig
    das obige Problem ist das letzte womit ich mich rumschlage.

    PS: @camper geht das auch ohne new bei dem Struct? hab ein paar schlechte Erfahrungen mit dem Heap und würde sowas gerne umgehen.
    Außerdem hatte ich mal versucht dein beispiel bei mir laufen zu lassen, aber es scheint als hätte meine IDE mehrere Probleme mit diesem Code vorallem mit der variable "table". Hab dein Beispiel eigentlich nur kopiert.
    Kann es sein das das bei Visual Studio nicht funktioniert?

    Edit:
    Ich denke mit wutz Version kann ich erstmal mehr anfangen da sie von meiner IDE ohne Fehler angenommen wird



  • Die Variable table ist vom Typ std::array, den gibts erst ab C++11. Vielleicht hast Du einen alten Compiler, vielleicht musst Du Deinem Compiler aber auch nur mitteilen, dass er C++11 - Code bekommt.



  • Kannes sein, dass dein ankommendes Format zufällig JSON ist?
    Würde sich dann nicht vielleicht sogar ein (Lightweight) JSON Parser anbieten?



  • Das wird bestimmt ein Parser für einen Pokemon GO Bot :p



  • @Belli ich meine eigentlich den c++14 eingestellt zu haben ich überprüfe das gleich mal

    @Skym0sh0 ich sags mal so JSON wäre möglich, bin aber vom Auftrag her an XML geknüpft.

    @KN4CK3R 😃 dieses Spiel hat alles verändert, aber nein es handelt sich um einen Nominatim Geocoder der auch Vektorprimitiven in eine Tilemap einbaut. Dennoch witziger Gedanke.

    Wutz'S Version klappt auf jedenfall jnd ich werde dabei bleiben. Ich denke mit dem Rest komme ich ab jetzt auch zurecht. Statt das nun zu verkomplizieren mache ich das so an der Stelle wo d und e ausgegeben werden, dass die Vektoren gezeichnet werden Code folgt noch.

    EDIT:
    Ich bin noch nicht so ganz bewandert mit sscanf() Daher wollte ich noch kurz die Frage einwerfen, wie ich das Format so erweitere, dass ich die ersten beiden Koordinatenpaare bekomme, also statt nur 2 Werte pro loop, 4 Werte pro loop.



  • Also ich bin schon davon ausgegangen, dass du weißt, wie man einen vector benutzt und es dir nur noch darum geht, den String zu interpretieren.
    http://ideone.com/PX75aW

    typedef struct {
    	double x1,y1,x2,y2; } Point;
    
    void readvals(const char*s,std::vector<Point>&v)
    {
        int n = 0;
        Point p;
        while (2 == sscanf(s += n, "%*[^']'%lf','%lf'%n", &p.x1, &p.y1, &n)
        		&&
        	   2 == sscanf(s += n, "%*[^']'%lf','%lf'%n", &p.x2, &p.y2, &n)
        	)
           v.push_back(p);
    }
    
    int main()
    {
      std::vector<Point> v;
      std::string s="[['1.81592098644987','52.5487429714954'],['-1.81592290792183','52.5487234624632'],['-99.88','77.66'],['-0.55','44.33']]";
      readvals(s.c_str(),v);
      for(auto const &p: v) std::cout << p.x1 << p.y1 << p.x2 << p.y2 << '\n';
      return 0;
    }
    


  • Vielen dank für die Lösung Wutz!
    Das hilft mir sehr viel weiter.

    Mit vectoren bin ich noch nicht so vertraut bin relativ neu in der c++ Welt.

    Ich hätte noch eine kleine Frage, beim Iterieren mittels Range, ist es dort auch möglich das folgende Element im vector anzusprechen? (so wie z.b. v[i] und v[i+1])

    Und sofern es sich um ein geschlossenes Polygon handelt bräuchte ich auch den Fall, dass wenn ich beim letzten Element bin, dieses mit dem ersten verbinde.
    Geht das mit Range? Oder muss ich da den herkömmlichen Iterator verwenden?



  • camper schrieb:

    Unser Streams-Guru Werner Salomon hat aber bestimmt noch eine bessere Lösung.

    nein - besser nicht, nur anders. Aber Danke für die Vorschusslorbeeren.

    Zunächst, wenn schon eine Datei lesen, dann gar nicht erst aus der Datei in den String, vom String in viele double und aus den double dann Point-Objekte machen, sondern doch besser gleich von Datei in Point (letzteres zumindest aus Sicht der Applikation).

    Als kleines Helferlein sei hier das schon oft erwähnte Char-Template empfohlen, welches ein ganz bestimmtes Zeichen 'C' überliest:

    template< char C >
    std::istream& Char( std::istream& in )
    {
        char c;
        if( in >> c && c != C )
            in.setstate( std::ios_base::failbit );
        return in;
    }
    

    jetzt noch eine Funktion, die das Point-Objekt liest:

    // -- Point lesen; Format ['x','y']
    template< typename T >
    std::istream& operator>>( std::istream& in, Point_<T>& pnt )
    {
        T x, y;
        if( in >> Char<'['> >> Char<'\''> >> x >> Char<'\''> >> Char<','> >> Char<'\''> >> y >> Char<'\''> >> Char<']'> ) 
            pnt = Point_<T>( x, y );
        return in;
    }
    

    .. natürlich gleich mit dem Format aus der Datei.

    Dann noch ein wenig Gelee-Code drum herum:

    #include <iostream>
    #include <vector>
    #include <fstream>
    // #include <point_from_OpenCV>
    // Char<> s.o.
    // Point-Lese-Funktion (s.o.)
    
    int main()
    {
        using namespace std;
        vector< Point_<double> > pnts;  // hier sollen die OpenCV-Point_'s rein
        ifstream file("input.txt");
        if( !file.is_open() )
        {
            cerr << "Fehler beim Oeffnen der Datei" << endl;
            return 0;
        }
        file >> Char<'['>; // führende [ überlesen
        auto delimiter = char(',');
        for( auto pnt = Point_<double>(); file && delimiter == ',' && file >> pnt; file >> delimiter )
            pnts.push_back( pnt );
        if( file && delimiter == ']' )
        {
            cout << "alles gut!" << endl;
            cout << pnts.size() << "Punkte gelesen" << endl;
        }
        return 0;
    }
    

    .. und fertig

    @vinte: nichts gegen Wutz und C. Aber wenn Du C++ programmieren möchtest, solltest Du die Finger von scanf lassen. Meine Version sollte auch mit Deinem Compiler machbar sein, wenn dieser nicht allzu antik ist; tausche dazu das 'auto' in Zeile 19 gegen ein 'char' und das 'auto' in Zeile 20 gegen 'Point_<double>' aus.

    vinte schrieb:

    Mit vectoren bin ich noch nicht so vertraut bin relativ neu in der c++ Welt.

    Ich hätte noch eine kleine Frage, beim Iterieren mittels Range, ist es dort auch möglich das folgende Element im vector anzusprechen? (so wie z.b. v[i] und v[i+1])

    Und sofern es sich um ein geschlossenes Polygon handelt bräuchte ich auch den Fall, dass wenn ich beim letzten Element bin, dieses mit dem ersten verbinde.
    Geht das mit Range? Oder muss ich da den herkömmlichen Iterator verwenden?

    Besser Du arbeitest mit Iteratoren. Ein kleiner Trick: kopiere den ersten Punkt einfach an das Ende der Vektors .. also nach dem Einlesen:

    pnts.push_back( pnts.front() );
    

    anschließend einfach per Iterator oder Index auf die benachbarten Elemente zugreifen.

    Gruß
    Werner

    PS.: für die Fortgeschrittenen unter uns: geht mit C++11 auch ein Char<"['"> .. ohne sich dabei die (Programmier-)Finger zu brechen?



  • Werner Salomon schrieb:

    Zunächst, wenn schon eine Datei lesen, dann gar nicht erst aus der Datei in den String, vom String in viele double und aus den double dann Point-Objekte machen,

    Das ist zweifellos richtig, aber dann wird die C-Variante sogar noch einfacher, da man statt sscanf gleich fscanf und dessen Stream-Orientiertheit ausnutzen kann.



  • vinte schrieb:

    Ich bekomme von einer Web-Anfrage eine Datei zurück bei der ich die wichtigen Inhalte in einen String speichere.

    Und da liegt schon der Haken. Aus dem Webservice in eine Datei und daraus wieder in einen Speicherbereich?
    Sowas ist Bastlerniveau, im Webservice-Consumer gleich den Datenstrom (der hier natürlich sowieso schon im Speicher vorliegt) direkt und ohne Umweg über eine Dateiablage interpretieren, im Hauptspeicher die Points ablegen und entsprechend weitergeben, ist angesagt.
    Und dann kommt dann doch wieder die Stringvariante ins Spiel.

    Paarweise kannst du deinen Vector auch einfach durchlaufen:
    http://ideone.com/EsSqkU

    typedef struct {
    	double x1,y1,x2,y2; } Point;
    
    void readvals(const char*s,std::vector<Point>&v)
    {
        int n = 0;
        Point p;
        while (2 == sscanf(s += n, "%*[^']'%lf','%lf'%n", &p.x1, &p.y1, &n)
        		&&
        	   2 == sscanf(s += n, "%*[^']'%lf','%lf'%n", &p.x2, &p.y2, &n)
        	)
           v.push_back(p);
    }
    
    int main()
    {
      std::vector<Point> v;
      std::string s="[['1','2'],['3','4'],['5','6'],['7','8'],['9','10'],['10','11'],['12','13'],['14','15']]";
      readvals(s.c_str(),v);
      for(int i=0;i<v.size()-1;++i) 
      std::cout << v[i].x1 << v[i].y1 << v[i].x2 << v[i].y2 << " " 
                << v[i+1].x1 << v[i+1].y1 << v[i+1].x2 << v[i+1].y2 << '\n';
      return 0;
    }
    

  • Mod

    @vinte: nichts gegen Wutz und C. Aber wenn Du C++ programmieren möchtest, solltest Du die Finger von scanf lassen.

    Ich widerspreche mal. Nichts gegen deine Lösung; aber Wutz' ist IMO die Beste. Das wir dabei mit Typ-unsicheren Funktionen arbeiten ist doof, aber verkraftbar, weil der Code so trotzdem einfacher und performanter wird - schau dir einfach mal den extraction operator für Point_ an, das ist doch nicht lesbar. Und das wir durch einen String bufferen ist egal, weil der Kopierschritt performancetechnisch insignifikant zum Parsen ist.

    Wen trotzdem die Typunsicherheit stört, der kann einfach einen statischen Wrapper basteln (oder mich fragen :D), der den Formatstring zur Compilezeit gegen die Template-Argumente prüft.


Anmelden zum Antworten