如何安全地混合使用 std::ifstream 的 tellg、seekg 和 read(*,n) 方法在文本模式下?

3
我正在尝试使用std :: ifstream循环逐行读取文件。在同一个循环中,我试图找到两个标记,这些标记包含我想要整体读取的块。
我认为,我可以使用seekg跟踪块的起始和结束位置,并在找到两个位置后使用read(*,end-start)来读取块。
然而,tellg返回流位置,但由于该文件已在文本模式下打开[以便能够调用getline]并使用\r\n作为行末,请注意ifstream的read方法的“字符数”参数是指换行符从\r\n转换为\n后的数量,因此我正好比预期多读入n个字符,其中n是两个标记之间的行数。
显然,有许多变通方法,但我正在寻找一种漂亮而直观的解决方案。 有什么建议吗?
EDIT1@130507: 我的目标是保持std lib流,注重内存而不是速度,并且需要解析和处理两部分,即周围的部分和标记之间的块。
我希望有一些可用的东西,例如切换到已打开的文本模式流的二进制模式,或者具有某些(基类)原始读取方法,它不像read那样进行字符转换,或者一些映射器方法,允许在字符转换之前和之后在流索引之间进行映射,但迄今为止找不到任何东西。
这里是一些代码:
std::ifstream testDataFileStream;
testDataFileStream.open(testDataFileName, std::ios_base::in);
testDataFileStream.unsetf(std::ios::skipws); // No white space skipping
if (testDataFileStream) {
    std::string line;
    while (getline(testDataFileStream, line))
        if (line == "starttag")
            break;
    if (line == "starttag")
    {
        std::ifstream::pos_type cmdStartPos = testDataFileStream.tellg();
        std::ifstream::pos_type cmdEndPos;
        while (getline(testDataFileStream, line))
            if (line == "endtag")
                break;
            else
                cmdEndPos = testDataFileStream.tellg();
        if (line == "endtag")
        {
            std::streamsize nofBytesToRead = cmdEndPos - cmdStartPos;

            // now, one possible workaround follows, however, it's obviously quite lame
            testDataFileStream.close();
            testDataFileStream.open(testDataFileName, std::ios_base::in | std::ios::binary);
            testDataFileStream.seekg(cmdStartPos);
            std::string cmdsString;
            cmdsString.resize(nofBytesToRead+1);
            testDataFileStream.read(&cmdsString[0], nofBytesToRead);
        } else {}
    } else {}
    testDataFileStream.close();
} else {}

一个测试文件可能看起来像以下内容:
some text
more text
starttag
much stuff on many lines
endtag
even more text

tellg和seekg在文本模式下变得非常棘手。 - Terenty Rezman
文本模式流更喜欢相对位置而不是绝对位置。执行 seekg(tellg()) 将会将当前流的位置移动到未知的位置。 这样做可能看起来有点低效,但我想知道如果你这样做会发生什么: while (testDataFileStream.tellg() != cmdStartPos) testDataFileStream.unget(); - Thanasis Papoutsidakis
4个回答

1
正如Jerry Coffin和Terenty Rezman所建议的,tellg()/seekg()方法会让你陷入困境。由于您希望解析所有行并对starttag/endtag块进行一些特定的解析,我建议您采取以下措施:
  • 以文本模式逐行读取文件
  • 跟踪进入和退出这些块的时间
  • 在读取一个块内的行时以某种适当的方式“组装”块。
  • 对每个块内和块外的行进行正确的处理
  • 在完成一个块时对该块进行正确的处理。
  • 在执行过程中处理解析错误。

这是一个粗略的示例。它跳过空行,但假设非空行中没有填充,只有标记。它假设块不能嵌套:

#include <fstream>
#include <iostream>

enum parse_error
{
    none,
    open_fail,
    nested_starttag,
    orphan_endtag,
    orphan_starttag
};

void handle_out_of_block_line(std::string const & line) 
{
    std::cout << "Read out-of-block line: \"" << line << '\"' << std::endl;
}

void handle_in_block_line(std::string const & line, std::string & block) 
{
    std::cout << "Read in-block line: \"" << line << '\"' << std::endl;
    block += line + '\n'; 
}

void handle_block(std::string const & block)
{
    std::cout << "Got block {\n" << block << "}" << std::endl;
}

parse_error parse(std::string const & filename)
{
    std::ifstream ifs(filename);
    if (!ifs) {
        std::cerr << 
        "error: cannot open \"" << filename << "\" for reading" << std::endl; 
        return parse_error::open_fail;
    }
    bool in_block = 0;  
    std::string line;
    std::string block;
    while(getline(ifs,line),ifs) {
        if (line.empty()) {
            continue; // Skip empty line.
        }
        if (line == "starttag") {
            if (in_block) {
                std::cerr << "error: starttag within starttag" << std::endl; 
                return parse_error::nested_starttag;
            }
            in_block = true;
            block.clear();
        }
        if (in_block) {
            handle_in_block_line(line,block);
        } else {
            handle_out_of_block_line(line);
        }
        if (line == "endtag") {
            if (!in_block) {
                std::cerr << "error: ophan endtag" << std::endl; 
                return parse_error::orphan_endtag;
            }
            in_block = false;
            handle_block(block);
        }
    }
    if (in_block) {
        std::cerr << "error: ophan starttag" << std::endl;
        return parse_error::orphan_starttag;
    }
    return parse_error::none;
}

int main(int argc, char const *argv[])
{
    return parse(argv[1]);
}

输入例如包含此内容的文件:

some text
more text
starttag
much stuff 
on many lines
endtag
even more text

它输出这个:
Read out-of-block line: "some text"
Read out-of-block line: "more text"
Read in-block line: "starttag"
Read in-block line: "much stuff "
Read in-block line: "on many lines"
Read in-block line: "endtag"
Got block {
starttag
much stuff 
on many lines
endtag
}
Read out-of-block line: "even more text"

1
为了扩展Jerry Coffin的方法,这里提供一个简单的例子。通过使用C++11的std::move,可以避免额外的分配。但是需要注意的是,对于长行,getline()会导致其std::string参数的重复重新分配。如果您真的关心内存管理,应该考虑将数据读入固定大小的缓冲区中。
无论如何,以下是代码:
#include <fstream>
#include <iostream>
#include <vector>
#include <utility>

int main() {
    std::ifstream testDataFileStream;
    testDataFileStream.open("data.txt", std::ios_base::in);
    testDataFileStream.unsetf(std::ios::skipws); // No white space skipping
    if (testDataFileStream) {
        std::vector<std::string> buffer;
        std::string line;
        bool found = false;
        while (getline(testDataFileStream, line)) {
            if (line == "starttag")
                found = true;
            if (found) {
                buffer.push_back(std::move(line));
                if (line == "endtag")
                    found = false;
            }
        }
        for (std::string & s : buffer) {
            std::cout << s << std::endl;
        }
    }
}

1
字符翻译发生在以文本模式打开文件时。
您可以以二进制模式打开文件。ios::binary

很抱歉我没有表述清楚,我需要文本模式中不在标签之间的外部部分,所以我认为只要有一种简单的方法在二进制中读取后进行转换即可。是否有这样的函数可用? - wonko realtime

0

在我看来,你选择了一个相对困难的方法来解决这个问题。

既然你已经要扫描文件寻找标签,为什么不在扫描标签的同时保留数据呢?也就是说,在扫描并丢弃数据直到找到起始标签后,继续扫描并保留从那里开始的数据,直到找到结束标签。


嗨,杰瑞。我希望有更简单的方法,比如说,在已经打开的文本模式流中切换到二进制模式,或者有一些(基类)原始读取方法,不像read那样进行字符转换,或者一些映射器方法,允许在字符转换之前和之后映射流索引之间进行映射。我喜欢你的方法的简单性,但不喜欢使用stringstream或类似工具来保留行时产生的重新分配开销。 - wonko realtime
@wonkorealtime:你测试过重新分配的开销实际上是多少吗?(我测试过了——通常在进行I/O时太小而无法测量)。 - Jerry Coffin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接