PHP中實現Bloom Filter算法,bloomfilter
<?php
/*Bloom Filter算法來去重過濾。
介紹下Bloom Filter的基本處理思路:申請一批空間用於保存0 1信息,再根據一批哈希函數確定元素對應的位置,如果每個哈希函數對應位置的值為全部1,說明此元素存在。相反,如果為0,則要把對應位置的值設置為1。由於不同的元素可能會有相同的哈希值,即同一個位置有可能保存了多個元素的信息,從而導致存在一定的誤判率。
如果申請空間太小,隨著元素的增多,1會越來越多,各個元素沖突的機會越來越來大,導致誤判率會越來越大。另外哈希函數的選擇及個數上也要平衡好,多個哈希函數雖然可以提供判斷的准確性,但是會降低程序的處理速度,而哈希函數的增加又要求有更多的空間來存儲位置信息。
Bloom-Filter的應用。
Bloom-Filter一般用於在大數據量的集合中判定某元素是否存在。例如郵件服務器中的垃圾郵件過濾器。在搜索引擎領域,Bloom-Filter最常用於網絡蜘蛛(Spider)的URL過濾,網絡蜘蛛通常有一個 URL列表,保存著將要下載和已經下載的網頁的URL,網絡蜘蛛下載了一個網頁,從網頁中提取到新的URL後,需要判斷該URL是否已經存在於列表中。此時,Bloom-Filter算法是最好的選擇。
比如說,一個象 Yahoo,Hotmail 和 Gmai 那樣的公眾電子郵件(email)提供商,總是需要過濾來自發送垃圾郵件的人(spamer)的垃圾郵件。一個辦法就是記錄下那些發垃圾郵件的 email 地址。由於那些發送者不停地在注冊新的地址,全世界少說也有幾十億個發垃圾郵件的地址,將他們都存起來則需要大量的網絡服務器。
布隆過濾器是由巴頓.布隆於一九七零年提出的。它實際上是一個很長的二進制向量和一系列隨機映射函數。我們通過上面的例子來說明起工作原理。
假定我們存儲一億個電子郵件地址,我們先建立一個十六億二進制(比特),即兩億字節的向量,然後將這十六億個二進制位全部設置為零。對於每一個電子郵件地址 X,我們用八個不同的隨機數產生器(F1,F2, ...,F8) 產生八個信息指紋(f1, f2, ..., f8)。再用一個隨機數產生器 G 把這八個信息指紋映射到 1 到十六億中的八個自然數 g1, g2, ...,g8。現在我們把這八個位置的二進制位全部設置為一。當我們對這一億個 email 地址都進行這樣的處理後。一個針對這些 email 地址的布隆過濾器就建成了。(見下圖) 現在,讓我們看看如何用布隆過濾器來檢測一個可疑的電子郵件地址 Y 是否在黑名單中。我們用相同的八個隨機數產生器(F1, F2, ..., F8)對這個地址產生八個信息指紋 s1,s2,...,s8,然後將這八個指紋對應到布隆過濾器的八個二進制位,分別是 t1,t2,...,t8。如果 Y 在黑名單中,顯然,t1,t2,..,t8 對應的八個二進制一定是一。這樣在遇到任何在黑名單中的電子郵件地址,我們都能准確地發現。
布隆過濾器決不會漏掉任何一個在黑名單中的可疑地址。但是,它有一條不足之處。也就是它有極小的可能將一個不在黑名單中的電子郵件地址判定為在黑名單中,因為有可能某個好的郵件地址正巧對應八個都被設置成一的二進制位。好在這種可能性很小。我們把它稱為誤識概率。在上面的例子中,誤識概率在萬分之一以下。
布隆過濾器的好處在於快速,省空間。但是有一定的誤識別率。常見的補救辦法是在建立一個小的白名單,存儲那些可能別誤判的郵件地址。
*/
// 使用php程序來描述上面的算法
$set = array(1,2,3,4,5,6);
// 判斷5是否在$set 中
$bloomFiter = array(0,0,0,0,0,0,0,0,0,0);
// 通過某種算法改變$bloomFiter 中位數組表示集合,這裡我們使用簡單的算法,把集合中對應的value 對應到bloom中的位置變成1
// 算法如下
foreach($set as $key){
$bloomFiter[$key] = 1 ;
}
var_dump($bloomFiter) ;
//此時 $bloomFiter = array(1,1,1,1,1,1);
//判斷是否在集合中
if($bloomFiter[9] ==1){
echo '在set 中';
}else{
echo '不在set 中' ;
}
// 上面只是一個簡單的例子,實際上哈希算法需要好幾個,但另一方面,如果哈希函數的個數少,那麼位數組中的0就多
class bloom_filter {
function __construct($hash_func_num=1, $space_group_num=1) {
$max_length = pow(2, 25);
$binary = pack('C', 0);
//1字節占用8位
$this->one_num = 8;
//默認32m*1
$this->space_group_num = $space_group_num;
$this->hash_space_assoc = array();
//分配空間
for($i=0; $i<$this->space_group_num; $i++){
$this->hash_space_assoc[$i] = str_repeat($binary, $max_length);
}
$this->pow_array = array(
0 => 1,
1 => 2,
2 => 4,
3 => 8,
4 => 16,
5 => 32,
6 => 64,
7 => 128,
);
$this->chr_array = array();
$this->ord_array = array();
for($i=0; $i<256; $i++){
$chr = chr($i);
$this->chr_array[$i] = $chr;
$this->ord_array[$chr] = $i;
}
$this->hash_func_pos = array(
0 => array(0, 7, 1),
1 => array(7, 7, 1),
2 => array(14, 7, 1),
3 => array(21, 7, 1),
4 => array(28, 7, 1),
5 => array(33, 7, 1),
6 => array(17, 7, 1),
);
$this->write_num = 0;
$this->ext_num = 0;
if(!$hash_func_num){
$this->hash_func_num = count($this->hash_func_pos);
}
else{
$this->hash_func_num = $hash_func_num;
}
}
function add($key) {
$hash_bit_set_num = 0;
// 離散key
$hash_basic = sha1($key);
// 截取前4位,然後十六進制轉換為十進制
$hash_space = hexdec(substr($hash_basic, 0, 4));
// 取模
$hash_space = $hash_space % $this->space_group_num;
for($hash_i=0; $hash_i<$this->hash_func_num; $hash_i++){
$hash = hexdec(substr($hash_basic, $this->hash_func_pos[$hash_i][0], $this->hash_func_pos[$hash_i][1]));
$bit_pos = $hash >> 3;
$max = $this->ord_array[$this->hash_space_assoc[$hash_space][$bit_pos]];
$num = $hash - $bit_pos * $this->one_num;
$bit_pos_value = ($max >> $num) & 0x01;
if(!$bit_pos_value){
$max = $max | $this->pow_array[$num];
$this->hash_space_assoc[$hash_space][$bit_pos] = $this->chr_array[$max];
$this->write_num++;
}
else{
$hash_bit_set_num++;
}
}
if($hash_bit_set_num == $this->hash_func_num){
$this->ext_num++;
return true;
}
return false;
}
function get_stat() {
return array(
'ext_num' => $this->ext_num,
'write_num' => $this->write_num,
);
}
}
//test
//取6個哈希值,目前是最多7個
$hash_func_num = 6;
//分配1個存儲空間,每個空間為32M,理論上是空間越大誤判率越低,注意php.ini中可使用的內存限制
$space_group_num = 1;
$bf = new bloom_filter($hash_func_num, $space_group_num);
$list = array(
'http://test/1',
'http://test/2',
'http://test/3',
'http://test/4',
'http://test/5',
'http://test/6',
'http://test/1',
'http://test/2',
);
foreach($list as $k => $v){
if($bf->add($v)){
echo $v, "\n";
}
}
print_r($bf->get_stat());