Uzyskiwanie std :: ifstream do obsługi LF, CR i CRLF?

85

W szczególności jestem zainteresowany istream& getline ( istream& is, string& str );. Czy istnieje opcja dla konstruktora ifstream, aby nakazać mu konwersję wszystkich kodowań nowej linii na „\ n” pod maską? Chcę mieć możliwość dzwonienia getlinei sprawnej obsługi wszystkich zakończeń linii.

Aktualizacja : Aby wyjaśnić, chcę mieć możliwość pisania kodu, który kompiluje się prawie wszędzie i pobiera dane wejściowe z prawie każdego miejsca. W tym rzadkie pliki, które mają „\ r” bez „\ n”. Minimalizacja niedogodności dla wszystkich użytkowników oprogramowania.

Obejście problemu jest łatwe, ale nadal jestem ciekawy, jak w standardzie elastycznie obsługiwać wszystkie formaty plików tekstowych.

getlineczyta w pełnym wierszu, aż do „\ n”, do ciągu. „\ N” jest pobierane ze strumienia, ale getline nie włącza go do ciągu. Jak dotąd jest to w porządku, ale tuż przed znakiem „\ n” może znajdować się znak „\ r”, który zostanie dołączony do ciągu.

Istnieją trzy typy końcówek linii widocznych w plikach tekstowych: „\ n” to konwencjonalne zakończenie na komputerach z systemem Unix, „\ r” był (jak sądzę) używany w starych systemach operacyjnych Mac, a Windows używa pary „\ r” po "\ n".

Problem polega na tym, że getlinepozostawia znak „\ r” na końcu łańcucha.

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

Edytuj Dziękuję Neilowi ​​za wskazanie, że f.good()nie tego chciałem. !f.fail()jest tym, czego chcę.

Mogę go usunąć ręcznie (zobacz edycję tego pytania), co jest łatwe w przypadku plików tekstowych systemu Windows. Ale martwię się, że ktoś umieści plik zawierający tylko „\ r”. W takim przypadku zakładam, że getline zajmie cały plik, myśląc, że jest to pojedyncza linia!

.. i to nawet nie biorąc pod uwagę Unicode :-)

.. może Boost ma dobry sposób na wykorzystanie jednej linii na raz z dowolnego typu pliku tekstowego?

Edytuj Używam tego do obsługi plików systemu Windows, ale nadal uważam, że nie powinienem tego robić! I to nie rozwidli plików tylko „\ r”.

if(!line.empty() && *line.rbegin() == '\r') {
    line.erase( line.length()-1, 1);
}
Aaron McDaid
źródło
2
\ n oznacza nową linię w sposób przedstawiony w bieżącym systemie operacyjnym. Biblioteka się tym zajmuje. Ale żeby to zadziałało, program skompilowany w Windows powinien czytać pliki tekstowe z Windows, program skompilowany w unixie, pliki tekstowe z
unixa
1
@George, mimo że kompiluję na komputerze z systemem Linux, czasami używam plików tekstowych pochodzących z komputera z systemem Windows. Mogę wypuścić swoje oprogramowanie (małe narzędzie do analizy sieci) i chcę być w stanie powiedzieć użytkownikom, że mogą podawać w prawie każdym momencie plik tekstowy (podobny do ASCII).
Aaron McDaid
3
Mały przypadek testowy, który pokazuje Twój problem .
Wyścigi lekkości na orbicie
1
Zauważ, że if (f.good ()) nie robi tego, co myślisz, że robi.
1
@JonathanMee: To może być jak ten . Może.
Wyścigi lekkości na orbicie

Odpowiedzi:

111

Jak zauważył Neil, „środowisko wykonawcze C ++ powinno poprawnie radzić sobie z każdą konwencją zakończenia linii dla twojej konkretnej platformy”.

Jednak ludzie przenoszą pliki tekstowe między różnymi platformami, więc to nie wystarczy. Oto funkcja, która obsługuje wszystkie trzy zakończenia linii („\ r”, „\ n” i „\ 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;
        }
    }
}

A oto program testowy:

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;
}
user763305
źródło
1
@Miek: Zaktualizowałem kod zgodnie z sugestią Bo Persons stackoverflow.com/questions/9188126/ ... i przeprowadziłem kilka testów. Teraz wszystko działa tak, jak powinno.
Johan Råde,
1
@Thomas Weller: Konstruktor i destruktor wartownika są wykonywane. Robią takie rzeczy, jak synchronizacja wątków, pomijanie białych znaków i aktualizowanie stanu strumienia.
Johan Råde
1
W przypadku EOF, jaki jest cel sprawdzenia, czy tjest pusty przed ustawieniem eofbit. Czy ten bit nie powinien być ustawiony niezależnie od wczytania innych znaków?
Yay295
1
Yay295: Flaga eof powinna być ustawiona nie wtedy, gdy dojdziesz do końca ostatniej linii, ale gdy próbujesz czytać poza ostatnią linią. Sprawdzenie zapewnia, że ​​dzieje się tak, gdy ostatnia linia nie ma EOL. (Spróbuj usunąć zaznaczenie, a następnie uruchom program testowy na pliku tekstowym, w którym ostatnia linia nie ma EOL, a zobaczysz.)
Johan Råde
3
To także czyta pustą ostatnią linię, co nie jest zachowaniem, std::get_linektóre ignoruje pustą ostatnią linię. Użyłem następującego kodu w przypadku std::get_lineis.setstate(std::ios::eofbit); if (t.empty()) is.setstate(std::ios::badbit); return is;
eof,
11

Środowisko wykonawcze C ++ powinno poprawnie obsługiwać dowolną konwencję końca linii dla danej platformy. W szczególności ten kod powinien działać na wszystkich platformach:

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

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

Oczywiście, jeśli masz do czynienia z plikami z innej platformy, wszystkie zakłady są wyłączone.

Ponieważ dwie najpopularniejsze platformy (Linux i Windows) obie kończą wiersze znakiem nowego wiersza, przy czym Windows poprzedza go znakiem powrotu karetki, możesz sprawdzić ostatni znak lineciągu w powyższym kodzie, aby sprawdzić, czy tak jest, \ra jeśli tak usuń go przed wykonaniem przetwarzania specyficznego dla aplikacji.

Na przykład możesz zapewnić sobie funkcję w stylu getline, która wygląda mniej więcej tak (nie testowana, użycie indeksów, substr itp. Tylko do celów pedagogicznych):

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

źródło
9
Pytanie dotyczy tego, jak radzić sobie z plikami z innej platformy.
Wyścigi lekkości na orbicie
4
@Neil, ta odpowiedź nie jest jeszcze wystarczająca. Gdybym tylko chciał obsłużyć CRLF, nie przyszedłbym do StackOverflow. Prawdziwym wyzwaniem jest obsłużenie plików, które mają tylko „\ r”. W dzisiejszych czasach są one dość rzadkie, teraz, gdy MacOS zbliżył się do Uniksa, ale nie chcę zakładać, że nigdy nie zostaną wprowadzone do mojego oprogramowania.
Aaron McDaid
1
@Aaron cóż, jeśli chcesz sobie poradzić z WSZYSTKIM, musisz napisać własny kod, aby to zrobić.
4
W swoim pytaniu od samego początku wyjaśniłem, że obejście tego problemu jest łatwe, co oznacza, że ​​chcę i jestem w stanie to zrobić. Zapytałem o to, ponieważ wydaje się, że jest to bardzo częste pytanie, a istnieje wiele różnych formatów plików tekstowych. Zakładałem / miałem nadzieję, że komitet normalizacyjny C ++ wbudował to w to. To było moje pytanie.
Aaron McDaid
1
@Neil, myślę, że jest inny problem, o którym zapomnieliśmy. Ale najpierw zgadzam się, że z praktycznego punktu widzenia zidentyfikuję niewielką liczbę obsługiwanych formatów. Dlatego potrzebuję kodu, który będzie kompilował się w systemach Windows i Linux i który będzie działał z każdym formatem. Twój safegetlinejest ważną częścią rozwiązania. Ale jeśli ten program jest kompilowany w systemie Windows, czy będę musiał również otworzyć plik w formacie binarnym? Czy kompilatory Windows (w trybie tekstowym) pozwalają '\ n' zachowywać się jak '\ r' '\ n'? ifstream f("f.txt", ios_base :: binary | ios_base::in );
Aaron McDaid
8

Czy czytasz plik w trybie BINARNY czy TEKSTOWY ? W TEKSTU trybie pasza powrót / linia przewóz pary, CRLF , jest interpretowana jako TEKST końca linii lub znaku końca linii, ale w BINARY Ci pobrać tylko JEDEN bajt na raz, co oznacza, że zarówno charakter koniecznościąbyć ignorowane i pozostawione w buforze do pobrania jako kolejny bajt! Powrót karetki oznacza w maszynie do pisania, że ​​wózek maszyny do pisania, w którym leży ramię drukujące, osiągnął prawą krawędź papieru i powrócił do lewej krawędzi. To bardzo mechaniczny model mechanicznej maszyny do pisania. Wówczas wysunięcie wiersza oznacza, że ​​rolka papieru jest nieco obrócona do góry, aby papier mógł rozpocząć kolejny wiersz pisania. O ile pamiętam jedna z małych cyfr w ASCII oznacza przesunięcie w prawo o jeden znak bez wpisywania, martwy znak i oczywiście \ b oznacza cofnięcie: cofnij samochód o jeden znak. W ten sposób możesz dodać efekty specjalne, takie jak podkład (typ podkreślenia), przekreślenie (typ minus), przybliżone różne akcenty, anulowanie (typ X), bez konieczności korzystania z rozszerzonej klawiatury, po prostu dostosowując położenie samochodu wzdłuż linii przed wejściem do linii zasilającej. Możesz więc używać napięć ASCII wielkości bajtów do automatycznego sterowania maszyną do pisania bez komputera pomiędzy nimi. Po wprowadzeniu automatycznej maszyny do pisaniaAUTOMATYCZNY oznacza, że ​​po osiągnięciu najdalszej krawędzi papieru samochód jest cofany w lewo ORAZ zastosowany wysuw linii, czyli zakłada się, że samochód jest automatycznie cofany, gdy rolka przesuwa się do góry! Więc nie potrzebujesz obu znaków sterujących, tylko jeden, \ n, nowy wiersz lub nowy wiersz.

Nie ma to nic wspólnego z programowaniem, ale ASCII jest starszy i HEJ! wygląda na to, że niektórzy ludzie nie myśleli, kiedy zaczęli pisać tekst! Platforma UNIX zakłada automatyczną maszynę elektryczną; model Windows jest bardziej kompletny i pozwala na sterowanie maszynami mechanicznymi, chociaż niektóre znaki sterujące stają się coraz mniej przydatne w komputerach, jak np. znak dzwonka, 0x07, jeśli dobrze pamiętam ... Niektóre zapomniane teksty musiały być pierwotnie przechwycone za pomocą znaków sterujących do maszyn do pisania sterowanych elektrycznie i utrwalił model ...

Właściwie poprawną odmianą byłoby po prostu dołączenie \ r, wysuw wiersza, powrót karetki jest niepotrzebny, to znaczy automatyczny, stąd:

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

byłby najbardziej poprawnym sposobem obsługi wszystkich typów plików. Zauważ jednak, że \ nw TEKST trybie jest w rzeczywistości parą bajtów 0x0d 0x0a, ale 0x0d JEST po prostu \ r: \ n obejmuje \ r w trybie TEKST , ale nie w trybie BINARNYM , więc \ n i \ r \ n są równoważne ... lub Powinien być. W rzeczywistości jest to bardzo podstawowe zamieszanie w branży, typowa bezwładność w branży, ponieważ konwencja mówi o CRLF, na WSZYSTKICH platformach, a następnie należy do różnych interpretacji binarnych. Ściśle mówiąc, pliki zawierające TYLKO 0x0d (powrót karetki) jako \ n (CRLF lub nowy wiersz) są zniekształcone w TEKŚCIEtryb (maszyna do pisania: po prostu zwróć samochód i przekreśl wszystko ...) i są nieliniowym formatem binarnym (albo \ r lub \ r \ n, czyli zorientowanym na wiersz), więc nie powinieneś czytać jako tekstu! Kod powinien zawieść, być może z jakąś wiadomością użytkownika. Nie zależy to tylko od systemu operacyjnego, ale także od implementacji biblioteki C, co zwiększa zamieszanie i możliwe warianty ... (szczególnie w przypadku przezroczystych warstw tłumaczenia UNICODE, dodając kolejny punkt artykulacji dla mylących odmian).

Problem z poprzednim fragmentem kodu (mechaniczna maszyna do pisania) polega na tym, że jest on bardzo nieefektywny, jeśli nie ma \ n znaków po \ r (automatyczna maszyna do pisania). Wtedy też zakłada tryb BINARNY , w którym biblioteka C jest zmuszona ignorować interpretacje tekstu (ustawienia regionalne) i oddawać zwykłe bajty. Nie powinno być różnicy w rzeczywistych znakach tekstu między obydwoma trybami, tylko w znakach kontrolnych, więc ogólnie rzecz biorąc, czytanie BINARY jest lepsze niż tryb TEKST . To rozwiązanie jest wydajne dla BINARYtryb typowych plików tekstowych systemu operacyjnego Windows niezależnie od odmian biblioteki C i nieefektywny w przypadku innych formatów tekstowych platformy (w tym tłumaczenia stron internetowych na tekst). Jeśli zależy Ci na wydajności, najlepszym rozwiązaniem jest użycie wskaźnika funkcji, wykonanie testu kontrolek linii \ r vs \ r \ n w dowolny sposób, a następnie wybranie najlepszego kodu użytkownika getline do wskaźnika i wywołanie go z to.

Nawiasem mówiąc, pamiętam, że znalazłem też kilka plików tekstowych \ r \ r \ n ... co przekłada się na tekst w dwóch wierszach, tak jak jest to nadal wymagane przez niektórych użytkowników tekstu drukowanego.

Danilo J. Bonsignore
źródło
+1 dla "ios :: binary" - czasami faktycznie chcesz odczytać plik tak, jak jest (np. W celu obliczenia sumy kontrolnej itp.) Bez zmiany końcówek linii przez środowisko wykonawcze.
Matthias
2

Jednym z rozwiązań byłoby wyszukanie i zamiana wszystkich końcówek na '\ n' - tak jak np. Git robi to domyślnie.

user2061057
źródło
1

Oprócz pisania własnego niestandardowego programu obsługi lub korzystania z zewnętrznej biblioteki, nie masz szczęścia. Najłatwiej jest to sprawdzić, aby się upewnićline[line.length() - 1] nie ma znaku „\ r”. W Linuksie jest to zbędne, ponieważ większość linii kończy się na '\ n', co oznacza, że ​​stracisz sporo czasu, jeśli jest to pętla. W systemie Windows jest to również zbędne. A co z klasycznymi plikami Mac, które kończą się na „\ r”? std :: getline nie będzie działać dla tych plików w systemie Linux lub Windows, ponieważ „\ n” i „\ r” \ n 'kończą się na „\ n”, eliminując potrzebę sprawdzania „\ r”. Oczywiście takie zadanie, które działa z tymi plikami, nie zadziałałoby dobrze. Oczywiście istnieje wiele systemów EBCDIC, z czym większość bibliotek nie odważy się sobie poradzić.

Sprawdzanie „\ r” jest prawdopodobnie najlepszym rozwiązaniem problemu. Czytanie w trybie binarnym umożliwiłoby sprawdzenie wszystkich trzech typowych zakończeń wierszy („\ r”, „\ r \ n” i „\ n”). Jeśli zależy Ci tylko na Linuksie i Windowsie, ponieważ zakończenia linii w starym stylu Maca nie powinny istnieć zbyt długo, sprawdź tylko '\ n' i usuń końcowy znak '\ r'.


źródło
0

Jeśli wiadomo, ile pozycji / liczb ma każda linia, można odczytać jedną linię z np. 4 liczbami jako

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

Działa to również z innymi zakończeniami linii.

Martin Thümmel
źródło