如何向后读取文件以有效地查找子字符串

时间:2016-05-16 13:17:45

标签: c++ file c++11 data-management

我有一个这种结构的巨大日志文件:

“timestamp”:{“identifier”:value}

"1463403600":{"AA":74.42},
"1463403601":{"AA":29.55},
"1463403603":{"AA":24.78},
"1463403604":{"AA":8.46},
"1463403605":{"AA":44.84},
"1463403607":{"AA":87.05},
"1463403608":{"AA":54.81},
"1463403609":{"AA":93.1},
"1463403611":{"AA":77.64},
"1463403612":{"AA":33.39},
"1463403613":{"AA":69.2},

我想在(!)给定的时间戳之后提取内容,如:

std::ifstream * myfunc( uint32_t timestamp) 

示例:

myfunc(1463403611);
/* returns
"1463403611":{"AA":77.64},
"1463403612":{"AA":33.39},
"1463403613":{"AA":69.2},
*/

日志文件很长 - 太长而无法将其保留在内存中。代码将在资源有限的嵌入式设备(80Mhz,~10kB可用内存)上运行,因此我正在寻找一些有效解决方案的想法。

日志文件可能有500k +条目,并且在99%的时间内,时间戳将在最后100行中,因此从文件的beginnig开始并检查每一行的正确时间戳将是非常低效的。

所以我想我正在寻找一种解决方案来逐行读取文件。 我没有真正的解决方案如何在不将大块加载到内存中的情况下提高效率。

我尝试从EOF开始阅读200字节的块,但是面对这个问题,在很多情况下,块会将时间戳减少一半。我试图检测到这一点并在需要时重新选择一些字节,但感觉必须有一个聪明的解决方案。

3 个答案:

答案 0 :(得分:21)

我发现这很有趣,所以我为binary-search想法敲了一个概念验证。

这是经过严格测试的,可能是一个小小的车,但似乎到目前为止工作并展示了分而治之的想法。您检查文件的中间位置,并根据您是高还是低,将数据分成两部分并搜索相关的一半。你递归地做到这一点,直到你足够接近。

#include <ctime>
#include <cmath>
#include <cstdlib>
#include <string>
#include <fstream>
#include <iostream>

// Don't use this, its just to show how many reads
// are being done to find the record.
int global_counter;

std::streampos find_stamp(std::istream& is, long stamp, std::streampos pos, std::streampos end)
{
    ++global_counter;

    if(pos == 0) // can't divide zero
        return 0;

    std::string s;
    long found_stamp;

    // extract nearest timestamp after pos
    is.seekg(pos);
    if(!(std::getline(std::getline(is, s, ','), s, '"') >> found_stamp))
        return end;

    // if its too big check first half of this region
    if(found_stamp > stamp)
        return find_stamp(is, stamp, pos / 2, pos);

    // if its not within 10 timestamp seconds check end half of this region
    if(stamp - found_stamp > 10)
        return find_stamp(is, stamp, (pos + end) / 2, end);

    // read record by record (prolly more efficient than skipping)
    pos = is.tellg();
    while(std::getline(std::getline(is, s, ','), s, '"') >> found_stamp)
    {
        if(found_stamp > stamp)
            return pos;
        pos = is.tellg();
    }
    return end;
}

void print_after(const std::string& filename, long stamp)
{
    // open at end of file (to get length)
    std::ifstream ifs(filename, std::ios::ate);

    std::streampos end = ifs.tellg();
    auto pos = end / 2; // start checking in middle

    // find position before required record
    // (may be in the middle of a record)
    if((pos = find_stamp(ifs, stamp, pos, end)) != end)
    {
        ifs.seekg(pos);

        std::string line;
        std::getline(ifs, line, ','); // skip to next whole record

        // print out all following recors
        while(std::getline(ifs, line, ','))
            std::cout << line;
    }
}

inline
std::string leading_zeros(int n, int zeros = 2)
{
    std::string s;
    for(int z = std::pow(10, zeros - 1); z; z /= 10)
        s += (n < z ? "0":"");
    return s + std::to_string(n);
}

int main()
{
    std::srand(std::time(0));

    // generate some test data
    std::ofstream ofs("test.txt");

    for(int i = 0; i < 1000; ++i)
    {
        ofs << '"' << leading_zeros(i, 10) << '"';
        ofs << ":{\"AA\":" << (std::rand() % 100);
        ofs << '.' << (std::rand() % 100) << "},\n";
    }

    ofs.close();

    global_counter = 0;
    print_after("test.txt", 993);

    std::cout << "find checked " << global_counter << " places in the file\n";
}

<强>输出:

"0000000994":{"AA":80.6}
"0000000995":{"AA":11.90}
"0000000996":{"AA":16.43}
"0000000997":{"AA":53.11}
"0000000998":{"AA":68.43}
"0000000999":{"AA":79.77}
find checked 6 places in the file

答案 1 :(得分:5)

由于您使用的IFS=$'\n'; for object in `aws s3 ls s3://my-bucket-name --recursive | tr -s ' ' | cut -d' ' -f4-`; do echo $object `aws s3api head-object --bucket my-bucket-name --key $object --query CacheControl` ; done 可能不可用的嵌入式设备,我想唯一可行的方法是使用填充文件的一部分的缓冲区,以便能够检查其内容一次大块。请注意,您需要重叠缓冲区窗口,以避免错过缓冲区开头处减半的行。您需要在块的开头寻找第一个换行符,然后在开始检查时间戳的块之前将其丢弃。当您将前一个块加载到缓冲区时,丢弃缓冲区开头的部分行也有助于将同一行的结尾与缓冲区的末尾对齐。

在缓冲区开头处理不完整的行使得这种方法非常难看且容易出错。这就是我建议使用mmap()的原因,如果可以的话,它会让你忽略这些问题。

答案 2 :(得分:3)

如果性能不是问题,您可以逐行读取整个文件 ,直到达到请求的时间,然后开始转储。没有理由在内存中读取整个文件。如果性能是一个问题,请寻找文件的一半,检查时间戳,然后以二进制搜索方式再次除以二。