上期我們理性的分析了為什麼要學習Scrapy,理由只有一個,那就是免費,一分錢都不用花!
咦?怎麼有人扔西紅柿?好吧,我承認電視看多了。不過今天是沒得看了,為了趕稿,又是一個不眠夜。。。言歸正傳,我們將在這一期介紹完Scrapy的基礎知識, 如果想深入研究,大家可以參考官方文件,那可是出了名的全面,我就不佔用公眾號的篇幅了。
架構簡介
下面是Scrapy的架構,包括元件以及在系統中發生的資料流的概覽(紅色箭頭所示)。 之後會對每個元件做簡單介紹,資料流也會做一個簡要描述。
元件
Engine: 引擎負責控制資料流在系統中所有元件中流動,並在相應動作發生時觸發事件。
Scheduler: 排程器從引擎接受Request並將他們入隊,以便之後引擎請求他們時提供給引擎。
Downloader: 下載器負責獲取頁面資料並提供給引擎,而後提供給Spider。
Spiders: Spider是Scrapy使用者編寫的用於分析Response並提取Item或提取更多需要下載的URL的類。 每個Spider負責處理特定網站。
Item Pipeline: 負責處理被Spider提取出來的Item。典型的功能有清洗、 驗證及持久化操作。
Downloader middlewares: 下載器中介軟體是在Engine及Downloader之間的特定鉤子(specific hooks),處理Downloader傳遞給Engine的Response。 其提供了一個簡便的機制,通過插入自定義程式碼來擴充套件Scrapy功能。
Spider middlewares: 是在Engine及Spider之間的特定鉤子(specific hook),處理Spider的輸入(Response)和輸出(Items及Requests)。 其提供了一個簡便的機制,通過插入自定義程式碼來擴充套件Scrapy功能。
資料流
Scrapy中的資料流由執行引擎控制,其過程如下:
- Engine從Spider獲取第一個需要爬取URL(s)。
- Engine用Scheduler排程Requests,並向Scheduler請求下一個要爬取的URL。
- Scheduler返回下一個要爬取的URL給Engine。
- Engine將URL通過Downloader middlewares轉發給Downloader。
- 一旦頁面下載完畢,下載器生成一個該頁面的Response,並將其通過Downloader middlewares傳送給Engine。
- 引擎從Downloader中接收到Response並通過Spider middlewares傳送給Spider處理。
- Spider處理Response並返回爬取到的Item及新的Request給Engine。
- Engine將爬取到的Item給Item Pipeline,然後將Request給Scheduler。
- 從第一步開始重複這個流程,直到Scheduler中沒有更多的URLs。
架構就是這樣,流程和我第二篇裡介紹的迷你架構差不多,但擴充套件性非常強大。
One more thing
Scrapy基於事件驅動網路框架 Twisted 編寫,Twisted是一個非同步非阻塞框架。一說到網路通訊框架就會提什麼同步、非同步、阻塞和非阻塞,到底是些啥玩意啊?為啥老是有人暗示或者明示非同步=非阻塞?比如Scrapy文件裡:Scrapy is written with Twisted, a popular event-driven networking framework for Python. Thus, it’s implemented using a non-blocking (aka asynchronous) code for concurrency. 這種說法對嗎?舉個栗子:
出場人物:老張,水壺兩把(普通水壺,簡稱水壺;會響的水壺,簡稱響水壺)
1. 老張把水壺放到火上,立等水開。(同步阻塞)
老張覺得自己有點傻。
2. 老張把水壺放到火上,去客廳看電視,時不時去廚房看看水開沒有。(同步非阻塞)
老張還是覺得自己有點傻,於是變高階了,買了把會響笛的那種水壺。水開之後,能大聲發出嘀~~~~的噪音。
3. 老張把響水壺放到火上,立等水開。(非同步阻塞)
老張覺得這樣傻等意義不大。
4. 老張把響水壺放到火上,去客廳看電視,水壺響之前不再去看它了,響了再去拿壺。(非同步非阻塞)
老張覺得自己聰明瞭。
所謂同步非同步,只是對於水壺而言。普通水壺,同步;響水壺,非同步。雖然都能幹活,但響水壺可以在自己完工之後,提示老張水開了。這是普通水壺所不能及的。同步只能讓呼叫者去輪詢自己(情況2中),造成老張效率的低下。
所謂阻塞非阻塞,僅僅對於老張而言。立等的老張,阻塞;看電視的老張,非阻塞。情況1和情況3中老張就是阻塞的,媳婦喊他都不知道。雖然3中響水壺是非同步的,可對於立等的老張沒有太大的意義。所以一般非同步是配合非阻塞使用的,這樣才能發揮非同步的效用。
入門教程
建立專案
在開始爬取之前,您必須建立一個新的Scrapy專案。 進入您打算儲存程式碼的目錄中,執行下列命令:
1 |
scrapy startproject tutorial |
該命令將會建立包含下列內容的 tutorial 目錄:
1 2 3 4 5 6 7 8 9 |
tutorial/ scrapy.cfg # 專案的配置檔案 tutorial/ # 該專案的python模組。之後您將在此加入程式碼 __init__.py items.py # 專案中的item檔案 pipelines.py # 專案中的pipelines檔案 settings.py # 專案的設定檔案 spiders/ # 放置spider程式碼的目錄 __init__.py |
編寫第一個爬蟲
Spider是使用者編寫用於從單個網站(或者一些網站)爬取資料的類。其包含了一個用於下載的初始URL,以及如何跟進網頁中的連結以及如何分析頁面中的內容的方法。
以下為我們的第一個Spider程式碼,儲存在 tutorial/spiders 目錄下的 quotes_spider.py檔案中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import scrapy class QuotesSpider(scrapy.Spider): name = "quotes" def start_requests(self): urls = [ 'http://quotes.toscrape.com/page/1/', 'http://quotes.toscrape.com/page/2/', ] for url in urls: yield scrapy.Request(url=url, callback=self.parse) def parse(self, response): page = response.url.split("/")[-2] filename = 'quotes-%s.html' % page with open(filename, 'wb') as f: f.write(response.body) self.log('Saved file %s' % filename) |
為了建立一個Spider,你必須繼承 scrapy.Spider 類, 且定義以下三個屬性:
- name: 用於區別Spider。 該名字必須是唯一的,您不可以為不同的Spider設定相同的名字。
- start_urls: 包含了Spider在啟動時進行爬取的url列表。 因此,第一個被獲取到的頁面將是其中之一。 後續的URL則從初始的URL獲取到的資料中提取。
- parse() 是spider的一個方法。 被呼叫時,每個初始URL完成下載後生成的Response 物件將會作為唯一的引數傳遞給該函式。 該方法負責解析返回的資料(response data),提取資料以及生成需要進一步處理的URL的 Request 物件。
執行我們的爬蟲
進入專案的根目錄,執行下列命令啟動spider:
1 |
scrapy crawl quotes |
這個命令啟動用於爬取 quotes.toscrape.com 的spider,你將得到類似的輸出:
1 2 3 4 5 6 7 8 9 |
2017-05-10 20:36:17 [scrapy.core.engine] INFO: Spider opened 2017-05-10 20:36:17 [scrapy.extensions.logstats] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min) 2017-05-10 20:36:17 [scrapy.extensions.telnet] DEBUG: Telnet console listening on 127.0.0.1:6023 2017-05-10 20:36:17 [scrapy.core.engine] DEBUG: Crawled (404) <GET http://quotes.toscrape.com/robots.txt> (referer: None) 2017-05-10 20:36:17 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://quotes.toscrape.com/page/1/> (referer: None) 2017-05-10 20:36:17 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://quotes.toscrape.com/page/2/> (referer: None) 2017-05-10 20:36:17 [quotes] DEBUG: Saved file quotes-1.html 2017-05-10 20:36:17 [quotes] DEBUG: Saved file quotes-2.html 2017-05-10 20:36:17 [scrapy.core.engine] INFO: Closing spider (finished) |
提取資料
我們之前只是儲存了HTML頁面,並沒有提取資料。現在升級一下程式碼,把提取功能加進去。至於如何使用瀏覽器的開發者模式分析網頁,之前已經介紹過了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import scrapy class QuotesSpider(scrapy.Spider): name = "quotes" start_urls = [ 'http://quotes.toscrape.com/page/1/', 'http://quotes.toscrape.com/page/2/', ] def parse(self, response): for quote in response.css('div.quote'): yield { 'text': quote.css('span.text::text').extract_first(), 'author': quote.css('small.author::text').extract_first(), 'tags': quote.css('div.tags a.tag::text').extract(), } |
再次執行這個爬蟲,你將在日誌裡看到被提取出的資料:
1 2 3 4 |
2017-05-10 20:38:33 [scrapy.core.scraper] DEBUG: Scraped from <200 http://quotes.toscrape.com/page/1/> {'tags': ['life', 'love'], 'author': 'André Gide', 'text': '“It is better to be hated for what you are than to be loved for what you are not.”'} 2017-05-10 20:38:33 [scrapy.core.scraper] DEBUG: Scraped from <200 http://quotes.toscrape.com/page/1/> {'tags': ['edison', 'failure', 'inspirational', 'paraphrased'], 'author': 'Thomas A. Edison', 'text': "“I have not failed. I've just found 10,000 ways that won't work.”"} |
儲存爬取的資料
最簡單儲存爬取的資料的方式是使用 Feed exports:
1 |
scrapy crawl quotes -o quotes.json |
該命令將採用 JSON 格式對爬取的資料進行序列化,生成quotes.json檔案。
在類似本篇教程裡這樣小規模的專案中,這種儲存方式已經足夠。如果需要對爬取到的item做更多更為複雜的操作,你可以編寫 Item Pipeline,tutorial/pipelines.py在最開始的時候已經自動建立了。
下一步
系列寫到這裡,組裡對下一步的計劃產生了分歧,本人的意思是系列已經接近尾聲了,可領導的意思是,連載可以正式開始了! What? 這不能忍啊!所以我立即做了一個艱難的決定,連載正式開始!詳情下回分解,再見!