Ottenere std :: ifstream per gestire LF, CR e CRLF?


85

Nello specifico mi interessa istream& getline ( istream& is, string& str );. C'è un'opzione per il costruttore ifstream per dirgli di convertire tutte le codifiche di nuova riga in "\ n" sotto il cofano? Voglio essere in grado di chiamare getlinee fare in modo che gestisca con garbo tutte le terminazioni di linea.

Aggiornamento : per chiarire, voglio essere in grado di scrivere codice che si compili quasi ovunque e riceva input da quasi ovunque. Compresi i file rari che hanno "\ r" senza "\ n". Riduzione al minimo dei disagi per gli utenti del software.

È facile risolvere il problema, ma sono ancora curioso di sapere come, nello standard, gestire in modo flessibile tutti i formati di file di testo.

getlinelegge in una riga intera, fino a '\ n', in una stringa. Il '\ n' viene consumato dallo stream, ma getline non lo include nella stringa. Finora va bene, ma potrebbe esserci una "\ r" appena prima della "\ n" che viene inclusa nella stringa.

Ci sono tre tipi di terminazioni di riga viste nei file di testo: "\ n" è la fine convenzionale sulle macchine Unix, "\ r" era (credo) usata sui vecchi sistemi operativi Mac e Windows ne utilizza una coppia, "\ r" seguito da "\ n".

Il problema è che getlinelascia la "\ r" alla fine della stringa.

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.
}

Modifica Grazie a Neil per aver sottolineato che f.good()non è quello che volevo. !f.fail()è quello che voglio.

Posso rimuoverlo manualmente da solo (vedi modifica di questa domanda), il che è facile per i file di testo di Windows. Ma sono preoccupato che qualcuno inserisca un file contenente solo "\ r". In tal caso, presumo che getline consumerà l'intero file, pensando che sia una singola riga!

.. e questo non considera nemmeno Unicode :-)

.. forse Boost ha un bel modo per consumare una riga alla volta da qualsiasi tipo di file di testo?

Modifica Lo sto usando per gestire i file di Windows, ma sento ancora che non dovrei farlo! E questo non eseguirà il fork per i file solo "\ r".

if(!line.empty() && *line.rbegin() == '\r') {
    line.erase( line.length()-1, 1);
}

2
\ n significa nuova riga in qualunque modo sia presentato nel sistema operativo corrente. La biblioteca si occupa di questo. Ma affinché funzioni, un programma compilato in Windows dovrebbe leggere file di testo da Windows, un programma compilato in unix, file di testo da unix ecc.
George Kastrinis

1
@George, anche se sto compilando su una macchina Linux, a volte utilizzo file di testo che provengono originariamente da una macchina Windows. Potrei rilasciare il mio software (un piccolo strumento per l'analisi della rete) e voglio essere in grado di dire agli utenti che possono alimentare quasi in qualsiasi momento file di testo (tipo ASCII).
Aaron McDaid


1
Nota che se (f.good ()) non fa quello che sembri pensi che faccia.

1
@JonathanMee: Potrebbe essere stato come questo . Può essere.
Gare di leggerezza in orbita

Risposte:


111

Come ha sottolineato Neil, "il runtime C ++ dovrebbe trattare correttamente qualunque sia la convenzione di fine riga per la tua particolare piattaforma."

Tuttavia, le persone spostano i file di testo tra piattaforme diverse, quindi non è abbastanza buono. Ecco una funzione che gestisce tutte e tre le terminazioni di riga ("\ r", "\ n" e "\ r \ n"):

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;
        }
    }
}

Ed ecco un programma di test:

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;
}

1
@Miek: Ho aggiornato il codice seguente Bo Persone suggerimento stackoverflow.com/questions/9188126/... e corse alcuni test. Tutto ora funziona come dovrebbe.
Johan Råde,

1
@Thomas Weller: Il costruttore e il distruttore della sentinella vengono eseguiti. Esse eseguono operazioni come la sincronizzazione dei thread, il salto di spazi vuoti e l'aggiornamento dello stato del flusso.
Johan Råde

1
Nel caso EOF, qual è lo scopo di controllare che tsia vuoto prima di impostare l'eofbit. Quel bit non dovrebbe essere impostato indipendentemente dagli altri caratteri che sono stati letti?
Yay295

1
Yay295: Il flag eof dovrebbe essere impostato, non quando si raggiunge la fine dell'ultima riga, ma quando si tenta di leggere oltre l'ultima riga. Il controllo si assicura che ciò avvenga quando l'ultima riga non ha EOL. (Prova a rimuovere il segno di spunta, quindi esegui il programma di test su un file di testo dove l'ultima riga non ha EOL e vedrai.)
Johan Råde

3
Questo legge anche un'ultima riga vuota, che non è il cui comportamento std::get_lineignora un'ultima riga vuota. Ho usato il seguente codice nel caso eof per emulare il std::get_linecomportamento:is.setstate(std::ios::eofbit); if (t.empty()) is.setstate(std::ios::badbit); return is;
Patrick Roocks

11

Il runtime C ++ dovrebbe gestire correttamente qualunque sia la convenzione di endline per la tua particolare piattaforma. Nello specifico, questo codice dovrebbe funzionare su tutte le piattaforme:

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

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

Ovviamente, se hai a che fare con file da un'altra piattaforma, tutte le scommesse sono disattivate.

Poiché le due piattaforme più comuni (Linux e Windows) terminano entrambe le righe con un carattere di nuova riga, con Windows che lo precede con un ritorno a capo, puoi esaminare l'ultimo carattere della linestringa nel codice sopra per vedere se lo è \re se è così rimuoverlo prima di eseguire l'elaborazione specifica dell'applicazione.

Ad esempio, potresti fornirti una funzione di stile getline simile a questa (non testata, uso di indici, substr ecc. Solo per scopi pedagogici):

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;
}

9
La domanda è su come gestire i file da un'altra piattaforma.
Gare di leggerezza in orbita

4
@ Neil, questa risposta non è ancora sufficiente. Se avessi voluto solo gestire i CRLF, non sarei arrivato a StackOverflow. La vera sfida è gestire i file che hanno solo "\ r". Al giorno d'oggi sono piuttosto rari, ora che MacOS si è avvicinato a Unix, ma non voglio presumere che non verranno mai alimentati dal mio software.
Aaron McDaid

1
@Aaron beh, se vuoi essere in grado di gestire QUALSIASI COSA devi scrivere il tuo codice per farlo.

4
Ho chiarito fin dall'inizio nella mia domanda che è facile aggirare il problema, il che implica che sono disposto e in grado di farlo. Ho chiesto informazioni su questo perché sembra essere una domanda così comune e ci sono una varietà di formati di file di testo. Pensavo / speravo che il comitato per gli standard C ++ l'avesse integrato. Questa era la mia domanda.
Aaron McDaid

1
@Neil, penso che ci sia un altro problema che ho / abbiamo dimenticato. Ma prima accetto che sia pratico per me identificare un piccolo numero di formati da supportare. Pertanto, voglio codice che verrà compilato su Windows e Linux e che funzioni con entrambi i formati. La tua safegetlineè una parte importante di una soluzione. Ma se questo programma viene compilato su Windows, dovrò anche aprire il file in formato binario? I compilatori Windows (in modalità testo) consentono a "\ n" di comportarsi come "\ r" "\ n"? ifstream f("f.txt", ios_base :: binary | ios_base::in );
Aaron McDaid

8

Stai leggendo il file in modalità BINARIA o in modalità TESTO ? In modalità TESTO la coppia ritorno a capo / avanzamento riga, CRLF , viene interpretata come TESTO fine riga o carattere di fine riga, ma in BINARIO si recupera solo UN byte alla volta, il che significa che entrambi i caratteri DEVONOessere ignorato e lasciato nel buffer per essere recuperato come un altro byte! Il ritorno del carrello significa, nella macchina da scrivere, che il carrello della macchina da scrivere, dove giace il braccio di stampa, ha raggiunto il bordo destro del foglio ed è riportato sul bordo sinistro. Questo è un modello molto meccanico, quello della macchina da scrivere meccanica. Quindi l'avanzamento riga significa che il rotolo di carta viene ruotato leggermente verso l'alto in modo che la carta sia in posizione per iniziare un'altra riga di digitazione. Per quanto ricordo, una delle cifre basse in ASCII significa spostarsi a destra di un carattere senza digitare, il carattere morto e, naturalmente, \ b significa backspace: sposta l'auto di un carattere indietro. In questo modo puoi aggiungere effetti speciali, come sottostante (tipo trattino basso), barrato (tipo meno), accenti diversi approssimativi, annullamento (tipo X), senza bisogno di una tastiera estesa, semplicemente regolando la posizione dell'auto lungo la linea prima di entrare in avanzamento linea. Quindi puoi usare tensioni ASCII di dimensioni di byte per controllare automaticamente una macchina da scrivere senza un computer in mezzo. Quando viene introdotta la macchina da scrivere automatica,AUTOMATICO significa che una volta raggiunto il bordo più lontano della carta, l'auto viene riportata a sinistra E l'avanzamento di riga applicato, cioè, si presume che la macchina ritorni automaticamente quando il rotolo si alza! Quindi non hai bisogno di entrambi i caratteri di controllo, solo uno, la \ n, nuova riga o avanzamento riga.

Questo non ha nulla a che fare con la programmazione, ma ASCII è più vecchio e HEY! sembra che alcune persone non stessero pensando quando hanno iniziato a fare cose di testo! La piattaforma UNIX presuppone una macchina di tipo automatica elettrica; il modello Windows è più completo e consente il controllo di macchine meccaniche, anche se alcuni caratteri di controllo diventano sempre meno utili nei computer, come il carattere campana, 0x07 se ricordo bene ... Alcuni testi dimenticati devono essere stati originariamente catturati con caratteri di controllo per macchine da scrivere a comando elettrico e ha perpetuato il modello ...

In realtà la variazione corretta sarebbe quella di includere solo \ r, avanzamento riga, il ritorno a capo non essendo necessario, cioè automatico, quindi:

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);
...

sarebbe il modo più corretto per gestire tutti i tipi di file. Nota tuttavia che \ n in modalità TESTO è effettivamente la coppia di byte 0x0d 0x0a, ma 0x0d È solo \ r: \ n include \ r in modalità TESTO ma non in BINARIO , quindi \ ne \ r \ n sono equivalenti ... o dovrebbe essere. Questa è una confusione molto basilare del settore in realtà, tipica inerzia del settore, poiché la convenzione è parlare di CRLF, in TUTTE le piattaforme, quindi cadere in diverse interpretazioni binarie. A rigor di termini, i file che includono SOLO 0x0d (ritorno a capo) come \ n (CRLF o avanzamento riga), non sono corretti in TEXTmodalità (macchina da scrivere: basta restituire la macchina e barrare tutto ...), e sono un formato binario non orientato alla riga (\ r o \ r \ n significa orientato alla riga) quindi non dovresti leggere come testo! Il codice dovrebbe fallire forse con qualche messaggio utente. Questo non dipende solo dal sistema operativo, ma anche dall'implementazione della libreria C, aggiungendo confusione e possibili variazioni ... (in particolare per i livelli di traduzione UNICODE trasparenti aggiungendo un altro punto di articolazione per variazioni confuse).

Il problema con lo snippet di codice precedente (macchina da scrivere meccanica) è che è molto inefficiente se non ci sono \ n caratteri dopo \ r (testo macchina da scrivere automatica). Quindi assume anche la modalità BINARY in cui la libreria C è costretta a ignorare le interpretazioni del testo (locale) e dare via i byte puri. Non dovrebbe esserci alcuna differenza nei caratteri di testo effettivi tra le due modalità, solo nei caratteri di controllo, quindi in generale la lettura di BINARIO è migliore della modalità TESTO . Questa soluzione è efficiente per BINARYmodalità file di testo tipici del sistema operativo Windows indipendentemente dalle variazioni della libreria C e inefficiente per altri formati di testo della piattaforma (comprese le traduzioni web in testo). Se ti interessa l'efficienza, la strada da percorrere è usare un puntatore a funzione, fare un test per i controlli di riga \ r vs \ r \ n come preferisci, quindi selezionare il miglior codice utente getline nel puntatore e invocarlo da esso.

Per inciso, ricordo di aver trovato anche dei \ r \ r \ n file di testo ... che si traducono in testo a doppia riga proprio come è ancora richiesto da alcuni consumatori di testo stampato.


+1 per "ios :: binary" - a volte, si desidera effettivamente leggere il file così com'è (ad esempio per calcolare un checksum, ecc.) Senza che il runtime modifichi le terminazioni di riga.
Matthias

2

Una soluzione potrebbe essere quella di cercare e sostituire prima tutte le terminazioni di riga con "\ n", proprio come fa Git per impostazione predefinita.


1

Oltre a scrivere il tuo gestore personalizzato o utilizzare una libreria esterna, sei sfortunato. La cosa più semplice da fare è verificare che line[line.length() - 1]non sia "\ r". Su Linux, questo è superfluo in quanto la maggior parte delle righe finirà con "\ n", il che significa che perderai un bel po 'di tempo se questo è in un ciclo. Su Windows, anche questo è superfluo. Tuttavia, che dire dei file Mac classici che terminano con "\ r"? std :: getline non funzionerebbe per quei file su Linux o Windows perché '\ n' e '\ r' '\ n' terminano entrambi con '\ n', eliminando la necessità di controllare la presenza di '\ r'. Ovviamente un'attività del genere che funziona con quei file non funzionerebbe bene. Naturalmente, poi esistono i numerosi sistemi EBCDIC, qualcosa che la maggior parte delle biblioteche non oserà affrontare.

La ricerca di "\ r" è probabilmente la migliore soluzione al tuo problema. La lettura in modalità binaria ti consentirebbe di controllare tutte e tre le terminazioni di riga comuni ('\ r', '\ r \ n' e '\ n'). Se ti interessano solo Linux e Windows poiché le terminazioni di riga Mac vecchio stile non dovrebbero essere disponibili per molto più tempo, controlla solo "\ n" e rimuovi il carattere finale "\ r".


0

Se si sa quanti elementi / numeri ha ciascuna riga, si potrebbe leggere una riga con ad esempio 4 numeri come

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

Funziona anche con altre terminazioni di riga.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.