一個月前寫了一個主題爬蟲,重點主要在對網頁和內部連結分類上,其它部分的細節問題沒有仔細研究。這個過程中我遇到一個很重要的問題:判斷一個URL是否已經爬取過。對待這個問題,我開始的做法是最直接,也是最簡單的:將已經爬行到的URL存入一個HashSet<String>之中,每次抓取網頁之前都判斷一下此URL是否在這個hash表中,如果已經存在,則放棄,否則,將其加入hash表並爬去此URL對應頁面。乍一看起來似乎無可非議,但是隨著url數量的增長, hashset的體積持續增加,尋找速度也逐漸層慢。借鑒了一些成熟的解決方案後決定用記憶體做緩衝,用資料庫儲存,來實現一個用LRU(least recently use)演算法封裝的hash表做成的過濾器來實現對URL的過濾。具體內容如下:
代碼基於這樣一個事實:1,一個網頁之中包含的URL地址成簇出現(屬於同一個IP地址)。2,集合A為某網站IP地址下經常被引用的URL集合,B為此網站中不經常被引用的URL集合。則待爬行的URL屬於A集合的機率高於屬於B的機率。
主要思想:根據以上兩個事實:
1,將已爬行的url按IP地址分成不同集合,並記錄其中每個ip地址被引用的次數(入度)
2,用記憶體中的hashmap作為緩衝,用資料庫做儲存(為了加快速度,整個過程中判斷的都是URL和IP地址的hashcode值) 。
3,記憶體中儲存一部分IP地址和其對應的URL雜湊表的映射。
4,用一個隊列儲存ip地址被命中的次數,當緩衝容量大於指定值的時候,取出隊列中最不常用(叫用次數最小)的ip地址,
將其對應的 URL表存入資料庫中。
5,對某個IP地址下緩衝URL的數量做限制,如果超過某個值MAXIP,則將整個URL緩衝表寫入資料庫,並從其中取出前MAXIP/2
個命中率最高的URL放入記憶體緩衝。
過濾某URL步驟如下:
1,判斷URL的ip地址是否被緩衝,如果是,則用緩衝的URL 雜湊表過濾此URL。
2 ,如果URL的ip地址未被緩衝,則在資料庫中將ip對應的URL列表取出放入記憶體。然後進行1.
此過濾器的java代碼如下:
package Processer;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Random;
import java.util.Map.Entry;
import database.DatabaseCon;
public class LRUFilter {
private int CurrentCacheSize;
private int MAX;
private int MAXIP;
private HashMap<Integer,HashMap> rootMap;
private HashMap<Integer,Int> DBMap;
private Connection con;
private LinkedList<Element> lQueue;
private HashMap<Integer,Element> slQueue;
public LRUFilter(Connection c){
this(20000,10000,c);
}
public LRUFilter(int capable,int oneip,Connection c){
MAX=capable;
CurrentCacheSize=0;
MAXIP=oneip;
rootMap=new HashMap<Integer,HashMap>();
DBMap=new HashMap<Integer,Int>();
lQueue=new LinkedList<Element>();
slQueue=new HashMap<Integer,Element>();
con=c;
initialCache();
}
private boolean hit(HashMap<Integer,DBElement> urlMap,int ip,int url){
boolean contain=false;
slQueue.get(ip).hits++;
DBElement dbe=null;
if((dbe=urlMap.get(url))!=null){
contain=true;
if(dbe.flag==0){
dbe.hits++;
dbe.flag=1;
}else
dbe.hits++;
}else{
if(DBMap.get(ip).value<MAXIP){
contain=false;
urlMap.put(url, new DBElement(1,ip,2));
CurrentCacheSize++;
while(CurrentCacheSize>MAX)
toDB(0);
if(urlMap.size()>MAXIP){
toDB(ip);
fromDB(ip);
}
}else{
contain=toDBDirect(url,new DBElement(1,ip,0));
Int i=null;
if(!contain&&(i=DBMap.get(ip))!=null)
i.value+=1;
}
}
return contain;
}
private boolean notHit(int ip,int url){
return hit(fromDB(ip),ip,url);
}
/*
* get the keyip and its count into the DBMap which are the count cache of the ip
*/
private void initialCache(){
try{
String sql="use crawler;select keyip,count(keyip) from visited group by keyip";
Statement stm=con.createStatement();
ResultSet rs=stm.executeQuery(sql);
while(rs.next())
DBMap.put(rs.getInt(1), new Int(rs.getInt(2)));
rs.close();
stm.close();
}catch(Exception e){
e.printStackTrace();
}
}
/*
* filter the url, ip is the url's ip.
*/
public boolean contain(String ip,String url){
HashMap<Integer,DBElement> urlMap=null;
int keyip=ip.hashCode();
int keyurl=url.hashCode();
if((urlMap=rootMap.get(keyip))!=null)
return hit(urlMap,keyip,keyurl);
else
return notHit(keyip,keyurl);
}
/*
* If ip is equal to 0,get the least recently use ip from the lQueue,writ all
* the url belong the ip to the database, change the number of url belong this ip in DBMap
* if ip is not equal to 0, write the url belong this ip to the database.renew the lQueue and slQueue and DBMap
*/
private boolean toDB(int ip){
HashMap<Integer,DBElement> urlMap=null;
Int count=null;
int num;
if(ip==0){
Collections.sort(lQueue,new MyComparator());
Element e=null;
if((e=lQueue.poll())!=null){
ip=e.ip;
slQueue.remove(ip);
if((urlMap=rootMap.remove(ip))!=null){
num=writeToDB(urlMap);
if((count=DBMap.get(ip))!=null)
count.value+=num;
CurrentCacheSize-=urlMap.size();
}
}else
return false;//empty
}else{
if((urlMap=rootMap.remove(ip))!=null){
num=writeToDB(urlMap);
if((count=DBMap.get(ip))!=null)
count.value+=num;
CurrentCacheSize-=urlMap.size();
}
lQueue.remove(slQueue.remove(ip));
}
return true;
}
private HashMap<Integer,DBElement> fromDB(int ip){
Int i=null;
HashMap<Integer,DBElement> urlMap=null;
if((i=DBMap.get(ip))!=null){
if(i.value>MAXIP)
urlMap=readFromDB(ip,true);
else
urlMap=readFromDB(ip,false);
}else{
urlMap=new HashMap<Integer,DBElement>();
DBMap.put(ip, new Int(0));
}
while(urlMap.size()+CurrentCacheSize>MAX)
toDB(0);
for(int j=0;j<lQueue.size();j++)
lQueue.get(j).hits=0;
Element e=new Element(ip,0);
lQueue.add(e);
slQueue.put(ip, e);
rootMap.put(ip, urlMap);
CurrentCacheSize+=urlMap.size();
while(CurrentCacheSize>MAX)
toDB(0);
return urlMap;
}
/*
* write to database,return the number of
* record which is inserted into the database
*/
private int writeToDB(HashMap<Integer,DBElement> urlDBMap){
boolean insertAble=false,updateAble=false;
int num=0;
try{
PreparedStatement insertStm=con.prepareStatement("use crawler;insert into visited values(?,?,?);");
DBElement dbe=null;
for(Iterator i=urlDBMap.entrySet().iterator();i.hasNext();){
Entry<Integer,DBElement> entry=(Entry<Integer,DBElement>)i.next();
int keyurl=entry.getKey();
DBElement e=entry.getValue();
if(e.flag==2){
insertStm.setInt(1, keyurl);
insertStm.setInt(2,e.hits);
insertStm.setInt(3, e.keyip);
insertStm.addBatch();
num++;
insertAble=true;
}
}
if(insertAble)
insertStm.executeBatch();
insertStm.close();
PreparedStatement updateStm=con.prepareStatement("use crawler;update visited set hits=? where keyurl=?;");
for(Iterator i=urlDBMap.entrySet().iterator();i.hasNext();){
Entry<Integer,DBElement> entry=(Entry<Integer,DBElement>)i.next();
int keyurl=entry.getKey();
DBElement e=entry.getValue();
if(e.flag==1){
updateStm.setInt(1, e.hits);
updateStm.setInt(2, keyurl);
updateStm.addBatch();
updateAble=true;
}
}
if(updateAble)
updateStm.executeBatch();
updateStm.close();
con.commit();
}catch(Exception e){
e.printStackTrace();
}
return num;
}
/*
* read from database,if the number of the record
* which belong to the ip exceed the MAXIP,just read
* half of it from the database
*/
public HashMap<Integer,DBElement> readFromDB(int ip,boolean exceed){
HashMap<Integer,DBElement> urlMap=new HashMap<Integer,DBElement>();
String sql=null;
int count=MAXIP/2;
if(exceed)
sql="select top "+count+" keyurl,hits from visited where keyip=?;";
else
sql="select keyurl,hits from visited where keyip=?;";
try{
PreparedStatement stm=con.prepareStatement(sql);
stm.setInt(1, ip);
ResultSet rs=stm.executeQuery();
while(rs.next())
urlMap.put(rs.getInt(1),new DBElement(rs.getInt(2)));
rs.close();
stm.close();
}catch(Exception e){
e.printStackTrace();
}
return urlMap;
}
/*
* insert into the database directly
*/
private boolean toDBDirect(int keyurl,DBElement dbe){
boolean contain=false;
try{
Statement stm=con.createStatement();
String sql=null;
ResultSet rs=stm.executeQuery("use crawler;select hits from visited where keyurl="+keyurl+";");
if(rs.next()){
contain=true;
int hits=rs.getInt(1)+dbe.hits;
sql="use crawler;update visited set hits="+hits+" where keyurl="+keyurl+";";
}else{
contain=false;
sql="use crawler;insert into visited values("+keyurl+","+dbe.hits+","+dbe.keyip+");";
}
stm.executeUpdate(sql);
rs.close();
stm.close();
}catch(Exception e){
e.printStackTrace();
}
return contain;
}
/*
* store the cache data
*/
public void store(){
while(toDB(0));
}
/*
* tool classes
*/
private class Element{
public Element(int i,int h){ip=i;hits=h;}
public int ip;
public int hits;
}
private class Int{
public Int(int v){value=v;}
public int value=0;
}
private class DBElement{
public DBElement(int h,int k,int f){hits=h;keyip=k;flag=f;}
public DBElement(int h){hits=h;}
public int hits=0;
public int keyip=0;
/*
* 0 stand for not change
* 1 stand for change
* 2 stand for a new record
*/
public int flag=0;;
}
private class MyComparator implements Comparator{
public int compare(Object o1,Object o2){
Element e1=(Element)o1;
Element e2=(Element)o2;
if(e1.hits<e1.hits)
return -1;
else if(e1.hits>e2.hits)
return 1;
else
return 0;
}
}
}