Python爬蟲開發(fā)(三-續(xù)):快速線程池爬蟲

0×00 簡介

本文算是填前面的一個(gè)坑,有朋友和我將我前面寫了這么多,真正沒看到什么特別突出的實(shí)戰(zhàn),給了應(yīng)對各種情況的方案。多線程那里講的也是坑。忽然想想,說的也對,為讀者考慮我確實(shí)應(yīng)該把多線程這里的坑補(bǔ)完。

本人對于Python學(xué)習(xí)創(chuàng)建了一個(gè)小小的學(xué)習(xí)圈子,為各位提供了一個(gè)平臺(tái),大家一起來討論學(xué)習(xí)Python。歡迎各位到來Python學(xué)習(xí)群:960410445一起討論視頻分享學(xué)習(xí)。Python是未來的發(fā)展方向,正在挑戰(zhàn)我們的分析能力及對世界的認(rèn)知方式,因此,我們與時(shí)俱進(jìn),迎接變化,并不斷的成長,掌握Python核心技術(shù),才是掌握真正的價(jià)值所在。

然后決定再以一篇文章的形式講一下這個(gè)輕型線程池爬蟲,同時(shí)也為大家提供一個(gè)思路。代碼都是經(jīng)過調(diào)試的,并且留了相對友好的用戶接口??梢院苋菀椎锰砑痈鞣N各樣增強(qiáng)型的功能。

0×01 功能定義

1. ?可選擇的單頁面爬蟲與多頁面線程池爬蟲

2. ?可定制對HTML的處理

3. ?可定制獲取HTML的方式(應(yīng)對動(dòng)態(tài)頁面)

4. ?當(dāng)設(shè)置為非單頁面爬蟲時(shí),自動(dòng)啟動(dòng)對當(dāng)前域名下所有的頁面進(jìn)行深度優(yōu)先爬取

5. ?自定義線程數(shù)

0×02 總體流程

0×03 線程池任務(wù)迭代實(shí)現(xiàn)

雖然在上面圖中寫出了線程池的影子,但是我們還是需要單獨(dú)拿出來寫一下線程池的到底是怎么樣工作的,以方便讀者更好地理解源代碼的內(nèi)容。

0×04 具體實(shí)現(xiàn)

到這里相信讀者知道用線程池來完成我們需要完成的爬蟲了吧。關(guān)于具體內(nèi)容的實(shí)現(xiàn),是接下來我們要講的。

1. ? ?依賴:

我們需要用到這五個(gè)模塊,我相信大家都很熟悉,那么就不多介紹了,如果有朋友不熟悉的話可以翻到前面的文章重新復(fù)習(xí)一下。

threading

Queue

urlparse

requests

bs4

2. ? ?類的聲明:

ScraperWorkerBase這個(gè)類是完全可以復(fù)寫的,只要和原有的接口保持一致,可以滿足用戶的各種各樣的需求,例如,定義頁面掃描函數(shù)需要復(fù)寫parse方法(當(dāng)然這些我是在后面會(huì)有實(shí)例給大家展示)

那么我們還需要介紹一下其他的接口:

Execute方法中控制主邏輯,用最簡潔的語言和代碼表現(xiàn)邏輯,如果有需要自定義自己的邏輯控制方法,那么務(wù)必保持第一個(gè)返回值仍然是inpage_url

__get_html_data控制獲取html數(shù)據(jù)的方法,你可以自己定制headers,cookies,post_data

__get_soup這個(gè)方法是以bs4模塊來解析html文檔

__get_all_url與__get_url_inpage不建議大家修改,如果修改了的話可能會(huì)影響主爬蟲控制器的運(yùn)行

然后我在這里做一張ScraperWorkerBase的流程圖大家可以參考一下

classScraperWorkerBase(object):"""

??? No needs to learn how is work,

??? rewrite parse_page using self.soup(Beautiful), and return result,

??? you can get the result by using


??????? (inpage_urls, your_own_result) urlscraper.execute()


??? But this class is default for scraper to use,

??? To enhance its function , you can completement this class

??? like:


??? class MyWorker(ScraperWorkerBase):


??????? def parse_page(self):

??????????? all_tags = self.soup.find_all('img')

??????????? for i in all_tags:

??????????????? print i


??? """def__init__(self, url =''):self.target_url = url??????? self.netloc = urlparse.urlparse(self.target_url)[1]?????? ??????? ???????? self.response =Noneself.soup =Noneself.url_in_site = []??????? self.url_out_site = []"""override this method to get html data via any way you want or need"""def__get_html_data(self):try:??????????? self.response = requests.get(self.target_url, timeout =5)except:return""print"[_] Got response"returnself.response.textdef__get_soup(self):text = self.__get_html_data()iftext =='':return[]returnbs4.BeautifulSoup(text)def__get_all_url(self):url_lists = []?????? ???????? self.soup = self.__get_soup()ifisinstance(self.soup, type(None)):return[]?????? ???????? all_tags = self.soup.findAll("a")forainall_tags:try:#print a['href']url_lists.append(a["href"])except:passreturnurl_listsdefget_urls_inpage(self):ret_list = self.__get_all_url()ifret_list == []:return([],[])else:forurlinret_list:??????????????? o = urlparse.urlparse(url)##print urlifself.netlocino[1]:??????????????????? self.url_in_site.append(o.geturl())else:??????????????????? self.url_out_site.append(o.geturl())?????????????????? ???????? inurlset = set(self.url_in_site)?????????? ???????? outurlset = set(self.url_out_site)returninurlset, outurlsetdefexecute(self):inpage_url = self.get_urls_inpage()??????? undefined_result = self.parse_page()returninpage_url, undefined_result"""You can override this method to define your own needs"""defparse_page(self):pass

這個(gè)類定義了處理HTML頁面的基本方法,如果需要僅僅是獲取頁面所有的超鏈接的話,那么最基礎(chǔ)的Worker類已經(jīng)替大家實(shí)現(xiàn)了,但是如果需要對某類網(wǎng)站特定元素進(jìn)行處理,那么完全可以只復(fù)寫parse_page

例如:

如果要繞開網(wǎng)站的限制進(jìn)行爬取數(shù)據(jù),就需要復(fù)寫:

但是如果需要對特定url進(jìn)行限制,最好不要去復(fù)寫__get_all_url方法,而應(yīng)該去復(fù)寫get_urls_inpage方法

關(guān)于Scraper的類說明:

這個(gè)類顯然沒有前面的那么好理解,但是如果使用過HTMLParser或者是SGMLParser的讀者,肯定是記得那個(gè)feed方法的。這與我們要介紹的這個(gè)類有一些相似的地方。

在這個(gè)類中,我們建立Scraper對象的時(shí)候,需要傳入的參數(shù)直接決定了我們的線程池爬蟲的類型:究竟要不要啟動(dòng)多線程,啟動(dòng)多少個(gè)線程,使用哪個(gè)處理函數(shù)來除了web頁面?這些都是我們要考慮的問題。所以接下來我們對這些部分進(jìn)行一些說明

這些設(shè)定很好理解,在__init__中輸入是否是單頁面爬蟲模式,設(shè)定線程數(shù),設(shè)定爬蟲解析的具體類。然后對應(yīng)初始化線程池:初始化的時(shí)候要生成多個(gè)_worker方法,循環(huán)工作,然后在_worker方法的工作時(shí)完成對傳入的實(shí)際進(jìn)行解析的ScraperWorkerBase類進(jìn)行調(diào)用,然后收集結(jié)果填入任務(wù)隊(duì)列。

通過feed的方法來添加目標(biāo)url,可以輸入list,也可以直接輸入str對象。

當(dāng)不想讓Scraper再工作的時(shí)候,調(diào)用kill_workers就可以停止所有的worker線程。

但是僅僅是明白這個(gè)只是可能僅僅會(huì)使用而已,既然是開發(fā)我們肯定是要清楚地講這個(gè)Scraper是怎么樣被組織起來的,他是怎么樣工作的。

首先第一個(gè)概念就是任務(wù)隊(duì)列:我們feed進(jìn)的數(shù)據(jù)實(shí)際就是把任務(wù)添加到任務(wù)隊(duì)列中,然后任務(wù)分配的時(shí)候,每個(gè)爬蟲都要get到屬于自己的任務(wù),然后各司其職的去做,互不干擾。

第二個(gè)類似的概念就是結(jié)果隊(duì)列:結(jié)果隊(duì)列毫無疑問就是用于存儲(chǔ)結(jié)果的,在外部獲取這個(gè)Scraper的結(jié)果隊(duì)列以后,需要去獲取結(jié)果隊(duì)列中的元素,由于隊(duì)列的性質(zhì),當(dāng)結(jié)果被抽走的時(shí)候,被獲取的結(jié)果就會(huì)被刪除。

在大家明確了這兩個(gè)概念以后,這個(gè)Scraper的工作原理接回很容易被理解了:

當(dāng)然這個(gè)圖我在做的時(shí)候是有點(diǎn)小偷懶的,本來應(yīng)該做兩種類型的Scraper,因?yàn)閷?shí)際在使用的過程中,Scraper在一開始要被指定為單頁還是多頁,但是為了避免大量的重復(fù)所以在作圖的時(shí)候我就在最后做了一個(gè)邏輯判斷來表明類型,來幫助大家理解這個(gè)解析過程。我相信一個(gè)visio流程圖比長篇大論的文字解釋要直觀的多對吧?

那么接下來我們看一下爬蟲的實(shí)體怎么寫:

classScraper(object):

def__init__(self, single_page = True,? workers_num =8, worker_class = ScraperWorkerBase):

self.count =0

??????? self.workers_num = workers_num


"""get worker_class"""

??????? self.worker_class = worker_class


"""check if the workers should die"""

self.all_dead =False


"""store the visited pages"""

??????? self.visited = set()


"""by ScraperWorkerBase 's extension result queue"""

??????? self.result_urls_queue = Queue.Queue()

??????? self.result_elements_queue = Queue.Queue()


"""

??????? if single_page == True,

??????? the task_queue should store the tasks (unhandled)

??????? """

??????? self.task_queue = Queue.Queue()


??????? self.single_page = single_page

ifself.single_page ==False:

??????????? self.__init_workers()

else:

??????????? self.__init_single_worker()


def__check_single_page(self):

ifself.single_page ==True:

raiseStandardError('[!] Single page won\'t allow you use many workers')


"""init worker(s)"""

def__init_single_worker(self):

??????? ret = threading.Thread(target=self._single_worker)

??????? ret.start()

def__init_workers(self):

??????? self.__check_single_page()


for_inrange(self.workers_num):

??????????? ret = threading.Thread(target=self._worker)

??????????? ret.start()

"""return results"""

defget_result_urls_queue(self):

returnself.result_urls_queue

defget_result_elements_queue(self):

returnself.result_elements_queue


"""woker function"""

def_single_worker(self):

ifself.all_dead !=False:

self.all_dead =False

scraper =None

whilenotself.all_dead:

try:


url = self.task_queue.get(block=True)

print'Workding', url

try:

ifurl[:url.index('#')]inself.visited:

continue

except:

pass


ifurlinself.visited:

continue

else:

pass

self.count = self.count+1

print'Having process', self.count ,'Pages'

??????????????? scraper = self.worker_class(url)

??????????????? self.visited.add(url)

??????????????? urlset, result_entity = scraper.execute()

foriinurlset[0]:

#self.task_queue.put(i)

??????????????????? self.result_urls_queue.put(i)


??????????????? if result_entity != None:

??????????????????? pass

??????????????? else:

??????????????????? self.result_elements_queue.put(result_entity)


??????????? except:

??????????????? pass?????????? ?

??????????? finally:

??????????????? pass?????? ?

??? def _worker(self):

??????? if self.all_dead != False:

??????????? self.all_dead = False

??????? scraper = None

??????? while not self.all_dead:

??????????? try:


??????????????? url = self.task_queue.get(block=True)

??????????????? print 'Workding', url

??????????????? try:

??????????????????? if url[:url.index('#')] in self.visited:

??????????????????????? continue

??????????????? except:

??????????????????? pass


??????????????? if url in self.visited:

??????????????????? continue

??????????????? else:

??????????????????? pass

??????????????? self.count = self.count + 1

??????????????? print 'Having process', self.count , 'Pages'

??????????????? scraper = self.worker_class(url)

??????????????? self.visited.add(url)

??????????????? urlset, result_entity = scraper.execute()

??????????????? for i in urlset[0]:

??????????????????? if i in self.visited:

??????????????????????? continue

??????????????????? else:

??????????????????????? pass

??????????????????? self.task_queue.put(i)

??????????????????? self.result_urls_queue.put(i)


??????????????? if result_entity != None:

??????????????????? pass

??????????????? else:

??????????????????? self.result_elements_queue.put(result_entity)


??????????? except:

??????????????? pass?????????? ?

??????????? finally:

??????????????? pass


??? """scraper interface"""

??? def kill_workers(self):

??????? if self.all_dead == False:

??????????? self.all_dead = True

??????? else:

??????????? pass

??? def feed(self, target_urls = []):

??????? if isinstance(target_urls, list):

??????????? for target_url in target_urls:

??????????????? self.task_queue.put(target_url)

??????? elif isinstance(target_urls, str):

??????????? self.task_queue.put(target_urls)

??????? else:

??????????? pass



??????? #return url result

??????? return (self.get_result_urls_queue(), self.get_result_elements_queue() )

這些設(shè)定很好理解,在__init__中輸入是否是單頁面爬蟲模式,設(shè)定線程數(shù),設(shè)定爬蟲解析的具體類。然后對應(yīng)初始化線程池:初始化的時(shí)候要生成多個(gè)_worker方法,循環(huán)工作,然后在_worker方法的工作時(shí)完成對傳入的實(shí)際進(jìn)行解析的ScraperWorkerBase類進(jìn)行調(diào)用,然后收集結(jié)果填入任務(wù)隊(duì)列。

通過feed的方法來添加目標(biāo)url,可以輸入list,也可以直接輸入str對象。

當(dāng)不想讓Scraper再工作的時(shí)候,調(diào)用kill_workers就可以停止所有的worker線程。

0×04 使用實(shí)例

下面是幾個(gè)相對完整的使用實(shí)例:

單頁面爬蟲使用實(shí)例

#encoding:utf-8

from scraper import *

import Queue

import time

import sys

import bs4

test_obj = Scraper(single_page=True, workers_num=15)

test_obj.feed(['http://freebuf.com'])

time.sleep(5)

z = test_obj.get_result_urls_queue()

while True:

??? try :

??????? print z.get(timeout=4)

??? except:

??????? pass

線程池爬蟲實(shí)例:

尋找一個(gè)網(wǎng)站下所有的url

#encoding:utf-8

from scraper import *

import Queue

import time

import sys

import bs4

test_obj = Scraper(single_page=False, workers_num=15)

test_obj.feed(['http://freebuf.com'])

time.sleep(5)

z = test_obj.get_result_urls_queue()

while True:

??? try :

??????? print z.get(timeout=4)

??? except:

??????? pass

我們發(fā)現(xiàn)和上面的單頁面爬蟲只是一個(gè)參數(shù)的區(qū)別。實(shí)際的效果還是不錯(cuò)的。

下面是自定義爬取方案的應(yīng)用

這樣的示例代碼基本把這個(gè)爬蟲的目的和接口完整的展示出來了,用戶可以在MyWorker中定義自己的處理函數(shù)。

0×05 測試使用

在實(shí)際的使用中,這個(gè)小型爬蟲的效果還是相當(dāng)不錯(cuò)的,靈活,簡單,可擴(kuò)展性高。有興趣的朋友可以給它配置更多的功能型組件,比如數(shù)據(jù)庫,爬取特定關(guān)鍵元素,針對某一個(gè)頁面的數(shù)據(jù)處理。比如在實(shí)際的使用中,這個(gè)模塊作為我自己正在編寫的一個(gè)xss_fuzz工具的一個(gè)部分而存在。

下面給出一些測試數(shù)據(jù)供大家參考(在普通網(wǎng)絡(luò)狀況):

這個(gè)結(jié)果是在本機(jī)上測試的結(jié)果,在不同的電腦商測試結(jié)果均不同,8線程是比較小的線程數(shù)目,有興趣的朋友可以采用16線程或者是更多的線程測試,效果可能更加明顯,如果為了防止頁面卡死,可以在worker中設(shè)置超時(shí)時(shí)間,一旦有那個(gè)頁面一時(shí)間很難打開也能很快轉(zhuǎn)換到新的頁面,同樣也能提高效率。

0×06 結(jié)語

關(guān)于爬蟲的開發(fā),我相信到現(xiàn)在,大家都已經(jīng)沒有什么問題了,如果要問網(wǎng)站爬行時(shí)候什么的頁面權(quán)重怎么處理,簡單無非是在爬蟲過程中計(jì)算某個(gè)頁面被多少頁面所指(當(dāng)然這個(gè)算法沒有這么簡單),并不是什么很高深的技術(shù),如果有興趣的小伙伴仍然可以去深入學(xué)習(xí),大家都知道搜索引擎的核心也是爬蟲技術(shù)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容