我有很多网页访问日志文件,其中每次访问都与用户ID和时间戳相关联。我需要确定最受欢迎(即最常访问)的三页序列。日志文件太大,无法一次保存在主内存中。
示例日志文件:
User ID Page ID
A 1
A 2
A 3
B 2
B 3
C 1
B 4
A 4
相应的结果:
A:1-2-3,2-3-4
B:2-3-4
2-3-4是最受欢迎的三页序列
我的想法是使用两个哈希表。用户ID的第一个哈希并存储其序列;第二个散列三页序列并存储每个序列出现的次数。这需要O(n)空间和O(n)时间。
但是,由于我必须使用两个哈希表,因此内存不能同时保存所有内容,而且我必须使用磁盘。经常访问磁盘效率不高。
我怎样才能更好地做到这一点?
答案 0 :(得分:4)
如果您想快速获得近似结果,请按预期使用哈希表,但为每个哈希表添加一个有限大小的队列,以删除最近最少使用的条目。
如果您需要精确的结果,请使用外部排序程序按用户ID对日志进行排序,然后将每3个连续的条目组合并再次排序,这次是按页面ID。
更新(按时间戳排序)
可能需要进行一些预处理才能正确使用日志文件的时间戳:
更新2(改进近似方法)
使用LRU队列的近似方法应该为随机分布的数据产生相当好的结果。但是,网页访问在一天中的不同时间可能会有不同的模式,或者在周末可能会有所不同。最初的方法可能会给这些数据带来不良结果。为了改善这一点,可以使用分层LRU队列。
将LRU队列分区为log(N)个较小的队列。大小为N / 2,N / 4,......最大的一个应该包含任何元素,下一个 - 仅元素,至少看到2次,下一个 - 至少4次,...如果元素从某个子元素中删除-queue,它被添加到另一个,因此它在完全删除之前存在于层次结构较低的所有子队列中。这样的优先级队列仍然具有O(1)复杂度,但允许对大多数流行页面进行更好的近似。
答案 1 :(得分:3)
这里可能存在语法错误,但这应该占用有限数量的RAM,用于几乎无限长度的日志文件。
typedef int pageid;
typedef int userid;
typedef pageid[3] sequence;
typedef int sequence_count;
const int num_pages = 1000; //where 1-1000 inclusive are valid pageids
const int num_passes = 4;
std::unordered_map<userid, sequence> userhistory;
std::unordered_map<sequence, sequence_count> visits;
sequence_count max_count=0;
sequence max_sequence={};
userid curuser;
pageid curpage;
for(int pass=0; pass<num_passes; ++pass) { //have to go in four passes
std::ifstream logfile("log.log");
pageid minpage = num_pages/num_passes*pass; //where first page is in a range
pageid maxpage = num_pages/num_passes*(pass+1)+1;
if (pass==num_passes-1) //if it's last pass, fix rounding errors
maxpage = MAX_INT;
while(logfile >> curuser >> curpage) { //read in line
sequence& curhistory = userhistory[curuser]; //find that user's history
curhistory[2] = curhistory[1];
curhistory[1] = curhistory[0];
curhistory[0] = curhistory[curpage]; //push back new page for that user
//if they visited three pages in a row
if (curhistory[2] > minpage && curhistory[2]<maxpage) {
sequence_count& count = visits[curhistory]; //get times sequence was hit
++count; //and increase it
if (count > max_count) { //if that's new max
max_count = count; //update the max
max_sequence = curhistory; //arrays, so this is memcpy or something
}
}
}
}
std::cout << "The sequence visited the most is :\n";
std::cout << max_sequence[2] << '\n';
std::cout << max_sequence[1] << '\n';
std::cout << max_sequence[0] << '\n';
std::cout << "with " << max_count << " visits.\n";
请注意,如果pageid
或userid
为string
而不是int
s,则会对速度/大小/缓存造成重大影响。
[EDIT2]它现在可以在4个(可自定义的)传递中工作,这意味着它使用更少的内存,使这在RAM中实际工作。它只是比较慢。
答案 2 :(得分:2)
如果您有1000个网页,那么您有10亿个可能的3页序列。如果你有一个简单的32位计数器数组,那么你将使用4GB的内存。可能有办法通过丢弃数据来削减这一点,但如果你想保证得到正确答案,那么这总是最糟糕的情况 - 没有避免它,并且发明了节省内存的方法。平均情况会使最坏的情况更加耗费内存。
最重要的是,您必须跟踪用户。对于每个用户,您需要存储他们访问过的最后两页。假设用户在日志中按名称引用,则需要将用户名称存储在哈希表中,加上两个页码,所以假设每个用户平均24个字节(可能是保守的 - 我假设短用户名)。 1000个用户将是24KB; 1000000用户24MB。
显然,序列计数器主导了内存问题。
如果你只有1000页,那么4GB内存在现代64位机器中并不合理,特别是在磁盘支持的虚拟内存量很大的情况下。如果你没有足够的交换空间,你可以创建一个mmapped文件(在Linux上 - 我认为Windows有类似的东西),并且依赖操作系统来总是将大多数用例缓存在内存中。
所以,基本上,数学要求如果你有大量的网页需要跟踪,并且你希望能够应对最坏的情况,那么你将不得不接受你必须使用磁盘文件。
我认为有限容量的哈希表可能是正确的答案。您可以根据可用内存大小调整特定机器的优化程序。有了这个,你需要处理表达到容量的情况。如果您很少到达那里,它可能不需要非常有效。以下是一些想法:
将最不常用的序列逐出文件,保留最常用的内存。我需要在桌子上两次通过以确定什么级别低于平均水平,然后进行驱逐。不管怎么说,每当你遇到哈希错过时,你都需要知道每个条目的位置,这可能会很棘手。
只需将整个表转储到文件中,然后从头开始构建一个新表。重复。最后,重新组合所有表中的匹配条目。最后一部分也可能证明是棘手的。
使用mmapped文件扩展表。确保该文件主要用于最常用的序列,如我的第一个建议。基本上,你只是将它用作虚拟内存 - 在忘记地址之后,文件将毫无意义,但你不需要保留那么久。我假设这里没有足够的常规虚拟内存,和/或你不想使用它。显然,这仅适用于64位系统。
答案 3 :(得分:1)
我认为您只需为每个用户ID存储最近看到的三元组吗? 所以你有两个哈希表。第一个包含userid的键,最近看到的三元组的值大小等于用户ID的数量。
编辑:假设文件已按时间戳排序。
第二个哈希表有一个userid键:page-triple,以及一个看到的次数值。
我知道你说c ++但是这里有一些awk在一次传递中做到这一点(应该很简单地转换为c ++):
# $1 is userid, $2 is pageid
{
old = ids[$1]; # map with id, most-recently-seen triple
split(old,oldarr,"-");
oldarr[1]=oldarr[2];
oldarr[2]=oldarr[3];
oldarr[3] = $2;
ids[$1]=oldarr[1]"-"oldarr[2]"-"oldarr[3]; # save new most-recently-seen
tripleid = $1":"ids[$1]; # build a triple-id of userid:triple
if (oldarr[1] != "") { # don't accumulate incomplete triples
triples[tripleid]++; } # count this triple-id
}
END {
MAX = 0;
for (tid in triples) {
print tid" "triples[tid];
if (triples[tid] > MAX) MAX = tid;
}
print "MAX is->" MAX" seen "triples[tid]" times";
}
答案 4 :(得分:1)
如果您使用的是Unix,sort
命令可以处理任意大的文件。所以你可以这样做:
sort -k1,1 -s logfile > sorted
(请注意,这是第一列的稳定排序(-s
))sorted
的自定义处理,使用C ++或shell脚本将每个三元组作为新行输出到另一个文件,例如triplets
。因此,在给出的示例中,您将获得包含三行的文件:1-2-3,2-3-4,2-3-4。此处理很快,因为步骤1意味着您一次只处理一个用户的访问,因此您可以一次处理sorted
个文件。sort triplets | uniq -c | sort -r -n | head -1
应该给出最常见的三元组及其计数(它对三元组进行排序,计算每个三元组的出现次数,按照计数的降序对它们进行排序,然后选择最高的三元组)。这种方法可能没有最佳性能,但它不应该耗尽内存。