
以Python的Pyspider為例剖析搜索引擎的網(wǎng)絡(luò)爬蟲實(shí)現(xiàn)方法
在這篇文章中,我們將分析一個(gè)網(wǎng)絡(luò)爬蟲。
網(wǎng)絡(luò)爬蟲是一個(gè)掃描網(wǎng)絡(luò)內(nèi)容并記錄其有用信息的工具。它能打開一大堆網(wǎng)頁,分析每個(gè)頁面的內(nèi)容以便尋找所有感興趣的數(shù)據(jù),并將這些數(shù)據(jù)存儲(chǔ)在一個(gè)數(shù)據(jù)庫中,然后對其他網(wǎng)頁進(jìn)行同樣的操作。
如果爬蟲正在分析的網(wǎng)頁中有一些鏈接,那么爬蟲將會(huì)根據(jù)這些鏈接分析更多的頁面。
搜索引擎就是基于這樣的原理實(shí)現(xiàn)的。
這篇文章中,我特別選了一個(gè)穩(wěn)定的、”年輕”的開源項(xiàng)目pyspider,它是由 binux 編碼實(shí)現(xiàn)的。
注:據(jù)認(rèn)為pyspider持續(xù)監(jiān)控網(wǎng)絡(luò),它假定網(wǎng)頁在一段時(shí)間后會(huì)發(fā)生變化,因此一段時(shí)間后它將會(huì)重新訪問相同的網(wǎng)頁。
概述
爬蟲pyspider主要由四個(gè)組件組成。包括調(diào)度程序(scheduler),抓取程序(fetcher),內(nèi)容處理程序(processor)以及一個(gè)監(jiān)控組件。
調(diào)度程序接受任務(wù)并決定該做什么。這里有幾種可能性,它可以丟棄一個(gè)任務(wù)(可能這個(gè)特定的網(wǎng)頁剛剛被抓取過了),或者給任務(wù)分配不同的優(yōu)先級(jí)。
當(dāng)各個(gè)任務(wù)的優(yōu)先級(jí)確定之后,它們被傳入抓取程序。它重新抓取網(wǎng)頁。這個(gè)過程很復(fù)雜,但邏輯上比較簡單。
當(dāng)網(wǎng)絡(luò)上的資源被抓取下來,內(nèi)容處理程序就負(fù)責(zé)抽取有用的信息。它運(yùn)行一個(gè)用戶編寫的Python腳本,這個(gè)腳本并不像沙盒一樣被隔離。它的職責(zé)還包括捕獲異?;蛉罩荆⑦m當(dāng)?shù)毓芾硭鼈儭?br />
最后,爬蟲pyspider中有一個(gè)監(jiān)控組件。
爬蟲pyspider提供一個(gè)異常強(qiáng)大的網(wǎng)頁界面(web ui),它允許你編輯和調(diào)試你的腳本,管理整個(gè)抓取過程,監(jiān)控正在進(jìn)行的任務(wù),并最終輸出結(jié)果。
項(xiàng)目和任務(wù)
在pyspider中,我們有項(xiàng)目和任務(wù)的概念。
一個(gè)任務(wù)指的是一個(gè)需要從網(wǎng)站檢索并進(jìn)行分析的單獨(dú)頁面。
一個(gè)項(xiàng)目指的是一個(gè)更大的實(shí)體,它包括爬蟲涉及到的所有頁面,分析網(wǎng)頁所需要的python腳本,以及用于存儲(chǔ)數(shù)據(jù)的數(shù)據(jù)庫等等。
在pyspider中我們可以同時(shí)運(yùn)行多個(gè)項(xiàng)目。
代碼結(jié)構(gòu)分析
根目錄
在根目錄中可以找到的文件夾有:
data,空文件夾,它是存放由爬蟲所生成的數(shù)據(jù)的地方。
docs,包含該項(xiàng)目文檔,里邊有一些markdown代碼。
pyspider,包含項(xiàng)目實(shí)際的代碼。
test,包含相當(dāng)多的測試代碼。
這里我將重點(diǎn)介紹一些重要的文件:
.travis.yml,一個(gè)很棒的、連續(xù)性測試的整合。你如何確定你的項(xiàng)目確實(shí)有效?畢竟僅在你自己的帶有固定版本的庫的機(jī)器上進(jìn)行測試是不夠的。
Dockerfile,同樣很棒的工具!如果我想在我的機(jī)器上嘗試一個(gè)項(xiàng)目,我只需要運(yùn)行Docker,我不需要手動(dòng)安裝任何東西,這是一個(gè)使開發(fā)者參與到你的項(xiàng)目中的很好的方式。
LICENSE,對于任何開源項(xiàng)目都是必需的,(如果你自己有開源項(xiàng)目的話)不要忘記自己項(xiàng)目中的該文件。
requirements.txt,在Python世界中,該文件用于指明為了運(yùn)行該軟件,需要在你的系統(tǒng)中安裝什么Python包,在任何的Python項(xiàng)目中該文件都是必須的。
run.py,該軟件的主入口點(diǎn)。
setup.py,該文件是一個(gè)Python腳本,用于在你的系統(tǒng)中安裝pyspider項(xiàng)目。
已經(jīng)分析完項(xiàng)目的根目錄了,僅根目錄就能說明該項(xiàng)目是以一種非常專業(yè)的方式進(jìn)行開發(fā)的。如果你正在開發(fā)任何的開源程序,希望你能達(dá)到這樣的水準(zhǔn)。
文件夾pyspider
讓我們更深入一點(diǎn)兒,一起來分析實(shí)際的代碼。
在這個(gè)文件夾中還能找到其他的文件夾,整個(gè)軟件背后的邏輯已經(jīng)被分割,以便更容易的進(jìn)行管理和擴(kuò)展。
這些文件夾是:database、fetcher、libs、processor、result、scheduler、webui。
在這個(gè)文件夾中我們也能找到整個(gè)項(xiàng)目的主入口點(diǎn),run.py。
文件run.py
這個(gè)文件首先完成所有必需的雜事,以保證爬蟲成功地運(yùn)行。最終它產(chǎn)生所有必需的計(jì)算單元。向下滾動(dòng)我們可以看到整個(gè)項(xiàng)目的入口點(diǎn),cli()。
函數(shù)cli()
這個(gè)函數(shù)好像很復(fù)雜,但與我相隨,你會(huì)發(fā)現(xiàn)它并沒有你想象中復(fù)雜。函數(shù)cli()的主要目的是創(chuàng)建數(shù)據(jù)庫和消息系統(tǒng)的所有連接。它主要解析命令行參數(shù),并利用所有我們需要的東西創(chuàng)建一個(gè)大字典。最后,我們通過調(diào)用函數(shù)all()開始真正的工作。
函數(shù)all()
一個(gè)網(wǎng)絡(luò)爬蟲會(huì)進(jìn)行大量的IO操作,因此一個(gè)好的想法是產(chǎn)生不同的線程或子進(jìn)程來管理所有的這些工作。通過這種方式,你可以在等待網(wǎng)絡(luò)獲取你當(dāng)前html頁面的同時(shí),提取前一個(gè)頁面的有用信息。
函數(shù)all()決定是否運(yùn)行子進(jìn)程或者線程,然后調(diào)用不同的線程或子進(jìn)程里的所有的必要函數(shù)。這時(shí)pyspider將產(chǎn)生包括webui在內(nèi)的,爬蟲的所有邏輯模塊所需要的,足夠數(shù)量的線程。當(dāng)我們完成項(xiàng)目并關(guān)閉webui時(shí),我們將干凈漂亮地關(guān)閉每一個(gè)進(jìn)程。
現(xiàn)在我們的爬蟲就開始運(yùn)行了,讓我們進(jìn)行更深入一點(diǎn)兒的探索。
調(diào)度程序
調(diào)度程序從兩個(gè)不同的隊(duì)列中獲取任務(wù)(newtask_queue和status_queue),并把任務(wù)加入到另外一個(gè)隊(duì)列(out_queue),這個(gè)隊(duì)列稍后會(huì)被抓取程序讀取。
調(diào)度程序做的第一件事情是從數(shù)據(jù)庫中加載所需要完成的所有的任務(wù)。之后,它開始一個(gè)無限循環(huán)。在這個(gè)循環(huán)中會(huì)調(diào)用幾個(gè)方法:
1._update_projects():嘗試更新的各種設(shè)置,例如,我們想在爬蟲工作的時(shí)候調(diào)整爬取速度。
2._check_task_done():分析已完成的任務(wù)并將其保存到數(shù)據(jù)庫,它從status_queue中獲取任務(wù)。
3._check_request():如果內(nèi)容處理程序要求分析更多的頁面,把這些頁面放在隊(duì)列newtask_queue中,該函數(shù)會(huì)從該隊(duì)列中獲得新的任務(wù)。
4._check_select():把新的網(wǎng)頁加入到抓取程序的隊(duì)列中。
5._check_delete():刪除已被用戶標(biāo)記的任務(wù)和項(xiàng)目。
6._try_dump_cnt():記錄一個(gè)文件中已完成任務(wù)的數(shù)量。對于防止程序異常所導(dǎo)致的數(shù)據(jù)丟失,這是有必要的。
def run(self):
while not self._quit:
try:
time.sleep(self.LOOP_INTERVAL)
self._update_projects()
self._check_task_done()
self._check_request()
while self._check_cronjob():
pass
self._check_select()
self._check_delete()
self._try_dump_cnt()
self._exceptions = 0
except KeyboardInterrupt:
break
except Exception as e:
logger.exception(e)
self._exceptions += 1
if self._exceptions > self.EXCEPTION_LIMIT:
break
continue
循環(huán)也會(huì)檢查運(yùn)行過程中的異常,或者我們是否要求python停止處理。
finally:
# exit components run in subprocess
for each in threads:
if not each.is_alive():
continue
if hasattr(each, 'terminate'):
each.terminate()
each.join()
抓取程序
抓取程序的目的是檢索網(wǎng)絡(luò)資源。
pyspider能夠處理普通HTML文本頁面和基于AJAX的頁面。只有抓取程序能意識(shí)到這種差異,了解這一點(diǎn)非常重要。我們將僅專注于普通的html文本抓取,然而大部分的想法可以很容易地移植到Ajax抓取器。
這里的想法在某種形式上類似于調(diào)度程序,我們有分別用于輸入和輸出的兩個(gè)隊(duì)列,以及一個(gè)大的循環(huán)。對于輸入隊(duì)列中的所有元素,抓取程序生成一個(gè)請求,并將結(jié)果放入輸出隊(duì)列中。
它聽起來簡單但有一個(gè)大問題。網(wǎng)絡(luò)通常是極其緩慢的,如果因?yàn)榈却粋€(gè)網(wǎng)頁而阻止了所有的計(jì)算,那么整個(gè)過程將會(huì)運(yùn)行的極其緩慢。解決方法非常的簡單,即不要在等待網(wǎng)絡(luò)的時(shí)候阻塞所有的計(jì)算。這個(gè)想法即在網(wǎng)絡(luò)上發(fā)送大量消息,并且相當(dāng)一部分消息是同時(shí)發(fā)送的,然后異步等待響應(yīng)的返回。一旦我們收回一個(gè)響應(yīng),我們將會(huì)調(diào)用另外的回調(diào)函數(shù),回調(diào)函數(shù)將會(huì)以最適合的方式管理這樣的響應(yīng)。
現(xiàn)在我們的腦海里已經(jīng)有了極好的想法了,讓我們更深入地探索這是如何實(shí)現(xiàn)的。
def run(self):
def queue_loop():
if not self.outqueue or not self.inqueue:
return
while not self._quit:
try:
if self.outqueue.full():
break
task = self.inqueue.get_nowait()
task = utils.decode_unicode_obj(task)
self.fetch(task)
except queue.Empty:
break
tornado.ioloop.PeriodicCallback(queue_loop, 100, io_loop=self.ioloop).start()
self._running = True
self.ioloop.start()
函數(shù)run()
函數(shù)run()是抓取程序fetcher中的一個(gè)大的循環(huán)程序。
函數(shù)run()中定義了另外一個(gè)函數(shù)queue_loop(),該函數(shù)接收輸入隊(duì)列中的所有任務(wù),并抓取它們。同時(shí)該函數(shù)也監(jiān)聽中斷信號(hào)。函數(shù)queue_loop()作為參數(shù)傳遞給tornado的類PeriodicCallback,如你所猜,PeriodicCallback會(huì)每隔一段具體的時(shí)間調(diào)用一次queue_loop()函數(shù)。函數(shù)queue_loop()也會(huì)調(diào)用另一個(gè)能使我們更接近于實(shí)際檢索Web資源操作的函數(shù):fetch()。
函數(shù)fetch(self, task, callback=None)
網(wǎng)絡(luò)上的資源必須使用函數(shù)phantomjs_fetch()或簡單的http_fetch()函數(shù)檢索,函數(shù)fetch()只決定檢索該資源的正確方法是什么。接下來我們看一下函數(shù)http_fetch()。
函數(shù)http_fetch(self, url, task, callback)
def http_fetch(self, url, task, callback):
'''HTTP fetcher'''
fetch = copy.deepcopy(self.default_options)
fetch['url'] = url
fetch['headers']['User-Agent'] = self.user_agent
def handle_response(response):
...
return task, result
try:
request = tornado.httpclient.HTTPRequest(header_callback=header_callback, **fetch)
if self.async:
self.http_client.fetch(request, handle_response)
else:
return handle_response(self.http_client.fetch(request))
終于,這里才是完成真正工作的地方。這個(gè)函數(shù)的代碼有點(diǎn)長,但有清晰的結(jié)構(gòu),容易閱讀。
在函數(shù)的開始部分,它設(shè)置了抓取請求的header,比如User-Agent、超時(shí)timeout等等。然后定義一個(gè)處理響應(yīng)response的函數(shù):handle_response(),后邊我們會(huì)分析這個(gè)函數(shù)。最后我們得到一個(gè)tornado的請求對象request,并發(fā)送這個(gè)請求對象。請注意在異步和非異步的情況下,是如何使用相同的函數(shù)來處理響應(yīng)response的。
讓我們往回看一下,分析一下函數(shù)handle_response()做了什么。
函數(shù)handle_response(response)
def handle_response(response):
result = {}
result['orig_url'] = url
result['content'] = response.body or ''
callback('http', task, result)
return task, result
這個(gè)函數(shù)以字典的形式保存一個(gè)response的所有相關(guān)信息,例如url,狀態(tài)碼和實(shí)際響應(yīng)等,然后調(diào)用回調(diào)函數(shù)。這里的回調(diào)函數(shù)是一個(gè)小方法:send_result()。
函數(shù)send_result(self, type, task, result)
def send_result(self, type, task, result):
if self.outqueue:
self.outqueue.put((task, result))
這個(gè)最后的函數(shù)將結(jié)果放入到輸出隊(duì)列中,等待內(nèi)容處理程序processor的讀取。
內(nèi)容處理程序processor
內(nèi)容處理程序的目的是分析已經(jīng)抓取回來的頁面。它的過程同樣也是一個(gè)大循環(huán),但輸出中有三個(gè)隊(duì)列(status_queue, newtask_queue 以及result_queue)而輸入中只有一個(gè)隊(duì)列(inqueue)。
讓我們稍微深入地分析一下函數(shù)run()中的循環(huán)過程。
函數(shù)run(self)
def run(self):
try:
task, response = self.inqueue.get(timeout=1)
self.on_task(task, response)
self._exceptions = 0
except KeyboardInterrupt:
break
except Exception as e:
self._exceptions += 1
if self._exceptions > self.EXCEPTION_LIMIT:
break
continue
這個(gè)函數(shù)的代碼比較少,易于理解,它簡單地從隊(duì)列中得到需要被分析的下一個(gè)任務(wù),并利用on_task(task, response)函數(shù)對其進(jìn)行分析。這個(gè)循環(huán)監(jiān)聽中斷信號(hào),只要我們給Python發(fā)送這樣的信號(hào),這個(gè)循環(huán)就會(huì)終止。最后這個(gè)循環(huán)統(tǒng)計(jì)它引發(fā)的異常的數(shù)量,異常數(shù)量過多會(huì)終止這個(gè)循環(huán)。
函數(shù)on_task(self, task, response)
def on_task(self, task, response):
response = rebuild_response(response)
project = task['project']
project_data = self.project_manager.get(project, updatetime)
ret = project_data['instance'].run(
status_pack = {
'taskid': task['taskid'],
'project': task['project'],
'url': task.get('url'),
...
}
self.status_queue.put(utils.unicode_obj(status_pack))
if ret.follows:
self.newtask_queue.put(
[utils.unicode_obj(newtask) for newtask in ret.follows])
for project, msg, url in ret.messages:
self.inqueue.put(({...},{...}))
return True
函數(shù)on_task()是真正干活的方法。
它嘗試?yán)幂斎氲娜蝿?wù)找到任務(wù)所屬的項(xiàng)目。然后它運(yùn)行項(xiàng)目中的定制腳本。最后它分析定制腳本返回的響應(yīng)response。如果一切順利,將會(huì)創(chuàng)建一個(gè)包含所有我們從網(wǎng)頁上得到的信息的字典。最后將字典放到隊(duì)列status_queue中,稍后它會(huì)被調(diào)度程序重新使用。
如果在分析的頁面中有一些新的鏈接需要處理,新鏈接會(huì)被放入到隊(duì)列newtask_queue中,并在稍后被調(diào)度程序使用。
現(xiàn)在,如果有需要的話,pyspider會(huì)將結(jié)果發(fā)送給其他項(xiàng)目。
最后如果發(fā)生了一些錯(cuò)誤,像頁面返回錯(cuò)誤,錯(cuò)誤信息會(huì)被添加到日志中。
數(shù)據(jù)分析咨詢請掃描二維碼
若不方便掃碼,搜微信號(hào):CDAshujufenxi
LSTM 模型輸入長度選擇技巧:提升序列建模效能的關(guān)鍵? 在循環(huán)神經(jīng)網(wǎng)絡(luò)(RNN)家族中,長短期記憶網(wǎng)絡(luò)(LSTM)憑借其解決長序列 ...
2025-07-11CDA 數(shù)據(jù)分析師報(bào)考條件詳解與準(zhǔn)備指南? ? 在數(shù)據(jù)驅(qū)動(dòng)決策的時(shí)代浪潮下,CDA 數(shù)據(jù)分析師認(rèn)證愈發(fā)受到矚目,成為眾多有志投身數(shù) ...
2025-07-11數(shù)據(jù)透視表中兩列相乘合計(jì)的實(shí)用指南? 在數(shù)據(jù)分析的日常工作中,數(shù)據(jù)透視表憑借其強(qiáng)大的數(shù)據(jù)匯總和分析功能,成為了 Excel 用戶 ...
2025-07-11尊敬的考生: 您好! 我們誠摯通知您,CDA Level I和 Level II考試大綱將于 2025年7月25日 實(shí)施重大更新。 此次更新旨在確保認(rèn) ...
2025-07-10BI 大數(shù)據(jù)分析師:連接數(shù)據(jù)與業(yè)務(wù)的價(jià)值轉(zhuǎn)化者? ? 在大數(shù)據(jù)與商業(yè)智能(Business Intelligence,簡稱 BI)深度融合的時(shí)代,BI ...
2025-07-10SQL 在預(yù)測分析中的應(yīng)用:從數(shù)據(jù)查詢到趨勢預(yù)判? ? 在數(shù)據(jù)驅(qū)動(dòng)決策的時(shí)代,預(yù)測分析作為挖掘數(shù)據(jù)潛在價(jià)值的核心手段,正被廣泛 ...
2025-07-10數(shù)據(jù)查詢結(jié)束后:分析師的收尾工作與價(jià)值深化? ? 在數(shù)據(jù)分析的全流程中,“query end”(查詢結(jié)束)并非工作的終點(diǎn),而是將數(shù) ...
2025-07-10CDA 數(shù)據(jù)分析師考試:從報(bào)考到取證的全攻略? 在數(shù)字經(jīng)濟(jì)蓬勃發(fā)展的今天,數(shù)據(jù)分析師已成為各行業(yè)爭搶的核心人才,而 CDA(Certi ...
2025-07-09【CDA干貨】單樣本趨勢性檢驗(yàn):捕捉數(shù)據(jù)背后的時(shí)間軌跡? 在數(shù)據(jù)分析的版圖中,單樣本趨勢性檢驗(yàn)如同一位耐心的偵探,專注于從單 ...
2025-07-09year_month數(shù)據(jù)類型:時(shí)間維度的精準(zhǔn)切片? ? 在數(shù)據(jù)的世界里,時(shí)間是最不可或缺的維度之一,而year_month數(shù)據(jù)類型就像一把精準(zhǔn) ...
2025-07-09CDA 備考干貨:Python 在數(shù)據(jù)分析中的核心應(yīng)用與實(shí)戰(zhàn)技巧? ? 在 CDA 數(shù)據(jù)分析師認(rèn)證考試中,Python 作為數(shù)據(jù)處理與分析的核心 ...
2025-07-08SPSS 中的 Mann-Kendall 檢驗(yàn):數(shù)據(jù)趨勢與突變分析的有力工具? ? ? 在數(shù)據(jù)分析的廣袤領(lǐng)域中,準(zhǔn)確捕捉數(shù)據(jù)的趨勢變化以及識(shí)別 ...
2025-07-08備戰(zhàn) CDA 數(shù)據(jù)分析師考試:需要多久?如何規(guī)劃? CDA(Certified Data Analyst)數(shù)據(jù)分析師認(rèn)證作為國內(nèi)權(quán)威的數(shù)據(jù)分析能力認(rèn)證 ...
2025-07-08LSTM 輸出不確定的成因、影響與應(yīng)對策略? 長短期記憶網(wǎng)絡(luò)(LSTM)作為循環(huán)神經(jīng)網(wǎng)絡(luò)(RNN)的一種變體,憑借獨(dú)特的門控機(jī)制,在 ...
2025-07-07統(tǒng)計(jì)學(xué)方法在市場調(diào)研數(shù)據(jù)中的深度應(yīng)用? 市場調(diào)研是企業(yè)洞察市場動(dòng)態(tài)、了解消費(fèi)者需求的重要途徑,而統(tǒng)計(jì)學(xué)方法則是市場調(diào)研數(shù) ...
2025-07-07CDA數(shù)據(jù)分析師證書考試全攻略? 在數(shù)字化浪潮席卷全球的當(dāng)下,數(shù)據(jù)已成為企業(yè)決策、行業(yè)發(fā)展的核心驅(qū)動(dòng)力,數(shù)據(jù)分析師也因此成為 ...
2025-07-07剖析 CDA 數(shù)據(jù)分析師考試題型:解鎖高效備考與答題策略? CDA(Certified Data Analyst)數(shù)據(jù)分析師考試作為衡量數(shù)據(jù)專業(yè)能力的 ...
2025-07-04SQL Server 字符串截取轉(zhuǎn)日期:解鎖數(shù)據(jù)處理的關(guān)鍵技能? 在數(shù)據(jù)處理與分析工作中,數(shù)據(jù)格式的規(guī)范性是保證后續(xù)分析準(zhǔn)確性的基礎(chǔ) ...
2025-07-04CDA 數(shù)據(jù)分析師視角:從數(shù)據(jù)迷霧中探尋商業(yè)真相? 在數(shù)字化浪潮席卷全球的今天,數(shù)據(jù)已成為企業(yè)決策的核心驅(qū)動(dòng)力,CDA(Certifie ...
2025-07-04CDA 數(shù)據(jù)分析師:開啟數(shù)據(jù)職業(yè)發(fā)展新征程? ? 在數(shù)據(jù)成為核心生產(chǎn)要素的今天,數(shù)據(jù)分析師的職業(yè)價(jià)值愈發(fā)凸顯。CDA(Certified D ...
2025-07-03