获取 std :: ifstream 来处理 LF、CR 和 CRLF

Getting std :: ifstream to handle LF, CR, and CRLF?

本文关键字:CR LF CRLF 处理 std ifstream 获取      更新时间:2023-10-16

具体来说,我对istream& getline ( istream& is, string& str );感兴趣。ifstream 构造函数是否有一个选项告诉它在后台将所有换行符编码转换为 ''?我希望能够调用getline并让它优雅地处理所有行尾。

更新:澄清一下,我希望能够编写几乎可以在任何地方编译的代码,并且几乎可以从任何地方获取输入。包括具有"\r"而不带""的罕见文件。尽量减少对软件任何用户的不便。

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

getline以整行(最多为"")读取为字符串。""从流中使用,但 getline 不会将其包含在字符串中。到目前为止这很好,但是在包含在字符串中的""之前可能有一个"\r"。

在文本文件中可以看到三种类型的行尾:""是Unix机器上的常规结尾,"\r"(我认为)用于旧的Mac操作系统,Windows使用一对,"\r"后跟""。

问题是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.
}

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

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

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

.. 也许 Boost 有一种很好的方法可以从任何文本文件类型一次消耗一行?

编辑我正在使用它来处理 Windows 文件,但我仍然觉得我不应该这样做!这不会为仅"\r"文件分叉。

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

正如 Neil 指出的那样,"C++运行时应该正确处理特定平台的行结束约定。

但是,人们确实在不同的平台之间移动文本文件,因此这还不够好。下面是一个处理所有三个行尾("\r"、""和"\r")的函数:

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

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

这与编程无关,但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);
...

将是处理所有类型的文件的最正确方法。但请注意,文本模式下的实际上是0x0d 0x0a的字节对,但0x0d只是\r:在文本模式下包括\r,但在二进制模式下不包括\r,所以和\r是等效的...或者应该是。这实际上是一个非常基本的行业混乱,典型的行业惯性,因为惯例是在所有平台上谈论CRLF,然后陷入不同的二元解释。严格来说,包含 ONLY 0x0d(回车)的文件为 (CRLF 或换行),在 TEXT 模式下格式不正确(打字机:只需返回汽车并删除所有内容...),并且是非面向行的二进制格式(\r 或 \r 表示面向行),因此您不应该作为文本阅读!代码应该失败,可能会有一些用户消息。这不仅取决于操作系统,还取决于 C 库的实现,这增加了混乱和可能的变化......(特别是对于透明的 UNICODE 转换层,为令人困惑的变化增加了另一个清晰度)。

前面的代码片段(机械打字机)的问题在于,如果\r(自动打字机文本)之后没有个字符,则效率非常低。然后,它还采用 BINARY 模式,其中 C 库被迫忽略文本解释(区域设置)并放弃纯粹的字节。两种模式之间的实际文本字符应该没有区别,只在控制字符上,所以一般来说阅读 BINARYTEXT 模式更好。此解决方案对于独立于 C 库变体的二进制模式典型 Windows 操作系统文本文件是有效的,对于其他平台文本格式(包括 Web 文本翻译)效率低下。如果您关心效率,那么要走的方法是使用函数指针,以您喜欢的方式对 \r 与 \r 行控件进行测试,然后在指针中选择最佳的 getline 用户代码并从中调用它。

顺便说一下,我记得我也发现了一些\r\r文本文件...它转换为双行文本,就像一些印刷文本消费者仍然需要的那样。

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

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

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

检查"\r"可能是解决问题的最佳方法。 在二进制模式下读取将允许您检查所有三个常见的行结尾("\r"、"\r"和"")。 如果您只关心 Linux 和 Windows,因为旧式 Mac 行尾不应该存在太久,请仅检查""并删除尾随的"\r"字符。

不幸的是,接受的解决方案的行为与std::getline()不完全相同。若要获取该行为(对于我的测试),需要进行以下更改:

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():
            is.setstate(std::ios::eofbit);       //
            if(t.empty())                        // <== change here
                is.setstate(std::ios::failbit);  // 
            return is;
        default:
            t += (char)c;
        }
    }
}

根据 https://en.cppreference.com/w/cpp/string/basic_string/getline:

  1. 从输入中提取字符并将其附加到 str,直到发生以下情况之一(按列出的顺序检查)

    1. 输入时的文件结束条件,在这种情况下,getline 设置 eofbit。
    2. 下一个可用的输入字符是 delim,由 Traits::eq(c, delim) 测试,在这种情况下,分隔符字符是从输入中提取的,但不附加到 str。
    3. str.max_size() 字符已被存储,在这种情况下,getline 会设置 failbit 并返回。
  2. 如果出于任何
  3. 原因没有提取任何字符(甚至没有丢弃的分隔符),getline 将设置 failbit 并返回。

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

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

这也适用于其他行尾。