如何向后读取文件以有效地查找子字符串
How to read a file backwards to find substring efficiently
我有一个这种结构的巨大日志文件:
"timestamp":{"identifier":值}
"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 行中,因此从文件的开头开始并检查每一行的正确时间戳将是非常低效的。
所以我想我正在寻找一种逐行向后读取文件的解决方案。
我真的没有解决方案如何在不将大块加载到内存中的情况下高效地做到这一点。
我尝试从 EOF 开始读取 200 字节的数据块,但遇到的问题是,在许多情况下,数据块将时间戳减半。我试图检测到它并在需要时重新选择一些字节,但感觉必须有一个聪明的解决方案。
如果性能不是问题,您可以逐行读取整个文件直到达到要求的时间,然后开始转储。没有理由读取内存中的整个文件。如果性能有问题,请查找文件的一半,检查时间戳,然后以二进制搜索方式再次除以二。
由于您使用的是 mmap()
可能不可用的嵌入式设备,我想唯一可行的方法是使用您填充文件的一部分的缓冲区,以便能够检查其内容一次一大块。请注意,您需要重叠缓冲区 windows 以避免错过在缓冲区开头被切成两半的行。在开始检查块的时间戳之前,您需要在块的开头寻找第一个换行符并将其与之前的任何内容一起丢弃。丢弃缓冲区开头的部分行还有助于在将前一个块加载到缓冲区时将同一行的结尾与缓冲区的结尾对齐。
在缓冲区开头处理不完整的行使得这是一种非常丑陋且容易出错的方法。这就是为什么我建议使用 mmap()
的原因,如果可以的话,它可以让你忽略这些问题。
好吧,我发现这种方式很有趣,所以我为 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
我有一个这种结构的巨大日志文件:
"timestamp":{"identifier":值}
"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 行中,因此从文件的开头开始并检查每一行的正确时间戳将是非常低效的。
所以我想我正在寻找一种逐行向后读取文件的解决方案。 我真的没有解决方案如何在不将大块加载到内存中的情况下高效地做到这一点。
我尝试从 EOF 开始读取 200 字节的数据块,但遇到的问题是,在许多情况下,数据块将时间戳减半。我试图检测到它并在需要时重新选择一些字节,但感觉必须有一个聪明的解决方案。
如果性能不是问题,您可以逐行读取整个文件直到达到要求的时间,然后开始转储。没有理由读取内存中的整个文件。如果性能有问题,请查找文件的一半,检查时间戳,然后以二进制搜索方式再次除以二。
由于您使用的是 mmap()
可能不可用的嵌入式设备,我想唯一可行的方法是使用您填充文件的一部分的缓冲区,以便能够检查其内容一次一大块。请注意,您需要重叠缓冲区 windows 以避免错过在缓冲区开头被切成两半的行。在开始检查块的时间戳之前,您需要在块的开头寻找第一个换行符并将其与之前的任何内容一起丢弃。丢弃缓冲区开头的部分行还有助于在将前一个块加载到缓冲区时将同一行的结尾与缓冲区的结尾对齐。
在缓冲区开头处理不完整的行使得这是一种非常丑陋且容易出错的方法。这就是为什么我建议使用 mmap()
的原因,如果可以的话,它可以让你忽略这些问题。
好吧,我发现这种方式很有趣,所以我为 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