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

0×00 簡介

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

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

然后決定再以一篇文章的形式講一下這個輕型線程池爬蟲辽慕,同時(shí)也為大家提供一個思路。代碼都是經(jīng)過調(diào)試的赦肃,并且留了相對友好的用戶接口溅蛉」耍可以很容易得添加各種各樣增強(qiáng)型的功能。

0×01 功能定義

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

2. ?可定制對HTML的處理

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

4. ?當(dāng)設(shè)置為非單頁面爬蟲時(shí)船侧,自動啟動對當(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. ? ?依賴:

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

threading

Queue

urlparse

requests

bs4

2. ? ?類的聲明:

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

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

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

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

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

__get_all_url與__get_url_inpage不建議大家修改蛤肌,如果修改了的話可能會影響主爬蟲控制器的運(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

這個類定義了處理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的類說明:

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

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

這些設(shè)定很好理解件豌,在__init__中輸入是否是單頁面爬蟲模式疮方,設(shè)定線程數(shù),設(shè)定爬蟲解析的具體類茧彤。然后對應(yīng)初始化線程池:初始化的時(shí)候要生成多個_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線程许蓖。

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

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

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

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

當(dāng)然這個圖我在做的時(shí)候是有點(diǎn)小偷懶的醉旦,本來應(yīng)該做兩種類型的Scraper,因?yàn)閷?shí)際在使用的過程中桨啃,Scraper在一開始要被指定為單頁還是多頁车胡,但是為了避免大量的重復(fù)所以在作圖的時(shí)候我就在最后做了一個邏輯判斷來表明類型,來幫助大家理解這個解析過程照瘾。我相信一個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í)候要生成多個_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í)例

下面是幾個相對完整的使用實(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í)例:

尋找一個網(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)和上面的單頁面爬蟲只是一個參數(shù)的區(qū)別。實(shí)際的效果還是不錯的建钥。

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

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

0×05 測試使用

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

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

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

0×06 結(jié)語

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末遭赂,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子横辆,更是在濱河造成了極大的恐慌撇他,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,252評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件狈蚤,死亡現(xiàn)場離奇詭異困肩,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)脆侮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評論 3 399
  • 文/潘曉璐 我一進(jìn)店門锌畸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人靖避,你說我怎么就攤上這事潭枣。” “怎么了幻捏?”我有些...
    開封第一講書人閱讀 168,814評論 0 361
  • 文/不壞的土叔 我叫張陵盆犁,是天一觀的道長。 經(jīng)常有香客問我篡九,道長谐岁,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,869評論 1 299
  • 正文 為了忘掉前任榛臼,我火速辦了婚禮伊佃,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘沛善。我一直安慰自己航揉,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,888評論 6 398
  • 文/花漫 我一把揭開白布路呜。 她就那樣靜靜地躺著迷捧,像睡著了一般。 火紅的嫁衣襯著肌膚如雪胀葱。 梳的紋絲不亂的頭發(fā)上漠秋,一...
    開封第一講書人閱讀 52,475評論 1 312
  • 那天,我揣著相機(jī)與錄音抵屿,去河邊找鬼庆锦。 笑死,一個胖子當(dāng)著我的面吹牛轧葛,可吹牛的內(nèi)容都是我干的搂抒。 我是一名探鬼主播艇搀,決...
    沈念sama閱讀 41,010評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼求晶!你這毒婦竟也來了焰雕?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,924評論 0 277
  • 序言:老撾萬榮一對情侶失蹤芳杏,失蹤者是張志新(化名)和其女友劉穎矩屁,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體爵赵,經(jīng)...
    沈念sama閱讀 46,469評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡吝秕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,552評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了空幻。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片烁峭。...
    茶點(diǎn)故事閱讀 40,680評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖秕铛,靈堂內(nèi)的尸體忽然破棺而出约郁,到底是詐尸還是另有隱情,我是刑警寧澤但两,帶...
    沈念sama閱讀 36,362評論 5 351
  • 正文 年R本政府宣布棍现,位于F島的核電站,受9級特大地震影響镜遣,放射性物質(zhì)發(fā)生泄漏己肮。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,037評論 3 335
  • 文/蒙蒙 一悲关、第九天 我趴在偏房一處隱蔽的房頂上張望谎僻。 院中可真熱鬧,春花似錦寓辱、人聲如沸艘绍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽诱鞠。三九已至,卻和暖如春这敬,著一層夾襖步出監(jiān)牢的瞬間航夺,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評論 1 274
  • 我被黑心中介騙來泰國打工崔涂, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留阳掐,地道東北人。 一個月前我還...
    沈念sama閱讀 49,099評論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像缭保,于是被迫代替她去往敵國和親汛闸。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,691評論 2 361

推薦閱讀更多精彩內(nèi)容