實現一個Spider子類的過程很像是完成一系列填空題,Scrapy框架提出以下問題讓用戶在Spider子類中作答:
● 爬蟲從哪個或哪些頁面開始爬取?
● 對於一個已下載的頁面,提取其中的哪些數據?
● 爬取完當前頁面後,接下來爬取哪個或哪些頁面?上面問題的答案包含了一個爬蟲最重要的邏輯,回答了這些問題,
一個爬蟲也就開發出來了。
下面給出一個簡易爬蟲的實例給讀者找找感覺
# -*- coding: utf-8 -*-
import scrapy
class BooksSpider(scrapy.Spider):
# 每一個爬蟲的唯一標識
name = "books"
# 定義爬蟲爬取的起始點,起始點可以是多個,我們這裏是一個
start_urls = ['http://books.toscrape.com/']
def parse(self, response):
# 提取數據
# 每一本書的信息是在<article class="product_pod">中,我們使用
# css()方法找到所有這樣的article 元素,並依次迭代
for book in response.css('article.product_pod'):
# 書名信息在article > h3 > a 元素的title屬性裏
# 例如: <a title="A Light in the Attic">A Light in the ...</a>
name = book.xpath('./h3/a/@title').extract_first()
# 書價信息在 <p class="price_color">的TEXT中。
# 例如: <p class="price_color">£51.77</p>
price = book.css('p.price_color::text').extract_first()
yield {
'name': name,
'price': price,
}
# 提取鏈接
# 下一頁的url 在ul.pager > li.next > a 裏面
# 例如: <li class="next"><a href="catalogue/page-2.html">next</a></li>
next_url = response.css('ul.pager li.next a::attr(href)').extract_first()
if next_url:
# 如果找到下一頁的url,得到絕對路徑,構造新的Request 對象
next_url = response.urljoin(next_url)
yield scrapy.Request(next_url, callback=self.parse)
實現一個Spider只需要完成下面4個步驟:
步驟 01 繼承scrapy.Spider。
步驟 02 爲Spider取名。
步驟 03 設定起始爬取點。
步驟 04 實現頁面解析函數。
繼承scrapy.Spider
Scrapy框架提供了一個Spider基類,我們編寫的Spider需要繼承
它:
import scrapy
class BooksSpider(scrapy.Spider):
...
這個Spider基類實現了以下內容:
● 供Scrapy引擎調用的接口,例如用來創建Spider實例的類方法from_crawler。
● 供用戶使用的實用工具函數,例如可以調用log方法將調試信息輸出到日誌。
● 供用戶訪問的屬性,例如可以通過settings屬性訪問配置文件中的配置。
實際上,在初學Scrapy時,不必關心Spider基類的這些細節,未來有需求時再去查閱文檔即可。
爲Spider命名
在一個Scrapy項目中可以實現多個Spider,每個Spider需要有一個能夠區分彼此的唯一標識,Spider的類屬性name便是這個唯一標識。
class BooksSpider(scrapy.Spider):
name = "books"
...
執行scrapy crawl命令時就用到了這個標識,告訴Scrapy使用哪個Spider進行爬取。
設定起始爬取點
Spider必然要從某個或某些頁面開始爬取,我們稱這些頁面爲起始爬取點,可以通過類屬性start_urls來設定起始爬取點:
class BooksSpider(scrapy.Spider):
...
start_urls = ['http://books.toscrape.com/']
...
start_urls通常被實現成一個列表,其中放入所有起始爬取點的url(例子中只有一個起始點)。看到這裏,大家可能會想,請求頁面下載不是一定要提交Request對象麼?而我們僅定義了url列表,是誰暗中構造並提交了相應的Request對象呢?通過閱讀Spider基類的源碼可以找到答案,相關代碼如下:
class Spider(object_ref):
...
def start_requests(self):
for url in self.start_urls:
yield self.make_requests_from_url(url)
def make_requests_from_url(self, url):
return Request(url, dont_filter=True)
def parse(self, response):
raise NotImplementedError
...
從代碼中可以看出,Spider基類的start_requests方法幫助我們構造並提交了Request對象,對其中的原理做如下解釋:
● 實際上,對於起始爬取點的下載請求是由Scrapy引擎調用Spider對象的start_requests方法提交的,由於BooksSpider類沒有實現start_requests方法,因此引擎會調用Spider基類的start_requests方法。
● 在start_requests方法中,self.start_urls便是我們定義的起始爬取點列表(通過實例訪問類屬性),對其進行迭代,用迭代出的每個url作爲參數調用make_requests_from_url方法。
● 在make_requests_from_url方法中,我們找到了真正構造Reqeust對象的代碼,僅使用url和dont_filter參數構造Request對象。
● 由於構造Request對象時並沒有傳遞callback參數來指定頁面解析函數,因此默認將parse方法作爲頁面解析函數。此時BooksSpider必須實現parse方法,否則就會調用Spider基類的parse方法,從而拋出NotImplementedError異常(可以看作基類定義了一個抽象接口)。
● 起始爬取點可能有多個,start_requests方法需要返回一個可迭代對象(列表、生成器等),其中每一個元素是一個Request對象。這裏,start_requests方法被實現成一個生成器函數(生成器對象是可迭代的),每次由yield語句返回一個Request對象。由於起始爬取點的下載請求是由引擎調用Spider對象的start_requests方法產生的,因此我們也可以在BooksSpider中實現start_requests方法(覆蓋基類Spider的start_requests方法),直接構造並提交起始爬取點的Request對象。在某些場景下使用這種方式更加靈活,例如有時想爲Request添加特定的HTTP請求頭部,或想爲Request指定特定的頁面解析函數。以下是通過實現start_requests方法定義起始爬取點的示例代碼
(改寫BooksSpider):
class BooksSpider(scrapy.Spider):
# start_urls = ['http://books.toscrape.com/']
# 實現start_requests 方法, 替代start_urls類屬性
def start_requests(self):
yield scrapy.Request('http://books.toscrape.com/',callback=self.parse_book,
headers={'User-Agent': 'Mozilla/5.0'},dont_filter=True)
# 改用parse_book 作爲回調函數
def parse_book(response):
...
到此,我們介紹完了爲爬蟲設定起始爬取點的兩種方式:
● 定義start_urls屬性。
● 實現start_requests方法。
實現頁面解析函數
頁面解析函數也就是構造Request對象時通過callback參數指定的回調函數(或默認的parse方法)。頁面解析函數是實現Spider中最核心的部分,它需要完成以下兩項工作:
● 使用選擇器提取頁面中的數據,將數據封裝後(Item或字典)提交給Scrapy引擎。
● 使用選擇器或LinkExtractor提取頁面中的鏈接,用其構造新的Request對象並提交給Scrapy引擎(下載鏈接頁面)。一個頁面中可能包含多項數據以及多個鏈接,因此頁面解析函數被要求返回一個可迭代對象(通常被實現成一個生成器函數),每次迭代返回一項數據(Item或字典)或一個Request對象。