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ù)。