對於比較大型的爬蟲來說,URL管理的管理是個核心問題,管理不好,就可能重複下載,也可能遺漏下載。這裡,我們設計一個URL Pool來管理URL。
這個URL Pool就是一個生產者-消費者模式:
依葫蘆畫瓢,URLPool就是這樣的
我們從網址池的使用目的出發來設計網址池的介面,它應該具有以下功能:
- 往池子裡面新增URL;
- 從池子裡面取URL以下載;
- 池子內部要管理URL狀態;
前面我提到URL的狀態有以下4中:
- 已經下載成功
- 下載多次失敗無需再下載
- 正在下載
- 下載失敗要再次嘗試
前兩個是永久狀態,也就是已經下載成功的不再下載,多次嘗試後仍失敗的也就不再下載,它們需要永久儲存起來,以便爬蟲重啟後,這種永久狀態記錄不會消失,已經成功下載的URL不再被重複下載。永久儲存的方法有很多種:
比如,直接寫入文字檔案,但它不利於查詢某個URL是否已經存在文字中;
比如,直接寫入MySQL等關係型資料庫,它利用查詢,但是速度又比較慢;
比如,使用key-value資料庫,查詢和速度都符合要求,是不錯的選擇!
我們這個URL Pool選用LevelDB來作為URL狀態的永久儲存。LevelDB是Google開源的一個key-value資料庫,速度非常快,同時自動壓縮資料。我們用它先來實現一個UrlDB作為永久儲存資料庫。
UrlDB 的實現
import leveldb
class UrlDB:
'''Use LevelDB to store URLs what have been done(succeed or faile)
'''
status_failure = b'0'
status_success = b'1'
def __init__(self, db_name):
self.name = db_name + '.urldb'
self.db = leveldb.LevelDB(self.name)
def load_from_db(self, status):
urls = []
for url, _status in self.db.RangeIter():
if status == _status:
urls.append(url)
return urls
def set_success(self, url):
if isinstance(url, str):
url = url.encode('utf8')
try:
self.db.Put(url, self.state_success)
s = True
except:
s = False
return s
def set_failure(self, url):
if isinstance(url, str):
url = url.encode('utf8')
try:
self.db.Put(url, self.status_failure)
s = True
except:
s = False
return s
def has(self, url):
if isinstance(url, str):
url = url.encode('utf8')
try:
attr = self.db.Get(url)
return attr
except:
pass
return False
UrlDB將被UrlPool使用,主要有三個方法被使用:
- has(url) 檢視是否已經存在某url
- set_success(url) 儲存url狀態為成功
- set_failure(url) 儲存url狀態為失敗
UrlPool 的實現
而正在下載和下載失敗次數這兩個URL的狀態只需暫時儲存在內容即可,我們把它們放到UrlPool這個類中進行管理。接著我們來實現網址池:
#Author: veelion
import pickle
import leveldb
import time
import urllib.parse as urlparse
class UrlPool:
'''URL Pool for crawler to manage URLs
'''
def __init__(self, pool_name):
self.name = pool_name
self.db = UrlDB(pool_name)
self.pool = {} # host: set([urls]), 記錄待下載URL
self.pending = {} # url: pended_time, 記錄已被pend但還未被更新狀態(正在下載)的URL
self.failure = {} # url: times, 記錄失敗的URL的次數
self.failure_threshold = 3
self.pending_threshold = 60 # pending的最大時間,過期要重新下載
self.in_mem_count = 0
self.max_hosts = ['', 0] # [host: url_count] 目前pool中url最多的host及其url數量
self.hub_pool = {} # {url: last_query_time}
self.hub_refresh_span = 0
self.load_cache()
def load_cache(self,):
path = self.name + '.pkl'
try:
with open(path, 'rb') as f:
self.pool = pickle.load(f)
cc = [len(v) for k, v in self.pool]
print('saved pool loaded! urls:', sum(cc))
except:
pass
def set_hubs(self, urls, hub_refresh_span):
self.hub_refresh_span = hub_refresh_span
self.hub_pool = {}
for url in urls:
self.hub_pool[url] = 0
def set_status(self, url, status_code):
if url in self.pending:
self.pending.pop(url)
if status_code == 200:
self.db.set_success(url)
return
if status_code == 404:
self.db.set_failure(url)
return
if url in self.failure:
self.failure[url] += 1
if self.failure[url] > self.failure_threshold:
self.db.set_failure(url)
self.failure.pop(url)
else:
self.add(url)
else:
self.failure[url] = 1
def push_to_pool(self, url):
host = urlparse.urlparse(url).netloc
if not host or '.' not in host:
print('try to push_to_pool with bad url:', url, ', len of ur:', len(url))
return False
if host in self.pool:
if url in self.pool[host]:
return True
self.pool[host].add(url)
if len(self.pool[host]) > self.max_hosts[1]:
self.max_hosts[1] = len(self.pool[host])
self.max_hosts[0] = host
else:
self.pool[host] = set([url])
self.in_mem_count += 1
return True
def add(self, url, always):
if always:
return self.push_to_pool(url)
pended_time = self.pending.get(url, 0)
if time.time() - pended_time < self.pending_threshold:
print('being downloading:', url)
return
if self.db.has(url):
return
if pended_time:
self.pending.pop(url)
return self.push_to_pool(url)
def addmany(self, urls, always=False):
if isinstance(urls, str):
print('urls is a str !!!!', urls)
self.add(urls, always)
else:
for url in urls:
self.add(url, always)
def pop(self, count, hubpercent=50):
print('\n\tmax of host:', self.max_hosts)
# 取出的url有兩種型別:hub=1, 普通=2
url_attr_url = 0
url_attr_hub = 1
# 1. 首先取出hub,保證獲取hub裡面的最新url.
hubs = {}
hub_count = count * hubpercent // 100
for hub in self.hub_pool:
span = time.time() - self.hub_pool[hub]
if span < self.hub_refresh_span:
continue
hubs[hub] = url_attr_hub # 1 means hub-url
self.hub_pool[hub] = time.time()
if len(hubs) >= hub_count:
break
# 2. 再取出普通url
# 如果某個host有太多url,則每次可以取出3(delta)個它的url
if self.max_hosts[1] * 10 > self.in_mem_count:
delta = 3
print('\tset delta:', delta, ', max of host:', self.max_hosts)
else:
delta = 1
left_count = count - len(hubs)
urls = {}
for host in self.pool:
if not self.pool[host]:
# empty_host.append(host)
continue
if self.max_hosts[0] == host:
while delta > 0:
url = self.pool[host].pop()
self.max_hosts[1] -= 1
if not self.pool[host]:
break
delta -= 1
else:
url = self.pool[host].pop()
urls[url] = url_attr_url
self.pending[url] = time.time()
if len(urls) >= left_count:
break
self.in_mem_count -= len(urls)
print('To pop:%s, hubs: %s, urls: %s, hosts:%s' % (count, len(hubs), len(urls), len(self.pool)))
urls.update(hubs)
return urls
def size(self,):
return self.in_mem_count
def empty(self,):
return self.in_mem_count == 0
def __del__(self):
path = self.name + '.pkl'
try:
with open(path, 'wb') as f:
pickle.dump(self.pool, f)
print('self.pool saved!')
except:
pass
UrlPool的實現有些複雜,且聽我一一分解。
UrlPool 的使用
先看看它的主要成員及其用途:
- self.db 是一個UrlDB的示例,用來永久儲存url的永久狀態
- self.pool 是用來存放url的,它是一個字典(dict)結構,key是url的host,value是一個用來儲存這個host的所有url的集合(set)。
- self.pending 用來管理正在下載的url狀態。它是一個字典結構,key是url,value是它被pop的時間戳。當一個url被pop()時,就是它被下載的開始。當該url被set_status()時,就是下載結束的時刻。如果一個url被add() 入pool時,發現它已經被pended的時間超過pending_threshold時,就可以再次入庫等待被下載。否則,暫不入池。
- self.failue 是一個字典,key是url,value是識別的次數,超過failure_threshold就會被永久記錄為失敗,不再嘗試下載。
- hub_pool 是一個用來儲存hub頁面的字典,key是hub url,value是上次重新整理該hub頁面的時間.
以上成員就構成了我們這個網址池的資料結構,再通過以下成員方法對這個網址池進行操作:
1. load_cache() 和 dump_cache() 對網址池進行快取
load_cache() 在 init()中呼叫,建立pool的時候,嘗試去載入上次退出時快取的URL pool;
dump_cache() 在 del() 中呼叫,也就是在網址池銷燬前(比如爬蟲意外退出),把記憶體中的URL pool快取到硬碟。
這裡使用了pickle 模組,這是一個把記憶體資料序列化到硬碟的工具。
** 2. set_hubs() 方法設定hub URL**
hub網頁就是像百度新聞那樣的頁面,整個頁面都是新聞的標題和連結,是我們真正需要的新聞的聚合頁面,並且這樣的頁面會不斷更新,把最新的新聞聚合到這樣的頁面,我們稱它們為hub頁面,其URL就是hub url。在新聞爬蟲中新增大量的這樣的url,有助於爬蟲及時發現並抓取最新的新聞。
該方法就是將這樣的hub url列表傳給網址池,在爬蟲從池中取URL時,根據時間間隔(self.hub_refresh_span)來取hub url。
** 3. add(), addmany(), push_to_pool() 對網址池進行入池操作**
把url放入網址池時,先檢查記憶體中的self.pending是否存在該url,即是否正在下載該url。如果正在下載就不入池;如果正下載或已經超時,就進行到下一步;
接著檢查該url是否已經在leveldb中存在,存在就表明之前已經成功下載或徹底失敗,不再下載了也不入池。如果沒有則進行到下一步;
最後通過push_to_pool() 把url放入self.pool中。存放的規則是,按照url的host進行分類,相同host的url放到一起,在取出時每個host取一個url,儘量保證每次取出的一批url都是指向不同的伺服器的,這樣做的目的也是為了儘量減少對抓取目標伺服器的請求壓力。力爭做一個伺服器友好的爬蟲 O(∩_∩)O
** 4. pop() 對網址池進行出池操作**
爬蟲通過該方法,從網址池中獲取一批url去下載。取出url分兩步:
第一步,先從self.hub_pool中獲得,方法是遍歷hub_pool,檢查每個hub-url距上次被pop的時間間隔是否超過hub頁面重新整理間隔(self.hub_refresh_span),來決定hub-url是否應該被pop。
第二步,從self.pool中獲取。前面push_to_pool中,介紹了pop的原則,就是每次取出的一批url都是指向不同伺服器的,有了self.pool的特殊資料結構,安裝這個原則獲取url就簡單了,按host(self.pool的key)遍歷self.pool即可。
** 5. set_status() 方法設定網址池中url的狀態**
其引數status_code 是http響應的狀態碼。爬蟲在下載完URL後進行url狀態設定。
首先,把該url成self.pending中刪除,已經下載完畢,不再是pending狀態;
接著,根據status_code來設定url狀態,200和404的直接設定為永久狀態;其它status就記錄失敗次數,並再次入池進行後續下載嘗試。
通過以上成員變數和方法,我們把這個網址池(UrlPool)解析的清清楚楚。小猿們可以毫不客氣的收藏起來,今後在寫爬蟲時可以用它方便的管理URL,並且這個實現只有一個py檔案,方便加入到任何專案中。
爬蟲知識點
1. 網址的管理
網址的管理,其目的就是為了:不重抓,不漏抓。
2. pickle 模組
把記憶體資料儲存到硬碟,再把硬碟資料重新載入到記憶體,這是很多程式停止和啟動的必要步驟。pickle就是實現資料在記憶體和硬碟之間轉移的模組。
3. leveldb 模組
這是一個經典且強大的硬碟型key-value資料庫,非常適合url-status這種結構的儲存。
4. urllib.parse
解析網址的模組,在處理url時首先想到的模組就應該是它。
下一篇我們把mysql再封裝一下。
我的公眾號:猿人學 Python 上會分享更多心得體會,敬請關注。
***版權申明:若沒有特殊說明,文章皆是猿人學 yuanrenxue.com 原創,沒有猿人學授權,請勿以任何形式轉載。***