ChatGPT解决这个技术问题 Extra ChatGPT

让 std :: ifstream 处理 LF、CR 和 CRLF?

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

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

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

getline 将整行读取到字符串中,直到 '\n'。 '\n' 从流中消耗,但 getline 不将其包含在字符串中。到目前为止这很好,但是在字符串中包含的 '\n' 之前可能有一个 '\r'。

在文本文件中看到 three types of line endings:'\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'-only 文件。

if(!line.empty() && *line.rbegin() == '\r') {
    line.erase( line.length()-1, 1);
}
\n 表示以当前操作系统中呈现的任何方式的新行。图书馆负责处理。但是为了让它工作,在 windows 中编译的程序应该从 windows 中读取文本文件,在 unix 中编译的程序,从 unix 中读取文本文件等。
@George,即使我在 Linux 机器上编译,有时我也会使用最初来自 Windows 机器的文本文件。我可能会发布我的软件(一个用于网络分析的小工具),并且我希望能够告诉用户他们几乎可以在任何时间输入(类似 ASCII 的)文本文件。
请注意, if(f.good()) 并不像您认为的那样做。
谢谢@Neil,即使我几天前检查了所有内容,我也爱上了它!那时我完全明白了。我想我让自己粗心地假设 f.good() 应该是 f.fail() 的反义词。

2
21 revs, 3 users 99%

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

@Miek:我按照 Bo Persons 的建议 stackoverflow.com/questions/9188126/… 更新了代码并运行了一些测试。现在一切正常。
@Thomas Weller:哨兵的构造函数和析构函数被执行。它们执行诸如线程同步、跳过空白和更新流状态之类的事情。
在 EOF 情况下,在设置 eofbit 之前检查 t 是否为空的目的是什么。无论是否读入了其他字符,不应该设置该位吗?
Yay295:应该设置 eof 标志,而不是在您到达最后一行的末尾时,而是在您尝试读取超出最后一行时。检查确保在最后一行没有 EOL 时发生这种情况。 (尝试删除检查,然后在最后一行没有 EOL 的文本文件上运行测试程序,你会看到。)
这也会读取一个空的最后一行,这不是 std::get_line 的行为,它忽略了一个空的最后一行。我在 eof 案例中使用了以下代码来模拟 std::get_line 行为:is.setstate(std::ios::eofbit); if (t.empty()) is.setstate(std::ios::badbit); return is;
佚名

C++ 运行时应该正确处理适用于您的特定平台的任何 endline 约定。具体来说,此代码应适用于所有平台:

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

问题是关于如何处理来自另一个平台的文件。
@Neil,这个答案还不够。如果我只是想处理 CRLF,我就不会来 StackOverflow。真正的挑战是处理只有“\r”的文件。现在它们非常罕见,现在 MacOS 已经更接近 Unix,但我不想假设它们永远不会被提供给我的软件。
@Aaron 好吧,如果您希望能够处理任何事情,您必须编写自己的代码来完成它。
我从一开始就在我的问题中明确表示,解决这个问题很容易,这意味着我愿意并且能够这样做。我问这个问题是因为它似乎是一个常见的问题,并且有多种文本文件格式。我假设/希望 C++ 标准委员会已经内置了它。这是我的问题。
@Neil,我认为我/我们忘记了另一个问题。但首先,我承认确定要支持的少量格式对我来说是切实可行的。因此,我想要可以在 Windows 和 Linux 上编译并且可以使用任何一种格式的代码。您的 safegetline 是解决方案的重要组成部分。但是如果这个程序是在 Windows 上编译的,我还需要以二进制格式打开文件吗? Windows 编译器(在文本模式下)是否允许 '\n' 表现得像 '\r''\n'? ifstream f("f.txt", ios_base :: binary | ios_base::in );
b
bouvierr

您是在 BINARY 还是 TEXT 模式下读取文件?在 TEXT 模式下,一对回车/换行符 CRLF 被解释为 TEXT 行尾或行尾字符,但在 BINARY 中,您一次只能获取一个字节,这意味着必须忽略任何一个字符并将其留在要作为另一个字节获取的缓冲区!回车是指在打字机中,打印臂所在的打字车已经到达纸张的右边缘并返回到左边缘。这是一个非常机械的模型,机械打字机的模型。然后换行意味着纸卷向上旋转一点,这样纸就可以开始另一行打字了。据我所知,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);
...

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

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

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


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

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


佚名

除了编写自己的自定义处理程序或使用外部库之外,您很不走运。最简单的方法是检查以确保 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' 字符。


M
Martin Thümmel

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

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

这也适用于其他行尾。


G
Gergely Nagy

不幸的是,接受的解决方案的行为与 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

从输入中提取字符并将它们附加到 str 直到出现以下之一(按列出的顺序检查)输入的文件结束条件,在这种情况下,getline 设置 eofbit。下一个可用的输入字符是 delim,由 Traits::eq(c, delim) 测试,在这种情况下,分隔符从输入中提取,但不附加到 str。 str.max_size() 字符已被存储,在这种情况下 getline 设置失败位并返回。如果由于某种原因没有提取字符(甚至没有被丢弃的分隔符),getline 设置失败位并返回。