web-dev-qa-db-fra.com

Obtenir std :: ifstream pour gérer LF, CR et CRLF?

Plus précisément, je suis intéressé par istream& getline ( istream& is, string& str );. Y a-t-il une option pour le constructeur ifstream pour lui dire de convertir tous les encodages de nouvelle ligne en "\ n" sous le capot? Je veux pouvoir appeler getline et le faire gérer avec élégance toutes les fins de ligne.

Mise à jour : Pour clarifier, je veux pouvoir écrire du code qui se compile presque n'importe où, et prendra des entrées de presque n'importe où. Y compris les fichiers rares qui ont "\ r" sans "\ n". Minimiser les désagréments pour tous les utilisateurs du logiciel.

Il est facile de contourner le problème, mais je suis toujours curieux de savoir comment, dans la norme, gérer de manière flexible tous les formats de fichiers texte.

getline lit une ligne complète, jusqu'à un '\ n', dans une chaîne. Le '\ n' est consommé à partir du flux, mais getline ne l'inclut pas dans la chaîne. C'est bien jusqu'à présent, mais il peut y avoir un "\ r" juste avant le "\ n" qui est inclus dans la chaîne.

Il y a trois types de fins de ligne vu dans les fichiers texte: '\ n' est la fin conventionnelle sur les machines Unix, '\ r' était (je pense) utilisé sur les anciens systèmes d'exploitation Mac et Windows utilise une paire, '\ r' suivi de '\ n'.

Le problème est que getline laisse le '\ r' à la fin de la chaîne.

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

Edit Merci à Neil d'avoir souligné que f.good() n'est pas ce que je voulais. !f.fail() est ce que je veux.

Je peux le supprimer moi-même manuellement (voir la modification de cette question), ce qui est facile pour les fichiers texte Windows. Mais je crains que quelqu'un alimente un fichier contenant uniquement "\ r". Dans ce cas, je suppose que getline consommera tout le fichier, pensant qu'il s'agit d'une seule ligne!

.. et ce n'est même pas envisagé Unicode :-)

.. peut-être que Boost a une belle façon de consommer une ligne à la fois de n'importe quel type de fichier texte?

Modifier J'utilise ceci pour gérer les fichiers Windows, mais je pense toujours que je ne devrais pas avoir à le faire! Et cela ne se terminera pas pour les fichiers "\ r" uniquement.

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

Comme l'a souligné Neil, "le runtime C++ devrait traiter correctement quelle que soit la convention de fin de ligne pour votre plate-forme particulière."

Cependant, les gens déplacent des fichiers texte entre différentes plates-formes, ce qui n'est pas suffisant. Voici une fonction qui gère les trois fins de ligne ("\ r", "\ n" et "\ 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;
        }
    }
}

Et voici un programme de 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;
}
106
Johan Råde

Le runtime C++ doit traiter correctement quelle que soit la convention de fin pour votre plate-forme particulière. Plus précisément, ce code devrait fonctionner sur toutes les plateformes:

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

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

Bien sûr, si vous traitez des fichiers d'une autre plateforme, tous les paris sont désactivés.

Comme les deux plates-formes les plus courantes (Linux et Windows) terminent toutes les deux des lignes par un caractère de nouvelle ligne, Windows le précédant d'un retour chariot, vous pouvez examiner le dernier caractère de la chaîne line dans le code ci-dessus pour voir Si c'est \r et si tel est le cas, supprimez-le avant d'effectuer votre traitement spécifique à l'application.

Par exemple, vous pourriez vous fournir une fonction de style getline qui ressemble à ceci (non testé, utilisation des index, substr etc. uniquement à des fins pédagogiques):

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;
}
10
Neil Butterworth

Lisez-vous le fichier en mode BINAIRE ou en mode TEXTE ? En mode [[# #]] texte [~ # ~], le retour chariot/saut de ligne, CRLF , est interprété comme TEXTE fin de ligne ou caractère de fin de ligne, mais en BINAIRE vous récupérez seulement UN octet à la fois, ce qui signifie que l'un ou l'autre caractère DOIT être ignoré et laissé dans le tampon pour être récupéré comme un autre octet! Le retour chariot signifie, dans la machine à écrire, que la voiture de machine à écrire, où se trouve le bras d'impression, a atteint le bord droit du papier et est retournée au bord gauche. Il s'agit d'un modèle très mécanique, celui de la machine à écrire mécanique. Ensuite, le saut de ligne signifie que le rouleau de papier est tourné un peu vers le haut de sorte que le papier est en position pour commencer une autre ligne de frappe. Aussi loin que je me souvienne, l'un des chiffres bas de ASCII signifie déplacer vers la droite un caractère sans taper, le caractère mort, et bien sûr\b signifie retour arrière: reculer la voiture d'un caractère. De cette façon, vous pouvez ajouter des effets spéciaux, comme sous-jacent (type souligné), barré (type moins), approximer les différents accents, annuler (type X), sans avoir besoin d'un clavier étendu, simplement en ajustant la position de la voiture le long de la ligne avant en entrant le saut de ligne. Vous pouvez donc utiliser des voltages de taille ASCII pour contrôler automatiquement une machine à écrire sans ordinateur entre les deux. Lorsque la machine à écrire automatique est introduite, AUTOMATIQUE signifie qu'une fois que vous atteignez le bord le plus éloigné du papier, la voiture est renvoyée vers la gauche ET le saut de ligne appliqué, c'est-à-dire que la voiture est supposée être retournée automatiquement lorsque le rouleau monte! Vous n'avez donc pas besoin des deux caractères de contrôle, un seul, le\n, la nouvelle ligne ou le saut de ligne.

Cela n'a rien à voir avec la programmation mais ASCII est plus ancien et HEY! On dirait que certaines personnes ne pensaient pas quand ils ont commencé à faire des choses de texte! La plate-forme UNIX suppose une machine à écrire électrique automatique; le modèle Windows est plus complet et permet le contrôle des machines mécaniques, bien que certains caractères de contrôle deviennent de moins en moins utiles dans les ordinateurs, comme le caractère de cloche, 0x07 si je me souviens bien ... Certains textes oubliés doivent avoir été initialement capturés avec des caractères de contrôle pour les machines à écrire à commande électrique et cela a perpétué le modèle ...

En fait, la variation correcte serait d'inclure simplement le\r, saut de ligne, le retour chariot étant inutile, c'est-à-dire automatique, d'où:

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

serait la façon la plus correcte de gérer tous les types de fichiers. Notez cependant que\n en mode TEXTE est en fait la paire d'octets 0x0d 0x0a, mais 0x0d [[# # ~] est [~ # ~] seulement\r:\n inclut\r en mode TEXTE mais pas en mode BINAIRE , donc\n et\r\n sont équivalents ... ou devraient l'être. Il s'agit d'une confusion industrielle très basique en fait, l'inertie typique de l'industrie, car la convention est de parler de CRLF, sur TOUTES les plates-formes, puis de tomber dans différentes interprétations binaires. À strictement parler, les fichiers comprenant UNIQUEMENT 0x0d (retour chariot) comme étant\n (CRLF ou saut de ligne), sont malformés en TEXT mode (machine à écrire: il suffit de retourner la voiture et de barrer tout ...), et sont un format binaire non orienté ligne (soit\r soit\r\n signifiant orienté ligne) donc vous n'êtes pas censé lire comme du texte! Le code devrait échouer, peut-être avec un message utilisateur. Cela ne dépend pas seulement de l'OS, mais aussi de l'implémentation de la bibliothèque C, ajoutant à la confusion et aux variations possibles ... (en particulier pour les couches de traduction UNICODE transparentes ajoutant un autre point d'articulation pour les variations déroutantes).

Le problème avec l'extrait de code précédent (machine à écrire mécanique) est qu'il est très inefficace s'il n'y a pas de caractères\n après\r (texte de machine à écrire automatique). Ensuite, il suppose également le mode BINAIRE où la bibliothèque C est forcée d'ignorer les interprétations de texte (locales) et de donner les octets. Il ne devrait pas y avoir de différence dans les caractères de texte réels entre les deux modes, uniquement dans les caractères de contrôle, donc en général, la lecture BINAIRE est meilleure que TEXTE mode. Cette solution est efficace pour MODE BINAIRE fichiers texte typiques du système d'exploitation Windows indépendamment des variations de bibliothèque C, et inefficace pour les autres formats de texte de la plate-forme (y compris les traductions Web en texte). Si vous vous souciez de l'efficacité, la voie à suivre consiste à utiliser un pointeur de fonction, effectuez un test pour les contrôles de ligne\r vs\r\n comme vous le souhaitez, puis sélectionnez le meilleur code utilisateur getline dans le pointeur et appelez-le à partir de il.

Soit dit en passant, je me souviens avoir également trouvé des fichiers texte\r\r\n ... qui se traduisent par du texte sur deux lignes, comme le demandent encore certains consommateurs de texte imprimé.

7

Une solution serait de rechercher d'abord et de remplacer toutes les fins de ligne par '\ n' - comme par exemple Git le fait par défaut.

1
user2061057

À part écrire votre propre gestionnaire personnalisé ou utiliser une bibliothèque externe, vous n'avez pas de chance. La chose la plus simple à faire est de vérifier que line[line.length() - 1] n'est pas '\ r'. Sous Linux, cela est superflu car la plupart des lignes se retrouveront avec '\ n', ce qui signifie que vous perdrez un peu de temps si c'est dans une boucle. Sous Windows, cela est également superflu. Cependant, qu'en est-il des fichiers Mac classiques qui se terminent par '\ r'? std :: getline ne fonctionnerait pas pour ces fichiers sous Linux ou Windows car '\ n' et '\ r' '\ n' se terminent tous les deux par '\ n', éliminant ainsi la nécessité de vérifier '\ r'. De toute évidence, une telle tâche qui fonctionne avec ces fichiers ne fonctionnerait pas bien. Bien sûr, il existe alors les nombreux systèmes EBCDIC, quelque chose que la plupart des bibliothèques n'oseront pas aborder.

La vérification de "\ r" est probablement la meilleure solution à votre problème. La lecture en mode binaire vous permettrait de vérifier les trois fins de ligne communes ('\ r', '\ r\n' et '\ n'). Si vous ne vous souciez que de Linux et de Windows, car les terminaisons de ligne Mac à l'ancienne ne devraient pas durer plus longtemps, vérifiez uniquement '\ n' et supprimez le caractère de fin '\ r'.

1
user539810