Std :: ifstream für LF, CR und CRLF?

84

Speziell interessiert mich istream& getline ( istream& is, string& str );. Gibt es eine Option für den ifstream-Konstruktor, um ihn anzuweisen, alle Newline-Codierungen unter der Haube in '\ n' zu konvertieren? Ich möchte in der Lage sein, getlinealle Zeilenenden anzurufen und ordnungsgemäß zu behandeln.

Update : Zur Verdeutlichung möchte ich in der Lage sein, Code zu schreiben, der fast überall kompiliert wird und Eingaben von fast überall entgegennimmt. Einschließlich der seltenen Dateien mit '\ r' ohne '\ n'. Minimierung von Unannehmlichkeiten für Benutzer der Software.

Es ist einfach, das Problem zu umgehen, aber ich bin immer noch gespannt, wie man im Standard alle Textdateiformate flexibel handhaben kann.

getlineLiest eine vollständige Zeile bis zu einem '\ n' in eine Zeichenfolge ein. Das '\ n' wird aus dem Stream verbraucht, aber getline nimmt es nicht in die Zeichenfolge auf. Das ist bisher in Ordnung, aber es könnte ein '\ r' kurz vor dem '\ n' geben, das in die Zeichenfolge aufgenommen wird.

In Textdateien gibt es drei Arten von Zeilenenden : '\ n' ist die herkömmliche Endung auf Unix-Computern, '\ r' wurde (glaube ich) auf alten Mac-Betriebssystemen verwendet, und Windows verwendet ein Paar, '\ r'. gefolgt von '\ n'.

Das Problem ist, dass getlinedas '\ r' am Ende der Zeichenfolge verbleibt.

ifstream f("a_text_file_of_unknown_origin");
string line;
getline(f, line);
if(!f.fail()) { // a non-empty line was read
   // BUT, there might be an '\r' at the end now.
}

Bearbeiten Vielen Dank an Neil für den Hinweis, dass f.good()ich das nicht wollte. !f.fail()ist was ich will.

Ich kann es manuell selbst entfernen (siehe Bearbeiten dieser Frage), was für die Windows-Textdateien einfach ist. Aber ich mache mir Sorgen, dass jemand eine Datei einspeist, die nur '\ r' enthält. In diesem Fall gehe ich davon aus, dass getline die gesamte Datei verbraucht, da es sich um eine einzelne Zeile handelt!

.. und das berücksichtigt nicht einmal Unicode :-)

.. Vielleicht hat Boost eine gute Möglichkeit, jeweils eine Zeile aus einem beliebigen Textdateityp zu verwenden?

Bearbeiten Ich verwende dies, um mit den Windows-Dateien umzugehen, aber ich habe immer noch das Gefühl, ich sollte es nicht müssen! Und dies wird nicht für die '\ r'-only-Dateien verzweigen.

if(!line.empty() && *line.rbegin() == '\r') {
    line.erase( line.length()-1, 1);
}
Aaron McDaid
quelle
2
\ n bedeutet neue Zeile, wie auch immer sie im aktuellen Betriebssystem dargestellt wird. Die Bibliothek kümmert sich darum. Damit dies funktioniert, sollte ein in Windows kompiliertes Programm Textdateien aus Windows lesen, ein in Unix kompiliertes Programm, Textdateien aus Unix usw.
George Kastrinis
1
@George, obwohl ich auf einem Linux-Computer kompiliere, verwende ich manchmal Textdateien, die ursprünglich von einem Windows-Computer stammen. Möglicherweise veröffentliche ich meine Software (ein kleines Tool für die Netzwerkanalyse) und möchte den Benutzern mitteilen, dass sie fast jederzeit (ASCII-ähnliche) Textdateien eingeben können.
Aaron McDaid
3
Kleiner Testfall, der Ihr Problem demonstriert .
Leichtigkeitsrennen im Orbit
1
Beachten Sie, dass wenn (f.good ()) nicht das tut, was Sie zu glauben scheinen.
1
@JonathanMee: Es mag wie waren diese . Vielleicht.
Leichtigkeitsrennen im Orbit

Antworten:

111

Wie Neil betonte, "sollte die C ++ - Laufzeit korrekt mit der Konvention zum Beenden von Zeilen für Ihre spezielle Plattform umgehen."

Menschen verschieben jedoch Textdateien zwischen verschiedenen Plattformen, sodass dies nicht gut genug ist. Hier ist eine Funktion, die alle drei Zeilenenden ("\ r", "\ n" und "\ r \ n") behandelt:

std::istream& safeGetline(std::istream& is, std::string& t)
{
    t.clear();

    // The characters in the stream are read one-by-one using a std::streambuf.
    // That is faster than reading them one-by-one using the std::istream.
    // Code that uses streambuf this way must be guarded by a sentry object.
    // The sentry object performs various tasks,
    // such as thread synchronization and updating the stream state.

    std::istream::sentry se(is, true);
    std::streambuf* sb = is.rdbuf();

    for(;;) {
        int c = sb->sbumpc();
        switch (c) {
        case '\n':
            return is;
        case '\r':
            if(sb->sgetc() == '\n')
                sb->sbumpc();
            return is;
        case std::streambuf::traits_type::eof():
            // Also handle the case when the last line has no line ending
            if(t.empty())
                is.setstate(std::ios::eofbit);
            return is;
        default:
            t += (char)c;
        }
    }
}

Und hier ist ein Testprogramm:

int main()
{
    std::string path = ...  // insert path to test file here

    std::ifstream ifs(path.c_str());
    if(!ifs) {
        std::cout << "Failed to open the file." << std::endl;
        return EXIT_FAILURE;
    }

    int n = 0;
    std::string t;
    while(!safeGetline(ifs, t).eof())
        ++n;
    std::cout << "The file contains " << n << " lines." << std::endl;
    return EXIT_SUCCESS;
}
763305
quelle
1
@Miek: Ich habe den Code gemäß dem Vorschlag von Bo Persons stackackflow.com/questions/9188126/… aktualisiert und einige Tests durchgeführt. Alles funktioniert jetzt so wie es sollte.
Johan Råde
1
@ Thomas Weller: Der Konstruktor und der Destruktor für den Wachposten werden ausgeführt. Dazu gehören beispielsweise die Thread-Synchronisierung, das Überspringen von Leerzeichen und das Aktualisieren des Stream-Status.
Johan Råde
1
Was ist im EOF-Fall der Zweck der Überprüfung, ob diese tleer ist, bevor das Eofbit eingestellt wird? Sollte dieses Bit nicht gesetzt werden, unabhängig davon, ob andere Zeichen eingelesen wurden?
Yay295
1
Yay295: Das eof-Flag sollte gesetzt sein, nicht wenn Sie das Ende der letzten Zeile erreichen, sondern wenn Sie versuchen, über die letzte Zeile hinaus zu lesen. Die Überprüfung stellt sicher, dass dies geschieht, wenn die letzte Zeile keine EOL hat. (Versuchen Sie, die Prüfung zu entfernen, und führen Sie dann das Testprogramm in einer Textdatei aus, in der die letzte Zeile keine EOL enthält. Sie werden sehen.)
Johan Råde
3
Dies liest auch eine leere letzte Zeile, bei deren Verhalten eine leere letzte Zeile nichtstd::get_line ignoriert wird. Ich habe den folgenden Code im Fall eof verwendet, um das std::get_lineVerhalten zu emulieren :is.setstate(std::ios::eofbit); if (t.empty()) is.setstate(std::ios::badbit); return is;
Patrick Roocks
11

Die C ++ - Laufzeit sollte korrekt mit der Endline-Konvention für Ihre bestimmte Plattform umgehen. Insbesondere sollte dieser Code auf allen Plattformen funktionieren:

#include <string>
#include <iostream>
using namespace std;

int main() {
    string line;
    while( getline( cin, line ) ) {
        cout << line << endl;
    }
}

Wenn Sie mit Dateien von einer anderen Plattform arbeiten, sind natürlich alle Wetten ungültig.

Als die beiden häufigsten Plattformen (Linux und Windows) , die beide Linien mit einem Newline - Zeichen beenden, mit dem Windows mit einem Wagenrücklauf vorhergehenden ,, können Sie das letzte Zeichen des untersuchen lineZeichenfolge in dem obigen Code zu sehen , ob es ist , \rund wenn ja Entfernen Sie es, bevor Sie Ihre anwendungsspezifische Verarbeitung durchführen.

Sie könnten sich beispielsweise eine Funktion im getline-Stil zur Verfügung stellen, die ungefähr so ​​aussieht (nicht getestet, Verwendung von Indizes, Substraten usw. nur für pädagogische Zwecke):

ostream & safegetline( ostream & os, string & line ) {
    string myline;
    if ( getline( os, myline ) ) {
       if ( myline.size() && myline[myline.size()-1] == '\r' ) {
           line = myline.substr( 0, myline.size() - 1 );
       }
       else {
           line = myline;
       }
    }
    return os;
}

quelle
9
Die Frage ist , etwa wie mit Dateien von einer anderen Plattform zu beschäftigen.
Leichtigkeitsrennen im Orbit
4
@Neil, diese Antwort reicht noch nicht aus. Wenn ich nur mit CRLFs umgehen wollte, wäre ich nicht zu StackOverflow gekommen. Die eigentliche Herausforderung besteht darin, die Dateien zu verarbeiten, die nur '\ r' enthalten. Sie sind heutzutage ziemlich selten, da MacOS näher an Unix herangekommen ist, aber ich möchte nicht davon ausgehen, dass sie niemals meiner Software zugeführt werden.
Aaron McDaid
1
@ Aaron gut, wenn Sie in der Lage sein wollen, ALLES zu handhaben, müssen Sie Ihren eigenen Code schreiben, um es zu tun.
4
Ich habe in meiner Frage von Anfang an klargestellt, dass es einfach ist, dies zu umgehen, was bedeutet, dass ich dazu bereit und in der Lage bin. Ich habe danach gefragt, weil es eine so häufige Frage zu sein scheint und es eine Vielzahl von Textdateiformaten gibt. Ich nahm an / hoffte, dass das C ++ - Standardkomitee dies eingebaut hatte. Dies war meine Frage.
Aaron McDaid
1
@Neil, ich denke es gibt ein anderes Problem, das ich / wir vergessen haben. Aber zuerst akzeptiere ich, dass es für mich praktisch ist, eine kleine Anzahl von Formaten zu identifizieren, die unterstützt werden sollen. Daher möchte ich Code, der unter Windows und Linux kompiliert wird und mit beiden Formaten funktioniert. Ihr safegetlineist ein wichtiger Teil einer Lösung. Wenn dieses Programm unter Windows kompiliert wird, muss ich die Datei dann auch im Binärformat öffnen? Erlauben Windows-Compiler (im Textmodus), dass sich '\ n' wie '\ r' '\ n' verhält? ifstream f("f.txt", ios_base :: binary | ios_base::in );
Aaron McDaid
8

Lesen Sie die Datei im BINARY- oder im TEXT- Modus? In TEXT - Modus das Paar Wagenrücksetz / Zeilenvorschub, CRLF wird interpretiert als TEXT Ende der Zeile oder Zeilenendezeichen, aber in BINARY holen Sie ONE Byte zu einem Zeitpunkt, der bedeutet , dass entweder Zeichen MUSTignoriert und im Puffer belassen werden, um als weiteres Byte abgerufen zu werden! Wagenrücklauf bedeutet in der Schreibmaschine, dass das Schreibmaschinenauto, in dem der Druckarm liegt, die rechte Kante des Papiers erreicht hat und zur linken Kante zurückgeführt wird. Dies ist ein sehr mechanisches Modell, das der mechanischen Schreibmaschine. Dann bedeutet der Zeilenvorschub, dass die Papierrolle ein wenig nach oben gedreht wird, sodass das Papier in der Lage ist, eine weitere Zeile zu schreiben. Soweit ich mich erinnere, bedeutet eine der niedrigen Ziffern in ASCII, dass Sie ein Zeichen ohne Eingabe nach rechts bewegen, das tote Zeichen, und natürlich bedeutet \ b Rücktaste: Bewegen Sie das Auto um ein Zeichen zurück. Auf diese Weise können Sie Spezialeffekte wie Basiswert (Typ Unterstrich), Durchgestrichen (Typ Minus) hinzufügen, unterschiedliche Akzente annähern, aufheben (Typ X), ohne eine erweiterte Tastatur zu benötigen. Nur durch Einstellen der Position des Fahrzeugs entlang der Linie vor dem Eingeben des Linienvorschubs. Sie können also ASCII-Spannungen in Byte-Größe verwenden, um eine Schreibmaschine automatisch zu steuern, ohne dass sich dazwischen ein Computer befindet. Wenn die automatische Schreibmaschine eingeführt wird,AUTOMATISCH bedeutet, dass, sobald Sie die äußerste Kante des Papiers erreicht haben, das Auto nach links zurückkehrt UND der angewendete Zeilenvorschub, dh das Auto wird automatisch zurückgegeben, wenn sich die Rolle nach oben bewegt! Sie benötigen also nicht beide Steuerzeichen, sondern nur eines, die \ n, die neue Zeile oder den Zeilenvorschub.

Dies hat nichts mit Programmierung zu tun, aber ASCII ist älter und HEY! Es sieht so aus, als hätten einige Leute nicht nachgedacht, als sie anfingen, Textsachen zu machen! Die UNIX-Plattform setzt eine elektrische automatische Maschinenmaschine voraus. Das Windows-Modell ist vollständiger und ermöglicht die Steuerung mechanischer Maschinen, obwohl einige Steuerzeichen auf Computern immer weniger nützlich sind, wie das Glockenzeichen 0x07, wenn ich mich recht erinnere ... Einige vergessene Texte müssen ursprünglich mit Steuerzeichen erfasst worden sein für elektrisch gesteuerte Schreibmaschinen und es verewigte das Modell ...

Tatsächlich wäre die richtige Variante, nur den Zeilenvorschub \ r einzuschließen, wobei der Wagenrücklauf unnötig ist, dh automatisch, daher:

char c;
ifstream is;
is.open("",ios::binary);
...
is.getline(buffer, bufsize, '\r');

//ignore following \n or restore the buffer data
if ((c=is.get())!='\n') is.rdbuf()->sputbackc(c);
...

wäre der korrekteste Weg, um alle Arten von Dateien zu behandeln. Beachte jedoch , dass \ n in TEXT - Modus ist eigentlich das Byte - Paar 0x0d 0x0A, aber 0x0d IST nur \ r \ n \ r enthält in TEXT - Modus jedoch nicht in BINARY , so \ n und \ r \ n oder äquivalent sind ... sollte sein. Dies ist eine sehr grundlegende Verwirrung in der Branche, eine typische Trägheit der Branche, da die Konvention darin besteht, auf ALLEN Plattformen von CRLF zu sprechen und dann in verschiedene binäre Interpretationen zu fallen. Genau genommen sind Dateien, die NUR 0x0d (Wagenrücklauf) als \ n (CRLF oder Zeilenvorschub) enthalten, in TEXT fehlerhaftModus (Schreibmaschinenmaschine: einfach das Auto zurückgeben und alles durchstreichen ...) und sind ein nicht zeilenorientiertes Binärformat (entweder \ r oder \ r \ n bedeutet zeilenorientiert), sodass Sie nicht als Text lesen sollen! Der Code sollte möglicherweise mit einer Benutzermeldung fehlschlagen. Dies hängt nicht nur vom Betriebssystem ab, sondern auch von der Implementierung der C-Bibliothek, was die Verwirrung und mögliche Variationen erhöht ... (insbesondere für transparente UNICODE-Übersetzungsebenen, die einen weiteren Artikulationspunkt für verwirrende Variationen hinzufügen).

Das Problem mit dem vorherigen Code-Snippet (mechanische Schreibmaschine) ist, dass es sehr ineffizient ist, wenn nach \ r keine \ n Zeichen stehen (automatischer Schreibmaschinentext). Dann wird auch der BINARY- Modus angenommen, in dem die C-Bibliothek gezwungen ist, Textinterpretationen (Gebietsschema) zu ignorieren und die bloßen Bytes weiterzugeben. Es sollte keinen Unterschied in den tatsächlichen Textzeichen zwischen beiden Modi geben, nur in den Steuerzeichen. Daher ist das Lesen von BINARY im Allgemeinen besser als der TEXT- Modus. Diese Lösung ist für BINARY effizientModus typische Windows OS-Textdateien unabhängig von Variationen der C-Bibliothek und ineffizient für andere Plattformtextformate (einschließlich Webübersetzungen in Text). Wenn Sie Wert auf Effizienz legen, müssen Sie einen Funktionszeiger verwenden, einen Test für \ r vs \ r \ n Zeilensteuerelemente durchführen, wie Sie möchten, und dann den besten getline-Benutzercode in den Zeiger auswählen und von dort aus aufrufen es.

Ich erinnere mich übrigens, dass ich auch einige \ r \ r \ n Textdateien gefunden habe ... die sich in zweizeiligen Text übersetzen lassen, so wie es einige gedruckte Textkonsumenten noch benötigen.

Danilo J. Bonsignore
quelle
+1 für "ios :: binary" - manchmal möchten Sie die Datei tatsächlich so lesen, wie sie ist (z. B. zum Berechnen einer Prüfsumme usw.), ohne dass die Laufzeit die Zeilenenden ändert.
Matthias
2

Eine Lösung wäre, zuerst alle Zeilenenden zu suchen und durch '\ n' zu ersetzen - genau wie z. B. Git dies standardmäßig tut.

user2061057
quelle
1

Abgesehen davon, dass Sie Ihren eigenen benutzerdefinierten Handler schreiben oder eine externe Bibliothek verwenden, haben Sie kein Glück. Am einfachsten ist es, zu überprüfen, ob line[line.length() - 1]nicht '\ r' ist. Unter Linux ist dies überflüssig, da die meisten Zeilen mit '\ n' enden, was bedeutet, dass Sie ein gutes Stück Zeit verlieren, wenn sich dies in einer Schleife befindet. Unter Windows ist dies ebenfalls überflüssig. Was ist jedoch mit klassischen Mac-Dateien, die mit '\ r' enden? std :: getline würde für diese Dateien unter Linux oder Windows nicht funktionieren, da '\ n' und '\ r' '\ n' beide mit '\ n' enden, sodass nicht mehr nach '\ r' gesucht werden muss. Offensichtlich würde eine solche Aufgabe, die mit diesen Dateien funktioniert, nicht gut funktionieren. Dann gibt es natürlich die zahlreichen EBCDIC-Systeme, die die meisten Bibliotheken nicht in Angriff nehmen können.

Die Suche nach '\ r' ist wahrscheinlich die beste Lösung für Ihr Problem. Wenn Sie im Binärmodus lesen, können Sie nach allen drei gemeinsamen Zeilenenden suchen ('\ r', '\ r \ n' und '\ n'). Wenn Sie sich nur für Linux und Windows interessieren, da Mac-Zeilenenden im alten Stil nicht mehr lange verfügbar sein sollten, suchen Sie nur nach '\ n' und entfernen Sie das nachfolgende Zeichen '\ r'.


quelle
0

Wenn bekannt ist, wie viele Elemente / Zahlen jede Zeile hat, könnte man eine Zeile mit zB 4 Zahlen als lesen

string num;
is >> num >> num >> num >> num;

Dies funktioniert auch mit anderen Zeilenenden.

Martin Thümmel
quelle