Obtendo std :: ifstream para lidar com LF, CR e CRLF?


85

Especificamente, estou interessado em istream& getline ( istream& is, string& str );. Existe uma opção para o construtor ifstream dizer a ele para converter todas as codificações de nova linha para '\ n' nos bastidores? Eu quero ser capaz de ligar getlinee lidar com todas as terminações de linha normalmente.

Atualização : para esclarecer, eu quero ser capaz de escrever código que compile em quase qualquer lugar e receba entrada de quase qualquer lugar. Incluindo os arquivos raros que possuem '\ r' sem '\ n'. Minimizando a inconveniência para qualquer usuário do software.

É fácil contornar o problema, mas ainda estou curioso para saber a maneira correta, no padrão, de lidar com flexibilidade com todos os formatos de arquivo de texto.

getlinelê em uma linha completa, até um '\ n', em uma string. O '\ n' é consumido do stream, mas getline não o inclui na string. Tudo bem até agora, mas pode haver um '\ r' logo antes do '\ n' que é incluído na string.

Existem três tipos de terminações de linha vistas em arquivos de texto: '\ n' é a terminação convencional em máquinas Unix, '\ r' era (eu acho) usado em sistemas operacionais Mac antigos e o Windows usa um par, '\ r' seguido por '\ n'.

O problema é que getlinedeixa o '\ r' no final da string.

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 Obrigado a Neil por apontar que f.good()não era o que eu queria.!f.fail()é o que eu quero.

Posso removê-lo manualmente (veja a edição desta questão), o que é fácil para os arquivos de texto do Windows. Mas estou preocupado que alguém insira um arquivo contendo apenas '\ r'. Nesse caso, presumo que getline consumirá todo o arquivo, pensando que é uma única linha!

.. e isso nem mesmo considerando Unicode :-)

.. talvez Boost tenha uma boa maneira de consumir uma linha de cada vez de qualquer tipo de arquivo de texto?

Editar Estou usando isso para lidar com os arquivos do Windows, mas ainda acho que não deveria! E isso não bifurcará para os arquivos somente '\ r'.

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

2
\ n significa nova linha de qualquer forma que seja apresentada no sistema operacional atual. A biblioteca cuida disso. Mas para que isso funcione, um programa compilado em windows deve ler arquivos de texto de windows, um programa compilado em unix, arquivos de texto em unix etc.
George Kastrinis

1
@George, embora esteja compilando em uma máquina Linux, às vezes estou usando arquivos de texto que vieram originalmente de uma máquina Windows. Posso lançar meu software (uma pequena ferramenta para análise de rede), e quero ser capaz de dizer aos usuários que eles podem alimentar em quase qualquer momento o arquivo de texto (semelhante ao ASCII).
Aaron McDaid


1
Observe que if (f.good ()) não faz o que você parece pensar que faz.

1
@JonathanMee: Pode ter sido como este . Talvez.
Lightness Races in Orbit

Respostas:


111

Como Neil apontou, "o tempo de execução C ++ deve lidar corretamente com qualquer convenção de finalização de linha para sua plataforma particular."

No entanto, as pessoas movem arquivos de texto entre plataformas diferentes, então isso não é bom o suficiente. Aqui está uma função que lida com todas as três terminações de linha ("\ 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;
        }
    }
}

E aqui está um programa de teste:

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: Eu atualizei o código seguindo a sugestão de Bo Pessoas stackoverflow.com/questions/9188126/… e executei alguns testes. Agora tudo funciona como deveria.
Johan Råde

1
@Thomas Weller: O construtor e o destruidor da sentinela são executados. Eles fazem coisas como sincronização de thread, ignorando espaços em branco e atualizando o estado do fluxo.
Johan Råde

1
No caso do EOF, qual a finalidade de verificar se testá vazio antes de definir o eofbit. Essa parte não deveria ser definida independentemente de outros caracteres terem sido lidos?
Yay295

1
Yay295: O sinalizador eof deve ser definido, não quando você chegar ao final da última linha, mas quando você tentar ler além da última linha. A verificação garante que isso aconteça quando a última linha não tiver EOL. (Tente remover a verificação e execute o programa de teste no arquivo de texto em que a última linha não tem EOL, e você verá.)
Johan Råde

3
Isso também lê uma última linha vazia, que não é o comportamento std::get_lineque ignora uma última linha vazia. Usei o seguinte código no caso eof para emular o std::get_linecomportamento:is.setstate(std::ios::eofbit); if (t.empty()) is.setstate(std::ios::badbit); return is;
Patrick Roocks

11

O tempo de execução C ++ deve lidar corretamente com qualquer convenção de linha final para sua plataforma específica. Especificamente, este código deve funcionar em todas as plataformas:

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

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

Claro, se você estiver lidando com arquivos de outra plataforma, todas as apostas estão canceladas.

Como as duas plataformas mais comuns (Linux e Windows) terminam as linhas com um caractere de nova linha, com o Windows precedendo-o com um retorno de carro, você pode examinar o último caractere da linestring no código acima para ver se é\r e se é remova-o antes de fazer o processamento específico do aplicativo.

Por exemplo, você pode fornecer a si mesmo uma função de estilo getline parecida com esta (não testada, uso de índices, substr etc. apenas para fins 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;
}

9
A questão é sobre como lidar com arquivos de outra plataforma.
Lightness Races in Orbit

4
@ Neil, esta resposta ainda não é suficiente. Se eu apenas quisesse lidar com CRLFs, não teria vindo para StackOverflow. O verdadeiro desafio é lidar com os arquivos que têm apenas '\ r'. Eles são muito raros hoje em dia, agora que o MacOS se aproximou do Unix, mas não quero assumir que nunca serão alimentados pelo meu software.
Aaron McDaid

1
@Aaron bem, se você quer ser capaz de lidar com QUALQUER COISA, você tem que escrever seu próprio código para fazê-lo.

4
Deixei claro na minha pergunta desde o início que é fácil contornar isso, o que implica que estou disposto e posso fazer isso. Eu perguntei sobre isso porque parece ser uma pergunta muito comum, e há uma variedade de formatos de arquivo de texto. Eu presumi / esperava que o comitê de padrões C ++ tivesse integrado isso. Esta foi a minha pergunta.
Aaron McDaid

1
@ Neil, acho que há outro problema que esquecemos. Mas primeiro, eu aceito que é prático para mim identificar um pequeno número de formatos a serem suportados. Portanto, quero um código que será compilado no Windows e Linux e que funcionará com qualquer um dos formatos. Você safegetlineé uma parte importante de uma solução. Mas se este programa estiver sendo compilado no Windows, também precisarei abrir o arquivo no formato binário? Os compiladores do Windows (em modo de texto) permitem que '\ n' se comporte como '\ r' '\ n'? ifstream f("f.txt", ios_base :: binary | ios_base::in );
Aaron McDaid

8

Você está lendo o arquivo em modo BINÁRIO ou TEXTO ? No modo TEXTO , o par retorno de carro / alimentação de linha, CRLF , é interpretado como TEXTO de fim de linha ou caractere de fim de linha, mas em BINÁRIO você busca apenas UM byte por vez, o que significa que qualquer um dos caracteres DEVEser ignorado e deixado no buffer para ser obtido como outro byte! Retorno de carro significa, na máquina de escrever, que o carro da máquina de escrever, onde está o braço de impressão, atingiu a borda direita do papel e voltou à borda esquerda. Este é um modelo muito mecânico, o da máquina de escrever mecânica. Em seguida, o avanço de linha significa que o rolo de papel é girado um pouco para cima, de forma que o papel esteja em posição de iniciar outra linha de digitação. Pelo que me lembro, um dos dígitos mais baixos em ASCII significa mover um caractere para a direita sem digitar, o caractere morto e, claro, \ b significa retroceder: mover o carro um caractere para trás. Dessa forma, você pode adicionar efeitos especiais, como subjacente (digite sublinhado), tachado (digite menos), acentos diferentes aproximados, cancelar (digite X), sem a necessidade de um teclado estendido, apenas ajustando a posição do carro ao longo da linha antes de entrar na alimentação de linha. Portanto, você pode usar voltagens ASCII de byte para controlar automaticamente uma máquina de escrever sem um computador no meio. Quando a máquina de escrever automática é introduzida,AUTOMÁTICO significa que uma vez que você atinge a borda mais distante do papel, o carro é retornado para a esquerda E o avanço de linha é aplicado, ou seja, o carro é assumido como retornado automaticamente conforme o rolo sobe! Portanto, você não precisa de ambos os caracteres de controle, apenas um, o \ n, nova linha ou alimentação de linha.

Isso não tem nada a ver com programação, mas ASCII é mais antigo e HEY! parece que algumas pessoas não estavam pensando quando começaram a fazer coisas de texto! A plataforma UNIX assume uma máquina de tipo elétrica automática; o modelo do Windows é mais completo e permite o controle de máquinas mecânicas, embora alguns caracteres de controle se tornem cada vez menos úteis em computadores, como o caractere de sino, 0x07 se bem me lembro ... Alguns textos esquecidos devem ter sido originalmente capturados com caracteres de controle para máquinas de escrever eletricamente controladas e perpetuou o modelo ...

Na verdade, a variação correta seria incluir apenas o \ r, alimentação de linha, o retorno do carro sendo desnecessário, ou seja, automático, portanto:

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

seria a maneira mais correta de lidar com todos os tipos de arquivos. Observe, entretanto, que \ n no modo TEXT é na verdade o par de bytes 0x0d 0x0a, mas 0x0d IS apenas \ r: \ n inclui \ r no modo TEXT , mas não no BINARY , então \ ne \ r \ n são equivalentes ... ou deveria estar. Esta é uma confusão muito básica da indústria, na verdade, inércia típica da indústria, já que a convenção é falar de CRLF, em TODAS as plataformas, então cair em diferentes interpretações binárias. A rigor, os arquivos que incluem SOMENTE 0x0d (retorno de carro) como sendo \ n (CRLF ou alimentação de linha) estão malformados em TEXTmodo (máquina de escrever: basta retornar o carro e tachar tudo ...), e são um formato binário não orientado por linha (\ r ou \ r \ n significando orientado por linha) então você não deve ler como texto! O código deve falhar, talvez com alguma mensagem do usuário. Isso não depende apenas do sistema operacional, mas também da implementação da biblioteca C, aumentando a confusão e as possíveis variações ... (particularmente para camadas de tradução UNICODE transparentes adicionando outro ponto de articulação para variações confusas).

O problema com o trecho de código anterior (máquina de escrever mecânica) é que ele é muito ineficiente se não houver \ n caracteres após \ r (texto de máquina de escrever automática). Em seguida, também assume o modo BINÁRIO , onde a biblioteca C é forçada a ignorar as interpretações de texto (local) e fornecer os bytes absolutos. Não deve haver diferença nos caracteres de texto reais entre os dois modos, apenas nos caracteres de controle, portanto, de modo geral, ler BINÁRIO é melhor do que o modo TEXTO . Esta solução é eficiente para BINARYmodo arquivos de texto típicos do sistema operacional Windows, independentemente das variações da biblioteca C, e ineficiente para outros formatos de texto de plataforma (incluindo traduções da web em texto). Se você se preocupa com a eficiência, o caminho a percorrer é usar um ponteiro de função, fazer um teste para \ r vs \ r \ n controles de linha da maneira que quiser, então selecione o melhor código de usuário getline no ponteiro e invoque-o de isto.

A propósito, lembro que encontrei alguns \ r \ r \ n arquivos de texto também ... o que se traduz em texto de linha dupla, assim como ainda é exigido por alguns consumidores de texto impresso.


+1 para "ios :: binary" - às vezes, você realmente quer ler o arquivo como ele está (por exemplo, para calcular uma soma de verificação, etc.) sem que o tempo de execução altere os finais de linha.
Matthias

2

Uma solução seria primeiro pesquisar e substituir todas as terminações de linha por '\ n' - como, por exemplo, o Git faz por padrão.


1

Além de escrever seu próprio manipulador personalizado ou usar uma biblioteca externa, você está sem sorte. A coisa mais fácil a fazer é verificar se line[line.length() - 1]não é '\ r'. No Linux, isso é supérfluo, pois a maioria das linhas termina com '\ n', o que significa que você perderá um bom tempo se ocorrer um loop. No Windows, isso também é supérfluo. No entanto, e os arquivos clássicos do Mac que terminam em '\ r'? std :: getline não funcionaria para esses arquivos no Linux ou Windows porque '\ n' e '\ r' '\ n' ambos terminam com '\ n', eliminando a necessidade de verificar por '\ r'. Obviamente, essa tarefa que funciona com esses arquivos não funcionaria bem. Claro, existem os numerosos sistemas EBCDIC, algo que a maioria das bibliotecas não se atreverá a enfrentar.

Verificar '\ r' é provavelmente a melhor solução para o seu problema. A leitura no modo binário permitiria a você verificar todas as três terminações de linha comuns ('\ r', '\ r \ n' e '\ n'). Se você se preocupa apenas com o Linux e o Windows, já que as terminações de linha do Mac antigo não devem durar muito mais tempo, verifique apenas '\ n' e remova o caractere '\ r' à direita.


0

Se for conhecido quantos itens / números cada linha tem, pode-se ler uma linha com, por exemplo, 4 números como

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

Isso também funciona com outras terminações de linha.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.