¿Obteniendo std :: ifstream para manejar LF, CR y CRLF?

85

Específicamente me interesa istream& getline ( istream& is, string& str );. ¿Existe una opción para el constructor de ifstream para decirle que convierta todas las codificaciones de nueva línea a '\ n' bajo el capó? Quiero poder llamar getliney que maneje con gracia todos los finales de línea.

Actualización : Para aclarar, quiero poder escribir código que se compile casi en cualquier lugar y que tome entradas de casi cualquier lugar. Incluyendo los archivos raros que tienen '\ r' sin '\ n'. Minimizar las molestias para los usuarios del software.

Es fácil solucionar el problema, pero todavía tengo curiosidad sobre la forma correcta, en el estándar, de manejar de manera flexible todos los formatos de archivos de texto.

getlinelee en una línea completa, hasta un '\ n', en una cadena. El '\ n' se consume de la secuencia, pero getline no lo incluye en la cadena. Eso está bien hasta ahora, pero puede haber una '\ r' justo antes de la '\ n' que se incluye en la cadena.

Hay tres tipos de terminaciones de línea que se ven en los archivos de texto: '\ n' es la terminación convencional en las máquinas Unix, '\ r' se usó (creo) en los sistemas operativos antiguos de Mac y Windows usa un par, '\ r' seguido de '\ n'.

El problema es que getlinedeja la '\ r' al final de la cadena.

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

Editar Gracias a Neil por señalar que eso f.good()no es lo que quería. !f.fail()es lo que quiero.

Yo mismo puedo eliminarlo manualmente (ver edición de esta pregunta), lo cual es fácil para los archivos de texto de Windows. Pero me preocupa que alguien introduzca un archivo que contenga solo '\ r'. En ese caso, supongo que getline consumirá todo el archivo, ¡pensando que es una sola línea!

.. y eso ni siquiera está considerando Unicode :-)

.. ¿Quizás Boost tiene una buena manera de consumir una línea a la vez desde cualquier tipo de archivo de texto?

Editar Estoy usando esto, para manejar los archivos de Windows, ¡pero todavía siento que no debería tener que hacerlo! Y esto no se bifurcará para los archivos solo '\ r'.

if(!line.empty() && *line.rbegin() == '\r') {
    line.erase( line.length()-1, 1);
}
Aaron McDaid
fuente
2
\ n significa nueva línea de cualquier forma que se presente en el sistema operativo actual. La biblioteca se encarga de eso. Pero para que eso funcione, un programa compilado en Windows debería leer archivos de texto de Windows, un programa compilado en Unix, archivos de texto de Unix, etc.
George Kastrinis
1
@George, aunque estoy compilando en una máquina Linux, a veces estoy usando archivos de texto que provienen originalmente de una máquina Windows. Podría lanzar mi software (una pequeña herramienta para el análisis de redes) y quiero poder decirles a los usuarios que pueden ingresar casi en cualquier momento de un archivo de texto (similar a ASCII).
Aaron McDaid
1
Tenga en cuenta que si (f.good ()) no hace lo que parece pensar que hace.
1
@JonathanMee: Puede haber sido como este . Tal vez.
Lightness Races in Orbit

Respuestas:

111

Como señaló Neil, "el tiempo de ejecución de C ++ debería tratar correctamente cualquier convención de final de línea para su plataforma en particular".

Sin embargo, la gente mueve archivos de texto entre diferentes plataformas, por lo que no es suficiente. Aquí hay una función que maneja los tres finales de línea ("\ r", "\ n" y "\ 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;
        }
    }
}

Y aquí hay un programa de prueba:

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
fuente
1
@Miek: He actualizado el código siguiendo la sugerencia de Bo Persons stackoverflow.com/questions/9188126/… y ejecuté algunas pruebas. Ahora todo funciona como debería.
Johan Råde
1
@Thomas Weller: Se ejecutan el constructor y el destructor del centinela. Estos hacen cosas como la sincronización de subprocesos, omitir espacios en blanco y actualizar el estado de la transmisión.
Johan Råde
1
En el caso de EOF, ¿cuál es el propósito de verificar que testá vacío antes de configurar el eofbit? ¿No debería establecerse ese bit independientemente de que se hayan leído otros caracteres?
Yay295
1
Yay295: El indicador eof debe establecerse, no cuando llegue al final de la última línea, sino cuando intente leer más allá de la última línea. La verificación asegura que esto suceda cuando la última línea no tiene EOL. (Intente eliminar el cheque y luego ejecute el programa de prueba en el archivo de texto donde la última línea no tiene EOL, y verá).
Johan Råde
3
Esto también lee una última línea vacía, que no es el comportamiento del std::get_linecual ignora una última línea vacía. Usé el siguiente código en el caso eof para emular el std::get_linecomportamiento:is.setstate(std::ios::eofbit); if (t.empty()) is.setstate(std::ios::badbit); return is;
Patrick Roocks
11

El tiempo de ejecución de C ++ debe tratar correctamente con cualquier convención de línea final para su plataforma en particular. Específicamente, este código debería funcionar en todas las plataformas:

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

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

Por supuesto, si está tratando con archivos de otra plataforma, todas las apuestas están canceladas.

Como las dos plataformas más comunes (Linux y Windows) terminan las líneas con un carácter de nueva línea, con Windows precediéndola con un retorno de carro, puede examinar el último carácter de la linecadena en el código anterior para ver si es así \ry si es así elimínelo antes de realizar el procesamiento específico de su aplicación.

Por ejemplo, podría proporcionarse una función de estilo getline que se parezca a esto (no probado, uso de índices, substr, etc., solo con fines pedagógicos):

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

fuente
9
La pregunta es acerca de cómo hacer frente a los archivos desde otra plataforma.
Lightness Races in Orbit
4
@Neil, esta respuesta aún no es suficiente. Si solo quisiera manejar CRLF, no habría venido a StackOverflow. El verdadero desafío es manejar los archivos que solo tienen '\ r'. Son bastante raros hoy en día, ahora que MacOS se ha acercado a Unix, pero no quiero asumir que nunca se incorporarán a mi software.
Aaron McDaid
1
@Aaron bueno, si quieres poder manejar CUALQUIER COSA tienes que escribir tu propio código para hacerlo.
4
Dejé claro en mi pregunta desde el principio que es fácil solucionar esto, lo que implica que estoy dispuesto y puedo hacerlo. Pregunté sobre esto porque parece ser una pregunta muy común y hay una variedad de formatos de archivos de texto. Supuse / esperaba que el comité de estándares de C ++ hubiera incorporado esto. Esta era mi pregunta.
Aaron McDaid
1
@Neil, creo que hay otro tema que yo / nosotros hemos olvidado. Pero primero, acepto que es práctico para mí identificar una pequeña cantidad de formatos que se admitirán. Por lo tanto, quiero un código que se compile en Windows y Linux y que funcione con cualquier formato. Tu safegetlinees una parte importante de una solución. Pero si este programa se está compilando en Windows, ¿también tendré que abrir el archivo en formato binario? ¿Los compiladores de Windows (en modo texto) permiten que '\ n' se comporte como '\ r' '\ n'? ifstream f("f.txt", ios_base :: binary | ios_base::in );
Aaron McDaid
8

¿Está leyendo el archivo en modo BINARIO o TEXTO ? En el modo TEXT , el par retorno de carro / avance de línea, CRLF , se interpreta como un carácter de fin de línea o fin de línea de TEXTO , pero en BINARIO se obtiene solo UN byte a la vez, lo que significa que cualquiera de los dos caracteres DEBEser ignorado y dejado en el búfer para ser recuperado como otro byte. Retorno de carro significa, en la máquina de escribir, que el carro de la máquina de escribir, donde se encuentra el brazo de impresión, ha llegado al borde derecho del papel y regresa al borde izquierdo. Este es un modelo muy mecánico, el de la máquina de escribir mecánica. Luego, el avance de línea significa que el rollo de papel se gira un poco hacia arriba para que el papel esté en posición para comenzar otra línea de escritura. Tan rápido como recuerdo, uno de los dígitos bajos en ASCII significa mover hacia la derecha un carácter sin escribir, el carácter muerto y, por supuesto, \ b significa retroceso: mover el automóvil un carácter hacia atrás. De esa manera, puede agregar efectos especiales, como subyacente (escriba subrayado), tachado (escriba menos), aproximadamente acentos diferentes, cancelar (escriba X), sin necesidad de un teclado extendido, simplemente ajustando la posición del automóvil a lo largo de la línea antes de ingresar al avance de línea. Por lo tanto, puede usar voltajes ASCII de tamaño byte para controlar automáticamente una máquina de escribir sin una computadora en el medio. Cuando se introduce la máquina de escribir automática,AUTOMÁTICO significa que una vez que llega al borde más lejano del papel, el carro regresa a la izquierda Y se aplica el avance de línea, es decir, se asume que el carro regresa automáticamente cuando el rollo sube. Por lo tanto, no necesita ambos caracteres de control, solo uno, \ n, nueva línea o salto de línea.

Esto no tiene nada que ver con la programación, pero ASCII es más antiguo y ¡HEY! ¡Parece que algunas personas no estaban pensando cuando empezaron a escribir mensajes de texto! La plataforma UNIX asume una máquina de tipos automática eléctrica; el modelo de Windows es más completo y permite el control de máquinas mecánicas, aunque algunos caracteres de control se vuelven cada vez menos útiles en las computadoras, como el carácter de campana, 0x07 si mal no recuerdo ... Algunos textos olvidados deben haber sido capturados originalmente con caracteres de control para máquinas de escribir controladas eléctricamente y perpetuó el modelo ...

En realidad, la variación correcta sería simplemente incluir \ r, salto de línea, siendo innecesario el retorno de carro, es decir, automático, por lo tanto:

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

sería la forma más correcta de manejar todo tipo de archivos. Sin embargo , tenga en cuenta que \ n en modo TEXTO es en realidad el par de bytes 0x0d 0x0a, pero 0x0d ES simplemente \ r: \ n incluye \ r en modo TEXTO pero no en BINARIO , por lo que \ n y \ r \ n son equivalentes ... o debiera ser. Esta es una confusión de la industria muy básica en realidad, la inercia típica de la industria, ya que la convención es hablar de CRLF, en TODAS las plataformas, y luego caer en diferentes interpretaciones binarias. Estrictamente hablando, los archivos que incluyen SÓLO 0x0d (retorno de carro) como \ n (CRLF o salto de línea), tienen un formato incorrecto en TEXTOmodo (máquina de escribir: simplemente devuelva el automóvil y tache todo ...), y son un formato binario no orientado a líneas (ya sea \ r o \ r \ n, es decir, orientado a líneas), por lo que no debe leer como texto. El código debería fallar tal vez con algún mensaje de usuario. Esto no depende solo del sistema operativo, sino también de la implementación de la biblioteca C, lo que aumenta la confusión y las posibles variaciones ... (particularmente para las capas de traducción transparentes de UNICODE que agregan otro punto de articulación para variaciones confusas).

El problema con el fragmento de código anterior (máquina de escribir mecánica) es que es muy ineficaz si no hay \ n caracteres después de \ r (texto de máquina de escribir automática). Luego, también asume el modo BINARIO donde la biblioteca C se ve obligada a ignorar las interpretaciones de texto (configuración regional) y regalar los bytes. No debería haber diferencia en los caracteres de texto reales entre ambos modos, solo en los caracteres de control, por lo que, en general, leer BINARIO es mejor que el modo TEXTO . Esta solución es eficiente para BINARYmodo archivos de texto típicos del sistema operativo Windows, independientemente de las variaciones de la biblioteca C, e ineficaz para otros formatos de texto de plataforma (incluidas las traducciones web a texto). Si le preocupa la eficiencia, el camino a seguir es usar un puntero de función, hacer una prueba para los controles de línea \ r vs \ r \ n de la forma que desee, luego seleccionar el mejor código de usuario de getline en el puntero e invocarlo desde eso.

Por cierto, recuerdo que también encontré algunos archivos de texto \ r \ r \ n ... que se traducen en texto de doble línea, tal como todavía lo requieren algunos consumidores de texto impreso.

Danilo J. Bonsignore
fuente
+1 para "ios :: binary": a veces, realmente desea leer el archivo tal como está (por ejemplo, para calcular una suma de comprobación, etc.) sin que el tiempo de ejecución cambie los finales de línea.
Matthias
2

Una solución sería buscar primero y reemplazar todos los finales de línea por '\ n', como por ejemplo, lo hace Git por defecto.

usuario2061057
fuente
1

Aparte de escribir su propio controlador personalizado o usar una biblioteca externa, no tiene suerte. Lo más fácil de hacer es verificar para asegurarse de que line[line.length() - 1]no sea '\ r'. En Linux, esto es superfluo ya que la mayoría de las líneas terminarán con '\ n', lo que significa que perderá un poco de tiempo si está en un bucle. En Windows, esto también es superfluo. Sin embargo, ¿qué pasa con los archivos clásicos de Mac que terminan en '\ r'? std :: getline no funcionaría para esos archivos en Linux o Windows porque '\ n' y '\ r' '\ n' ambos terminan con '\ n', eliminando la necesidad de buscar '\ r'. Obviamente, una tarea que funcione con esos archivos no funcionaría bien. Por supuesto, existen numerosos sistemas EBCDIC, algo que la mayoría de las bibliotecas no se atreverán a abordar.

Comprobar '\ r' es probablemente la mejor solución a su problema. Leer en modo binario le permitiría verificar los tres finales de línea comunes ('\ r', '\ r \ n' y '\ n'). Si solo le interesan Linux y Windows, ya que los finales de línea de Mac de estilo antiguo no deberían durar mucho más tiempo, verifique solo '\ n' y elimine el carácter '\ r' final.


fuente
0

Si se sabe cuántos elementos / números tiene cada línea, se podría leer una línea con, por ejemplo, 4 números como

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

Esto también funciona con otros finales de línea.

Martin Thümmel
fuente