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