對於電商類型和內容服務類型的網站,常常會出現由於配置錯誤形成頁面連接沒法訪問的狀況(404)。html
顯然,要確保網站中的全部連接都具備可訪問性,經過人工進行檢測確定是不現實的,經常使用的作法是使用爬蟲技術按期對網站進行資源爬取,及時發現訪問異常的連接。前端
對於網絡爬蟲,當前市面上已經存在大量的開源項目和技術討論的文章。不過,感受你們廣泛都將焦點集中在爬取效率方面,例如當前就存在大量討論不一樣併發機制哪一個效率更高的文章,而在爬蟲的其它特性方面探討的很少。python
我的認爲,爬蟲的核心特性除了快
,還應該包括全
和穩
,而且從重要性的排序來看,全
、穩
、快
應該是從高到低的。git
全
排在第一位,是由於這是爬蟲的基本功能,若爬取的頁面不全,就會出現信息遺漏的狀況,這種狀況確定是不容許的;而穩
排在第二位,是由於爬蟲一般都是須要長期穩定運行的,若由於策略處理不當形成爬蟲運行過程當中偶爾沒法正常訪問頁面,確定也是沒法接受的;最後纔是快
,咱們一般須要爬取的頁面連接會很是多,所以效率就很關鍵,但這也必須創建在全
和穩
的基礎上。github
固然,爬蟲自己是一個很深的技術領域,我接觸的也只是皮毛。本文只針對使用爬蟲技術實現 Web 頁面資源可用性檢測的實際場景,詳細剖析下其中涉及到的幾個技術點,重點解決以下幾個問題:編程
在早些年,基本上絕大多數網站都是經過後端渲染的,即在服務器端組裝造成完整的 HTML 頁面,而後再將完整頁面返回給前端進行展示。而近年來,隨着 AJAX 技術的不斷普及,以及 AngularJS 這類 SPA 框架的普遍應用,前端渲染的頁面愈來愈多。後端
不知你們有沒有據說過,前端渲染相比於後端渲染,是不利於進行 SEO 的,由於對爬蟲不友好。究其緣由,就是由於前端渲染的頁面是須要在瀏覽器端執行 JavaScript 代碼(即 AJAX 請求)才能獲取後端數據,而後才能拼裝成完整的 HTML 頁面。瀏覽器
針對這類狀況,當前也已經有不少解決方案,最經常使用的就是藉助 PhantomJS、puppeteer 這類 Headless 瀏覽器工具,至關於在爬蟲中內置一個瀏覽器內核,對抓取的頁面先渲染(執行 Javascript 腳本),而後再對頁面內容進行抓取。bash
不過,要使用這類技術,一般都是須要使用 Javascript 來開發爬蟲工具,對於我這種寫慣了 Python 的人來講的確有些痛苦。服務器
直到某一天,kennethreitz 大神發佈了開源項目 requests-html,看到項目介紹中的那句 Full JavaScript support!
時不由熱淚盈眶,就是它了!該項目在 GitHub 上發佈後不到三天,star 數就達到 5000 以上,足見其影響力。
requests-html 爲啥會這麼火?
寫過 Python 的人,基本上都會使用 requests 這麼一個 HTTP 庫,說它是最好的 HTTP 庫一點也不誇張(不限編程語言),對於其介紹語 HTTP Requests for Humans
也當之無愧。也是由於這個緣由,Locust 和 HttpRunner 都是基於 requests 來進行開發的。
而 requests-html,則是 kennethreitz 在 requests 的基礎上開發的另外一個開源項目,除了能夠複用 requests 的所有功能外,還實現了對 HTML 頁面的解析,即支持對 Javascript 的執行,以及經過 CSS 和 XPath 對 HTML 頁面元素進行提取的功能,這些都是編寫爬蟲工具很是須要的功能。
在實現 Javascript 執行方面,requests-html 也並無本身造輪子,而是藉助了 pyppeteer 這個開源項目。還記得前面提到的 puppeteer 項目麼,這是 GoogleChrome 官方實現的 Node API
;而 pyppeteer 這個項目,則至關因而使用 Python 語言對 puppeteer 的非官方實現,基本具備 puppeteer 的全部功能。
理清了以上關係後,相信你們對 requests-html 也就有了更好的理解。
在使用方面,requests-html 也十分簡單,用法與 requests 基本相同,只是多了 render
功能。
from requests_html import HTMLSession session = HTMLSession() r = session.get('http://python-requests.org') r.html.render()
爬蟲實現訪問頻率控制
在執行 render()
以後,返回的就是通過渲染後的頁面內容。
爲了防止流量攻擊,不少網站都有訪問頻率限制,即限制單個 IP 在必定時間段內的訪問次數。若超過這個設定的限制,服務器端就會拒絕訪問請求,即響應狀態碼爲 403(Forbidden)。
這用來應對外部的流量攻擊或者爬蟲是能夠的,但在這個限定策略下,公司內部的爬蟲測試工具一樣也沒法正常使用了。針對這個問題,經常使用的作法就是在應用系統中開設白名單,將公司內部的爬蟲測試服務器 IP 加到白名單中,而後針對白名單中的 IP 不作限制,或者提高限額。但這一樣可能會出現問題。由於應用服務器的性能不是無限的,假如爬蟲的訪問頻率超過了應用服務器的處理極限,那麼就會形成應用服務器不可用的狀況,即響應狀態碼爲 503(Service Unavailable Error)。
基於以上緣由,爬蟲的訪問頻率應該是要與項目組的開發和運維進行統一評估後肯定的;而對於爬蟲工具而言,實現對訪問頻率的控制也就頗有必要了。
那要怎樣實現訪問頻率的控制呢?
咱們能夠先回到爬蟲自己的實現機制。對於爬蟲來講,無論採用什麼實現形式,應該均可以歸納爲生產者和消費者模型,即:
對於這種模型,最簡單的作法是使用一個 FIFO 的隊列,用於存儲未爬取的連接隊列(unvisited_urls_queue)。無論是採用何種併發機制,這個隊列均可以在各個 worker 中共享。對於每個 worker 來講,均可以按照以下作法:
而後回到咱們的問題,要限制訪問頻率,即單位時間內請求的連接數目。顯然,worker 之間相互獨立,要在執行端層面協同實現總體的頻率控制並不容易。但從上面的步驟中能夠看出,unvisited_urls_queue 被全部 worker 共享,而且做爲源頭供給的角色。那麼只要咱們能夠實現對 unvisited_urls_queue 補充的數量控制,就實現了爬蟲總體的訪問頻率控制。
以上思路是正確的,但在具體實現的時候會存在幾個問題:
而且在當前的實際場景中,最佳的併發機制是選擇多進程(下文會詳細說明緣由),每一個 worker 在不一樣的進程中,那要實現對集合的共享就不大容易了。同時,若是每一個 worker 都要負責對總請求數進行判斷,即將訪問頻率的控制邏輯放到 worker 中實現,那對於 worker 來講會是一個負擔,邏輯上也會比較複雜。
所以比較好的方式是,除了未訪問連接隊列(unvisited_urls_queue),另外再新增一個爬取結果的存儲隊列(fetched_urls_queue),這兩個隊列都在各個 worker 中共享。那麼,接下來邏輯就變得簡單了:
具體的控制方法也很簡單,假設咱們是要實現 RPS 的控制,那麼就可使用以下方式(只截取關鍵片斷):
start_timer = time.time() requests_queued = 0 while True: try: url = self.fetched_urls_queue.get(timeout=5) except queue.Empty: break # visited url will not be crawled twice if url in self.visited_urls_set: continue # limit rps or rpm if requests_queued >= self.requests_limit: runtime_secs = time.time() - start_timer if runtime_secs < self.interval_limit: sleep_secs = self.interval_limit - runtime_secs # exceed rps limit, sleep time.sleep(sleep_secs) start_timer = time.time() requests_queued = 0 self.unvisited_urls_queue.put(url) self.visited_urls_set.add(url) requests_queued += 1
不過,他們的併發測試結果對於本文中討論的爬蟲場景並不適用。由於在本文的爬蟲場景中,實現前端頁面渲染是最核心的一項功能特性,而要實現前端頁面渲染,底層都是須要使用瀏覽器內核的,至關於每一個 worker 在運行時都會跑一個 Chromium 實例。
衆所周知,Chromium 對於 CPU 和內存的開銷都是比較大的,所以爲了不機器資源出現瓶頸,使用多進程機制(multiprocessing)充分調用多處理器的硬件資源無疑是最佳的選擇。
另外一個須要注意也是比較被你們忽略的點,就是在頁面連接的請求方法上。
請求頁面連接,不都是使用 GET 方法麼?
的確,使用 GET 請求確定是可行的,但問題在於,GET 請求時會加載頁面中的全部資源信息,這自己會是比較耗時的,特別是遇到連接爲比較大的圖片或者附件的時候。這無疑會耗費不少無謂的時間,畢竟咱們的目的只是爲了檢測連接資源是否可訪問而已。
比較好的的作法是對網站的連接進行分類:
在如上分類中,除了第三類是必需要使用 GET 方法獲取頁面並加載完整內容(render),前兩類徹底可使用 HEAD 方法進行代替。一方面,HEAD 方法只會獲取狀態碼和 headers 而不獲取 body,比 GET 方法高效不少;另外一方面,前兩類連接也無需進行頁面渲染,省去了調用 Chromium 進行解析的步驟,執行效率的提升也會很是明顯。
本文針對如何使用爬蟲技術實現 Web 頁面資源可用性檢測進行了講解,重點圍繞爬蟲如何實現 全
、穩
、快
三個核心特性進行了展開。對於爬蟲技術的更多內容,後續有機會咱們再進一步進行探討。