溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

如何實現(xiàn)網(wǎng)址池URL Pool

發(fā)布時間:2022-01-11 10:29:09 來源:億速云 閱讀:144 作者:柒染 欄目:編程語言

今天就跟大家聊聊有關(guān)如何實現(xiàn)網(wǎng)址池URL Pool,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結(jié)了以下內(nèi)容,希望大家根據(jù)這篇文章可以有所收獲。

對于比較大型的爬蟲來說,URL管理的管理是個核心問題,管理不好,就可能重復(fù)下載,也可能遺漏下載。這里,我們設(shè)計一個URL池來管理URL。
這個URL池就是一個生產(chǎn)者 - 消費者模式:

如何實現(xiàn)網(wǎng)址池URL Pool

生產(chǎn)者 - 消費者流程圖

依葫蘆畫瓢,URLPool就是這樣的

如何實現(xiàn)網(wǎng)址池URL Pool

設(shè)計的網(wǎng)絡(luò)爬蟲URLPool

我們從網(wǎng)址池的使用目的出發(fā)來設(shè)計網(wǎng)址池的接口,它應(yīng)該具有以下功能:

  • 往池子里面添加URL;

  • 從池子里面取URL以下載;

  • 池子內(nèi)部要管理URL狀態(tài);

前面我提到的網(wǎng)址的狀態(tài)有以下4中:

  • 已經(jīng)下載成功

  • 下載多次失敗無需再下載

  • 正在下載

  • 下載失敗要再次嘗試

前兩個是永久狀態(tài),也就是已經(jīng)下載成功的不再下載,多次嘗試后仍失敗的也就不再下載,它們需要永久存儲起來,以便爬蟲重啟后,這種永久狀態(tài)記錄不會消失,已經(jīng)成功下載的網(wǎng)址不再被重復(fù)下載永久存儲的方法有很多種:

比如,直接寫入文本文件,但它不利于查找某個URL是否已經(jīng)存在文本中;
比如,直接寫入的MySQL關(guān)系型數(shù)據(jù)庫,它利用查找,但是速度又比較慢,
比如,使用鍵值數(shù)據(jù)庫,查找和速度都符合要求,是不錯的選擇!

我們這個URL池選用LevelDB來作為URL狀態(tài)的永久存儲.LevelDB是谷歌開源的一個鍵值數(shù)據(jù)庫,速度非???,同時自動壓縮數(shù)據(jù)。我們用它先來實現(xiàn)一個UrlDB作為永久存儲數(shù)據(jù)庫。

UrlDB的實現(xiàn)

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)查看是否已經(jīng)存在某url

  • set_success(url)存儲url狀態(tài)為成功

  • set_failure(url)存儲url狀態(tài)為失敗

UrlPool的實現(xiàn)

而正在下載和下載失敗次數(shù)這兩個URL的狀態(tài)只需暫時保存在內(nèi)容即可,我們把它們放到UrlPool這個類中進(jìn)行管理接著我們來實現(xiàn)網(wǎng)址池:

#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但還未被更新狀態(tài)(正在下載)的URL
        self.failure = {}  # url: times, 記錄失敗的URL的次數(shù)
        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數(shù)量
        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的實現(xiàn)有些復(fù)雜,且聽我一一分解。

UrlPool的使用

先看看它的主要成員及其用途:

  • self.db是一個UrlDB的示例,用來永久存儲url的永久狀態(tài)

  • self.pool是用來存放url的,它是一個字典(dict)結(jié)構(gòu),key是url的主機(jī),值是一個用來存儲這個主機(jī)的所有url的集合(set)。

  • self.pending用來管理正在下載的url狀態(tài)。它是一個字典結(jié)構(gòu),key是url,value是它被pop的時間戳。當(dāng)一個url被pop()時,就是它被下載的開始。當(dāng)該url被set_status()時,就是下載結(jié)束的時刻。如果一個url被添加()入池時,發(fā)現(xiàn)它已經(jīng)被套的時間超過pending_threshold時,就可以再次入庫等待被下載。否則,暫不入池。

  • self.failue是一個字典,key是url,value是識別的次數(shù),超過failure_threshold就會被永久記錄為失敗,不再嘗試下載。

  • hub_pool是一個用來存儲hub page面的字典,key是hub url,value是上次刷新該hub頁面的時間。

以上成員就構(gòu)成了我們這個網(wǎng)址池的數(shù)據(jù)結(jié)構(gòu),再通過以下成員方法對這個網(wǎng)址池進(jìn)行操作:

1. load_cache()和dump_cache()對網(wǎng)址池進(jìn)行緩存
load_cache()在init()中調(diào)用,創(chuàng)建池的時候,嘗試去加載上次退出時緩存的URL池;
dump_cache()在del()中調(diào)用,也就是在網(wǎng)址池銷毀前(比如爬蟲意外退出),把內(nèi)存中的URL pool緩存到硬盤。
這里使用了pickle模塊,這是一個把內(nèi)存數(shù)據(jù)序列化到硬盤的工具。

** 2. set_hubs()方法設(shè)置hub URL **
hub網(wǎng)頁就是像百度新聞那樣的頁面,整個頁面都是新聞的標(biāo)題和鏈接,是我們真正需要的新聞的聚合頁面,并且這樣的頁面會不斷更新,把最新的新聞聚合到這樣的頁面,我們稱它們?yōu)閔ub頁面,其URL就是hub url。在新聞爬蟲中添加大量的這樣的url,有助于爬蟲及時發(fā)現(xiàn)并抓取最新的新聞。
該方法就是將這樣的hub url列表傳給網(wǎng)址池,在爬蟲從池中取URL時,根據(jù)時間間隔(self.hub_refresh_span)來取集樞紐網(wǎng)址。

** 3. add(),addmany(),push_to_pool()對網(wǎng)址池進(jìn)行入池操作**
把url放入網(wǎng)址池時,先檢查內(nèi)存中的self.pending是否存在該url,即是否正在下載該。網(wǎng)址如果正在下載就不入池;如果正下載或已經(jīng)超時,就進(jìn)行到下一步;
接著檢查該網(wǎng)址是否已經(jīng)在性LevelDB中存在,存在就表明之前已經(jīng)成功下載或徹底失敗,不再下載了也不入池。如果沒有則進(jìn)行到下一步;
最后通過push_to_pool()把url放入self.pool中。存放的規(guī)則是,按照url的主機(jī)進(jìn)行分類,相同主機(jī)的url放到一起,在取出時 -個主取一個url,盡量保證每次取出的一批url都是指向不同的服務(wù)器的,這樣做的目的也是為了盡量減少對抓取目標(biāo)服務(wù)器的請求壓力。力爭做一個服務(wù)器友好的爬蟲O(∩∩ _∩)O

4. pop()對網(wǎng)址池進(jìn)行出池操作
爬蟲通過該方法,從網(wǎng)址池中獲取一批url去下載。取出url分兩步:
第一步,先從self.hub_pool中獲得,方法是遍歷hub_pool,檢查每個集線器-URL距上次被彈出的時間間隔是否超過轂頁面刷新間隔(self.hub_refresh_span),來決定轂-URL是否應(yīng)該被彈出。
第二步,從self.pool中獲取。前面push_to_pool中,介紹了流行的原則,就是每次取出的一批URL都是指向不同服務(wù)器的,有了self.pool的特殊數(shù)據(jù)結(jié)構(gòu),安裝這個原則獲取網(wǎng)址就簡單了,按主機(jī)(自我.pool的鍵)遍歷self.pool即可。

5. set_status()方法設(shè)置網(wǎng)址池中url的狀態(tài)
其參數(shù)status_code是http響應(yīng)的狀態(tài)碼。爬蟲在下載完URL后進(jìn)行url狀態(tài)設(shè)置。
首先,把該url成self.pending中刪除,已經(jīng)下載完畢,不再是未決狀態(tài);
接著,根據(jù)STATUS_CODE來設(shè)置URL狀態(tài),200和404的直接設(shè)置為永久狀態(tài);其它狀態(tài)就記錄失敗次數(shù),并再次入池進(jìn)行后續(xù)下載嘗試。

通過以上成員變量和方法,我們把這個網(wǎng)址池(UrlPool)解析的清清楚楚。小猿們可以毫不客氣的收藏起來,今后在寫爬蟲時可以用它方便的管理URL,并且這個實現(xiàn)只有一個PY文件,方便加入到任何項目中。

爬蟲知識點

1.網(wǎng)址的管理
網(wǎng)址的管理,其目的就是為了:不重抓,不漏抓。

2. pickle模塊
把內(nèi)存數(shù)據(jù)保存到硬盤,再把硬盤數(shù)據(jù)重新加載到內(nèi)存,這是很多程序停止和啟動的必要步驟.pickle就是實現(xiàn)數(shù)據(jù)在內(nèi)存和硬盤之間轉(zhuǎn)移的模塊。

3. leveldb模塊
這是一個經(jīng)典且強(qiáng)大的硬盤型key-value數(shù)據(jù)庫,非常適合url-status這種結(jié)構(gòu)的存儲。

4. urllib.parse
解析網(wǎng)址的模塊,在處理url時首先想到的模塊就應(yīng)該是它。

看完上述內(nèi)容,你們對如何實現(xiàn)網(wǎng)址池URL Pool有進(jìn)一步的了解嗎?如果還想了解更多知識或者相關(guān)內(nèi)容,請關(guān)注億速云行業(yè)資訊頻道,感謝大家的支持。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI