获取std :: ifstream处理LF,CR和CRLF?


85

我特别感兴趣istream& getline ( istream& is, string& str );。ifstream构造函数是否可以选择告诉其将所有换行编码转换为'\ n'?我希望能够打电话getline并让它优雅地处理所有行尾。

更新:澄清一下,我希望能够编写几乎可以在任何地方编译的代码,并且可以从几乎任何地方获取输入。包括带有'\ r'而不带有'\ n'的稀有文件。最大限度地减少软件用户的不便。

解决该问题很容易,但是我仍然对标准中灵活处理所有文本文件格式的正确方法感到好奇。

getline将一个完整的行读取到一个字符串中,直到一个“ \ n”。'\ n'是从流中使用的,但是getline不在字符串中包含它。到目前为止还可以,但是在包含在字符串中的“ \ n”之前可能有一个“ \ r”。

三种类型的行结尾的文本文件中看到:“\ n”是在Unix机器上,“\ r”的传统结局是在旧的Mac操作系统使用,Windows使用一对,“\ r”(我认为)后跟“ \ n”。

问题在于,getline将'\ r'留在字符串的末尾。

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

编辑感谢Neil指出的f.good()不是我想要的。!f.fail()是我想要的

我可以自己手动删除它(请参阅此问题的编辑),这对于Windows文本文件来说很容易。但是我担心有人会提供仅包含'\ r'的文件。在这种情况下,我认为getline将消耗整个文件,以为这是一行!

..甚至都没有考虑Unicode :-)

..也许Boost有一个不错的方法来一次消耗任何文本文件类型的一行?

编辑我正在使用它来处理Windows文件,但我仍然觉得我不必这样做!而且,这不会为“ \ r”专用文件进行分叉。

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

2
\ n表示以当前操作系统中显示的任何方式换行。图书馆负责。但对于工作,在Windows编译的程序应该从UNIX等读取窗口的文本文件,可以在UNIX编译的程序,文本文件
乔治Kastrinis

1
@George,即使我在Linux机器上编译,有时我也会使用最初来自Windows机器的文本文件。我可能会发布我的软件(用于网络分析的小工具),并且希望能够告诉用户他们几乎可以在任何时间输入(类似ASCII的)文本文件。
亚伦·麦克戴德


1
请注意,if(f.good())并没有执行您似乎认为的操作。

1
@JonathanMee:这可能是像这样。也许。
Lightness Races in Orbit 2015年

Answers:


111

正如Neil指出的那样,“ C ++运行时应正确处理特定平台的行尾约定。”

但是,人们确实会在不同平台之间移动文本文件,因此这还不够好。这是一个处理所有三个行尾(“ \ r”,“ \ n”和“ \ 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;
        }
    }
}

这是一个测试程序:

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:我按照Bo Persons建议stackoverflow.com/questions/9188126/…更新了代码,并进行了一些测试。现在一切正常。
约翰·拉德(JohanRåde)2012年

1
@Thomas Weller:哨兵的构造函数和析构函数已执行。它们执行诸如线程同步,跳过空白和更新流状态之类的事情。
JohanRåde2015年

1
在EOF情况下,t设置eofbit之前检查是否为空的目的是什么。不管是否已读入其他字符,都不应设置该位吗?
Yay295

1
Yay295:应该设置eof标志,而不是在到达最后一行的末尾时,而是在尝试读取最后一行以外的值时,将其设置。检查可确保当最后一行没有EOL时会发生这种情况。(尝试删除检查,然后在文本文件中运行测试程序,其中最后一行没有EOL,您将看到。)
JohanRåde2015年

3
这还将读取最后一行为空,这不是其行为std::get_line会忽略最后一行为空。我在eof案例中使用了以下代码来模拟std::get_line行为:is.setstate(std::ios::eofbit); if (t.empty()) is.setstate(std::ios::badbit); return is;
Patrick Roocks

11

C ++运行时应正确处理针对特定平台的任何终端约定。具体来说,此代码应在所有平台上均适用:

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

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

当然,如果您正在处理来自另一个平台的文件,则所有选择都将关闭。

由于两个最常见的平台(Linux和Windows)都以换行符终止行,而Windows以回车符结尾,因此您可以检查line上述代码中字符串的最后一个字符,以查看是否为\r如果是这样在执行特定于应用程序的处理之前,先将其删除。

例如,您可以为自己提供一个类似于以下内容的getline样式函数(未经测试,使用索引,substr等仅用于教学目的):

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
问题是关于如何处理来自另一个平台的文件。
Lightness Races in Orbit

4
@Neil,这个答案还不够。如果我只是想处理CRLF,我就不会来StackOverflow。真正的挑战是处理只有'\ r'的文件。如今,它们非常罕见,因为MacOS已经接近Unix,但是我不想假设它们永远也不会被提供给我的软件。
亚伦·麦克戴德

1
@Aaron好吧,如果您希望能够处理任何事情,则必须编写自己的代码才能做到。

4
我从一开始就在我的问题中明确指出,解决此问题很容易,这意味着我愿意并且能够这样做。我之所以问这个问题,是因为这似乎是一个常见的问题,并且有多种文本文件格式。我假设/希望C ++标准委员会已将其内置。这是我的问题。
亚伦·麦克戴德

1
@Neil,我想我/我们忘记了另一个问题。但是首先,我接受我认为确定少数要支持的格式是可行的。因此,我想要可以在Windows和Linux上编译并且可以使用任何一种格式的代码。您safegetline是解决方案的重要组成部分。但是,如果正在Windows上编译此程序,是否还需要以二进制格式打开文件?Windows编译器(在文本模式下)是否允许'\ n'的行为类似于'\ r'\ n'? ifstream f("f.txt", ios_base :: binary | ios_base::in );
亚伦·麦克戴德

8

您是以BINARY还是TEXT模式读取文件?在TEXT模式下,回车/换行对CRLF被解释为TEXT行尾或行尾字符,但是在BINARY中,一次只能获取一个字节,这意味着任何一个字符必须被忽略并留在缓冲区中作为另一个字节获取!回车是指在打字机中打印臂所在的打字机车已经到达纸张的右边缘并返回到左边缘。这是机械打字机的机械模型。然后换行意味着纸卷稍微向上旋转,因此纸张就位以开始另一行打字。就我所记得的fas而言,ASCII中的低位数字之一表示不键入而移到右边的一个字符,即死字符,当然\ b表示退格:将汽车向后移一个字符。这样,您可以添加特殊效果,例如基础(下划线类型),删除线(负号类型),近似不同的重音符号,抵消(X型),而无需扩展键盘,只需在进入换行之前调整汽车沿线的位置即可。因此,您可以使用字节大小的ASCII电压来自动控制一台打字机,而无需中间的计算机。引进自动打字机后,AUTOMATIC(自动)意味着,一旦您到达纸张的最远边缘,汽车就会返回到左侧并应用换行,也就是说,假定纸卷向上移动时,汽车会自动返回!因此,您不需要两个控制字符,只需一个\ n,换行符或换行符。

这与编程无关,但是ASCII较旧,嘿!看起来有些人在开始做文字工作时没有思考!UNIX平台假定为一台自动打字机。Windows模型更加完整,可以控制机械,尽管某些控制字符在计算机中的用途越来越少,例如铃铛字符,0x07(如果我记得很好的话)...某些被遗忘的文本必须最初是用控制字符捕获的电动打字机,它使模型永存...

实际上,正确的变化是只包含\ r,换行符,不需要回车,即自动回车,因此:

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

将是处理所有类型文件的最正确方法。然而要注意\ n的文本模式实际上是字节对0X0D 0X0A,但0X0D IS只是\ R:\ n包括在\ r TEXT模式,而不是在BINARY,所以\ n和\ r \ n为相当于...或应该。实际上,这是一个非常基本的行业混乱,是典型的行业惯性,因为在所有平台上都将CRLF称为惯例,然后陷入不同的二进制解释中。严格来说,\ n(CRLF或换行)中包含0x0d(回车)的文件格式错误。TEXT模式(打字机机器:只需返回汽车并删除所有内容...),并且是非行二进制格式(\ r或\ r \ n表示行格式),因此您不应将其阅读为文本!该代码应该失败,可能带有一些用户消息。这不仅取决于OS,还取决于C库的实现,这增加了混乱和可能的变化……(尤其是对于透明的UNICODE转换层,还增加了另一个使混乱的变化清晰明了的点)。

先前的代码片段(机械打字机)的问题在于,如果\ r(自动打字机文本)后没有\ n字符,则效率很低。然后,它也假定为BINARY模式,在该模式下,C库被迫忽略文本解释(语言环境),并释放纯粹的字节。两种模式之间的实际文本字符应该没有区别,只有控制字符之间没有区别,因此一般而言,阅读BINARY优于TEXT模式。该解决方案对于BINARY是有效的模式,典型的Windows OS文本文件独立于C库的变体,对于其他平台文本格式(包括将网页翻译成文本)的效率低下。如果您关心效率,最好的方法是使用函数指针,以自己喜欢的方式对\ r vs \ r \ n线控件进行测试,然后在指针中选择最佳的getline用户代码并从中调用它。

顺带一提,我还记得我也找到了一些\ r \ r \ n文本文件...就像一些印刷文本使用者所需要的那样,它可以转换为双行文本。


+1为“ ios :: binary”-有时,您实际上想按原样读取文件(例如,用于计算校验和等),而运行时不更改行尾。
Matthias

2

一种解决方案是首先搜索并将所有行尾替换为'\ n'-就像Git默认情况下一样。


1

除了编写自己的自定义处理程序或使用外部库以外,您还很不走运。最简单的方法是检查以确保line[line.length() - 1]不是'\ r'。在Linux上,这是多余的,因为大多数行都以'\ n'结尾,这意味着如果这是循环的话,您将浪费相当多的时间。在Windows上,这也是多余的。但是,以'\ r'结尾的经典Mac文件呢?std :: getline不适用于Linux或Windows上的那些文件,因为'\ n'和'\ r''\ n'都以'\ n'结尾,从而无需检查'\ r'。显然,使用这些文件的任务无法正常工作。当然,存在众多的EBCDIC系统,大多数图书馆都不敢解决。

检查“ \ r”可能是解决您问题的最佳方法。以二进制模式读取将允许您检查所有三个公共行的结尾('\ r','\ r \ n'和'\ n')。如果您只关心Linux和Windows,因为老式Mac行尾不应再出现太久,请仅检查'\ n'并删除尾随的'\ r'字符。


0

如果知道每行有多少个项目/编号,则可以读取一行,例如4个数字作为

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

这也适用于其他行尾。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.