以Python的Pyspider為例剖析搜尋引擎的網路爬蟲實現方法

來源:互聯網
上載者:User
在這篇文章中,我們將分析一個網路爬蟲。

網路爬蟲是一個掃描網路內容並記錄其有用資訊的工具。它能開啟一大堆網頁,分析每個頁面的內容以便尋找所有感興趣的資料,並將這些資料存放區在一個資料庫中,然後對其他網頁進行同樣的操作。

如果爬蟲正在分析的網頁中有一些連結,那麼爬蟲將會根據這些連結分析更多的頁面。

搜尋引擎就是基於這樣的原理實現的。

這篇文章中,我特別選了一個穩定的、”年輕”的開源項目pyspider,它是由 binux 編碼實現的。

註:據認為pyspider持續監控網路,它假定網頁在一段時間後會發生變化,因此一段時間後它將會重新訪問相同的網頁。

概述

爬蟲pyspider主要由四個組件組成。包括發送器(scheduler),抓取程式(fetcher),內容處理常式(processor)以及一個監控組件。

發送器接受任務並決定該做什麼。這裡有幾種可能性,它可以丟棄一個任務(可能這個特定的網頁剛剛被抓取過了),或者給任務分配不同的優先順序。

當各個任務的優先順序確定之後,它們被傳入抓取程式。它重新抓取網頁。這個過程很複雜,但邏輯上比較簡單。

當網路上的資源被抓取下來,內容處理常式就負責抽取有用的資訊。它運行一個使用者編寫的Python指令碼,這個指令碼並不像沙箱一樣被隔離。它的職責還包括捕獲異常或日誌,並適當地管理它們。

最後,爬蟲pyspider中有一個監控組件。

爬蟲pyspider提供一個異常強大的網頁介面(web ui),它允許你編輯和調試你的指令碼,管理整個抓取過程,監控進行中的任務,並最終輸出結果。

項目和任務

在pyspider中,我們有項目和任務的概念。

一個任務指的是一個需要從網站檢索並進行分析的單獨頁面。

一個項目指的是一個更大的實體,它包括爬蟲涉及到的所有頁面,分析網頁所需要的python指令碼,以及用於儲存資料的資料庫等等。

在pyspider中我們可以同時運行多重專案。

代碼結構分析

根目錄

在根目錄中可以找到的檔案夾有:

  • data,空檔案夾,它是存放由爬蟲所產生的資料的地方。
  • docs,包含該項目文檔,裡邊有一些markdown代碼。
  • pyspider,包含項目實際的代碼。
  • test,包含相當多的測試代碼。
  • 這裡我將重點介紹一些重要的檔案:
  • .travis.yml,一個很棒的、連續性測試的整合。你如何確定你的項目確實有效?畢竟僅在你自己的帶有固定版本的庫的機器上進行測試是不夠的。
  • Dockerfile,同樣很棒的工具!如果我想在我的機器上嘗試一個項目,我只需要運行Docker,我不需要手動安裝任何東西,這是一個使開發人員參與到你的項目中的很好的方式。
  • LICENSE,對於任何開源項目都是必需的,(如果你自己有開源項目的話)不要忘記自己項目中的該檔案。
  • requirements.txt,在Python世界中,該檔案用於指明為了運行該軟體,需要在你的系統中安裝什麼Python包,在任何的Python項目中該檔案都是必須的。
  • run.py,該軟體的主進入點。
  • setup.py,該檔案是一個Python指令碼,用於在你的系統中安裝pyspider項目。

已經分析完項目的根目錄了,僅根目錄就能說明該項目是以一種非常專業的方式進行開發的。如果你正在開發任何的開來源程式,希望你能達到這樣的水準。

檔案夾pyspider

讓我們更深入一點兒,一起來分析實際的代碼。

在這個檔案夾中還能找到其他的檔案夾,整個軟體背後的邏輯已經被分割,以便更容易的進行管理和擴充。

這些檔案夾是:database、fetcher、libs、processor、result、scheduler、webui。

在這個檔案夾中我們也能找到整個項目的主進入點,run.py。

檔案run.py

這個檔案首先完成所有必需的雜事,以保證爬蟲成功地運行。最終它產生所有必需的計算單元。向下滾動我們可以看到整個項目的進入點,cli()。

函數cli()

這個函數好像很複雜,但與我相隨,你會發現它並沒有你想象中複雜。函數cli()的主要目的是建立資料庫和訊息系統的所有串連。它主要解析命令列參數,並利用所有我們需要的東西建立一個大字典。最後,我們通過調用函數all()開始真正的工作。

函數all()

一個網路爬蟲會進行大量的IO操作,因此一個好的想法是產生不同的線程或子進程來管理所有的這些工作。通過這種方式,你可以在等待網路擷取你當前html頁面的同時,提取前一個頁面的有用資訊。

函數all()決定是否運行子進程或者線程,然後調用不同的線程或子進程裡的所有的必要函數。這時pyspider將產生包括webui在內的,爬蟲的所有邏輯模組所需要的,足夠數量的線程。當我們完成項目並關閉webui時,我們將乾淨漂亮地關閉每一個進程。

現在我們的爬蟲就開始運行了,讓我們進行更深入一點兒的探索。

發送器

發送器從兩個不同的隊列中擷取任務(newtask_queue和status_queue),並把任務加入到另外一個隊列(out_queue),這個隊列稍後會被抓取程式讀取。

發送器做的第一件事情是從資料庫中載入所需要完成的所有的任務。之後,它開始一個無限迴圈。在這個迴圈中會調用幾個方法:

1._update_projects():嘗試更新的各種設定,例如,我們想在爬蟲工作的時候調整爬取速度。

2._check_task_done():分析已完成的任務並將其儲存到資料庫,它從status_queue中擷取任務。

3._check_request():如果內容處理常式要求分析更多的頁面,把這些頁面放在隊列newtask_queue中,該函數會從該隊列中獲得新的任務。

4._check_select():把新的網頁加入到抓取程式的隊列中。

5._check_delete():刪除已被使用者標記的任務和項目。

6._try_dump_cnt():記錄一個檔案中已完成任務的數量。對於防止程式異常所導致的資料丟失,這是有必要的。

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

迴圈也會檢查運行過程中的異常,或者我們是否要求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()

抓取程式

抓取程式的目的是檢索網路資源。

pyspider能夠處理普通HTML文本頁面和基於AJAX的頁面。只有抓取程式能意識到這種差異,瞭解這一點非常重要。我們將僅專註於普通的html文本抓取,然而大部分的想法可以很容易地移植到Ajax抓取器。

這裡的想法在某種形式上類似於發送器,我們有分別用於輸入和輸出的兩個隊列,以及一個大的迴圈。對於輸入隊列中的所有元素,抓取程式產生一個請求,並將結果放入輸出隊列中。

它聽起來簡單但有一個大問題。網路通常是極其緩慢的,如果因為等待一個網頁而阻止了所有的計算,那麼整個過程將會啟動並執行極其緩慢。解決方案非常的簡單,即不要在等待網路的時候阻塞所有的計算。這個想法即在網路上發送大量訊息,並且相當一部分訊息是同時發送的,然後非同步等待響應的返回。一旦我們收回一個響應,我們將會調用另外的回呼函數,回呼函數將會以最適合的方式管理這樣的響應。

爬蟲pyspider中的所有的複雜的非同步調度都是由另一個優秀的開源項目

http://www.tornadoweb.org/en/stable/

完成。

現在我們的腦海裡已經有了極好的想法了,讓我們更深入地探索這是如何?的。

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()

函數run()

函數run()是抓取程式fetcher中的一個大的迴圈程式。

函數run()中定義了另外一個函數queue_loop(),該函數接收輸入隊列中的所有任務,並抓取它們。同時該函數也監聽中斷訊號。函數queue_loop()作為參數傳遞給tornado的類PeriodicCallback,如你所猜,PeriodicCallback會每隔一段具體的時間調用一次queue_loop()函數。函數queue_loop()也會調用另一個能使我們更接近於實際檢索Web資源操作的函數:fetch()。

函數fetch(self, task, callback=None)

網路上的資源必須使用函數phantomjs_fetch()或簡單的http_fetch()函數檢索,函數fetch()只決定檢索該資源的正確方法是什麼。接下來我們看一下函數http_fetch()。

函數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))

終於,這裡才是完成真正工作的地方。這個函數的代碼有點長,但有清晰的結構,容易閱讀。

在函數的開始部分,它設定了抓取請求的header,比如User-Agent、逾時timeout等等。然後定義一個處理響應response的函數:handle_response(),後邊我們會分析這個函數。最後我們得到一個tornado的請求對象request,並發送這個請求對象。請注意在非同步和非非同步情況下,是如何使用相同的函數來處理響應response的。

讓我們往回看一下,分析一下函數handle_response()做了什麼。

函數handle_response(response)

def handle_response(response): result = {} result['orig_url'] = url result['content'] = response.body or '' callback('http', task, result) return task, result

這個函數以字典的形式儲存一個response的所有相關資訊,例如url,狀態代碼和實際響應等,然後調用回呼函數。這裡的回呼函數是一個小方法:send_result()。

函數send_result(self, type, task, result)

def send_result(self, type, task, result): if self.outqueue:  self.outqueue.put((task, result))

這個最後的函數將結果放入到輸出隊列中,等待內容處理常式processor的讀取。

內容處理常式processor

內容處理常式的目的是分析已經抓取回來的頁面。它的過程同樣也是一個大迴圈,但輸出中有三個隊列(status_queue, newtask_queue 以及result_queue)而輸入中只有一個隊列(inqueue)。

讓我們稍微深入地分析一下函數run()中的迴圈過程。

函數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

這個函數的代碼比較少,易於理解,它簡單地從隊列中得到需要被分析的下一個任務,並利用on_task(task, response)函數對其進行分析。這個迴圈監聽中斷訊號,只要我們給Python發送這樣的訊號,這個迴圈就會終止。最後這個迴圈統計它引發的異常的數量,異常數量過多會終止這個迴圈。

函數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

函數on_task()是真正幹活的方法。

它嘗試利用輸入的任務找到任務所屬的項目。然後它運行項目中的定製指令碼。最後它分析定製指令碼返回的響應response。如果一切順利,將會建立一個包含所有我們從網頁上得到的資訊的字典。最後將字典放到隊列status_queue中,稍後它會被發送器重新使用。

如果在分析的頁面中有一些新的連結需要處理,新連結會被放入到隊列newtask_queue中,並在稍後被發送器使用。

現在,如果有需要的話,pyspider會將結果發送給其他項目。

最後如果發生了一些錯誤,像頁面返回錯誤,錯誤資訊會被添加到日誌中。

結束!

  • 聯繫我們

    該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

    如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

    A Free Trial That Lets You Build Big!

    Start building with 50+ products and up to 12 months usage for Elastic Compute Service

    • Sales Support

      1 on 1 presale consultation

    • After-Sales Support

      24/7 Technical Support 6 Free Tickets per Quarter Faster Response

    • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.