接下來介紹一個簡單的項目,完成一遍Scrapy抓取流程。經過這個過程,咱們能夠對Scrapy的基本用法和原理有大致瞭解。
css
本節要完成的任務以下。
數據庫
建立一個Scrapy項目。json
建立一個Spider來抓取站點和處理數據。cookie
經過命令行將抓取的內容導出。數據結構
將抓取的內容保存的到MongoDB數據庫。app
咱們須要安裝好Scrapy框架、MongoDB和PyMongo庫。
框架
建立一個Scrapy項目,項目文件能夠直接用scrapy
命令生成,命令以下所示:
dom
scrapy startproject tutorial
這個命令能夠在任意文件夾運行。若是提示權限問題,能夠加sudo運行該命令。這個命令將會建立一個名爲tutorial的文件夾,文件夾結構以下所示:scrapy
scrapy.cfg # Scrapy部署時的配置文件 tutorial # 項目的模塊,須要從這裏引入 __init__.py items.py # Items的定義,定義爬取的數據結構 middlewares.py # Middlewares的定義,定義爬取時的中間件 pipelines.py # Pipelines的定義,定義數據管道 settings.py # 配置文件 spiders # 放置Spiders的文件夾 __init__.py
Spider是本身定義的Class,Scrapy用它來從網頁裏抓取內容,並解析抓取的結果。不過這個Class必須繼承Scrapy提供的Spider類scrapy.Spider
,還要定義Spider的名稱和起始請求,以及怎樣處理爬取後的結果的方法。
ide
也可使用命令行建立一個Spider。好比要生成Quotes這個Spider,能夠執行以下命令:
cd tutorial scrapy genspider quotes
進入剛纔建立的tutorial文件夾,而後執行genspider
命令。第一個參數是Spider的名稱,第二個參數是網站域名。執行完畢以後,spiders文件夾中多了一個quotes.py,它就是剛剛建立的Spider,內容以下所示:
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
allowed_domains = ["quotes.toscrape.com"]
start_urls = ['http://quotes.toscrape.com/']
def parse(self, response):
pass
這裏有三個屬性——name
、allowed_domains
和start_urls
,還有一個方法parse
。
name
,它是每一個項目惟一的名字,用來區分不一樣的Spider。
allowed_domains
,它是容許爬取的域名,若是初始或後續的請求連接不是這個域名下的,則請求連接會被過濾掉。
start_urls
,它包含了Spider在啓動時爬取的url列表,初始請求是由它來定義的。
parse
,它是Spider的一個方法。默認狀況下,被調用時start_urls
裏面的連接構成的請求完成下載執行後,返回的響應就會做爲惟一的參數傳遞給這個函數。該方法負責解析返回的響應、提取數據或者進一步生成要處理的請求。
Item是保存爬取數據的容器,它的使用方法和字典相似。不過,相比字典,Item多了額外的保護機制,能夠避免拼寫錯誤或者定義字段錯誤。
建立Item須要繼承scrapy.Item
類,而且定義類型爲scrapy.Field
的字段。觀察目標網站,咱們能夠獲取到到內容有text
、author
、tags
。
定義Item,此時將items.py修改以下:
import scrapy
class QuoteItem(scrapy.Item):
text = scrapy.Field()
author = scrapy.Field()
tags = scrapy.Field()
這裏定義了三個字段,接下來爬取時咱們會使用到這個Item。
上文中咱們看到,parse()
方法的參數resposne
是start_urls
裏面的連接爬取後的結果。因此在parse
方法中,咱們能夠直接對response
變量包含的內容進行解析,好比瀏覽請求結果的網頁源代碼,或者進一步分析源代碼內容,或者找出結果中的連接而獲得下一個請求。
咱們能夠看到網頁中既有咱們想要的結果,又有下一頁的連接,這兩部份內容咱們都要進行處理。
首先看看網頁結構,以下圖所示。每一頁都有多個class
爲quote
的區塊,每一個區塊內都包含text
、author
、tags
。那麼咱們先找出全部的quote
,而後提取每個quote
中的內容。
提取的方式能夠是CSS選擇器或XPath選擇器。在這裏咱們使用CSS選擇器進行選擇,parse()
方法的改寫以下所示:
def parse(self, response):
quotes = response.css('.quote')
for quote in quotes:
text = quote.css('.text::text').extract_first()
author = quote.css('.author::text').extract_first()
tags = quote.css('.tags .tag::text').extract()
這裏首先利用選擇器選取全部的quote,並將其賦值爲quotes
變量,而後利用for
循環對每一個quote
遍歷,解析每一個quote
的內容。
對text
來講,觀察到它的class
爲text
,因此能夠用.text
選擇器來選取,這個結果其實是整個帶有標籤的節點,要獲取它的正文內容,能夠加::text
來獲取。這時的結果是長度爲1的列表,因此還須要用extract_first()
方法來獲取第一個元素。而對於tags
來講,因爲咱們要獲取全部的標籤,因此用extract()
方法獲取整個列表便可。
以第一個quote
的結果爲例,各個選擇方法及結果的說明以下內容。
源碼以下:
<div class="quote" itemscope="" itemtype="http://schema.org/CreativeWork">
<span class="text" itemprop="text">「The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.」</span>
<span>by <small class="author" itemprop="author">Albert Einstein</small>
<a href="/author/Albert-Einstein">(about)</a>
</span>
<div class="tags">
Tags:
<meta class="keywords" itemprop="keywords" content="change,deep-thoughts,thinking,world">
<a class="tag" href="/tag/change/page/1/">change</a>
<a class="tag" href="/tag/deep-thoughts/page/1/">deep-thoughts</a>
<a class="tag" href="/tag/thinking/page/1/">thinking</a>
<a class="tag" href="/tag/world/page/1/">world</a>
</div>
</div>
不一樣選擇器的返回結果以下內容。
quote.css('.text')
[<Selector xpath="descendant-or-self::*[@class and contains(concat(' ', normalize-space(@class), ' '), ' text ')]" data='<span class="text" itemprop="text">「The '>]
quote.css('.text::text')
[<Selector xpath="descendant-or-self::*[@class and contains(concat(' ', normalize-space(@class), ' '), ' text ')]/text()" data='「The world as we have created it is a pr'>]
quote.css('.text').extract()
['<span class="text" itemprop="text">「The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.」</span>']
quote.css('.text::text').extract()
['「The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.」']
quote.css('.text::text').extract_first()
「The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.」
因此,對於text
,獲取結果的第一個元素便可,因此使用extract_first()
方法,對於tags
,要獲取全部結果組成的列表,因此使用extract()
方法。
上文定義了Item,接下來就要使用它了。Item能夠理解爲一個字典,不過在聲明的時候須要實例化。而後依次用剛纔解析的結果賦值Item的每個字段,最後將Item返回便可。
QuotesSpider
的改寫以下所示:
import scrapy
from tutorial.items import QuoteItem
class QuotesSpider(scrapy.Spider):
name = "quotes"
allowed_domains = ["quotes.toscrape.com"]
start_urls = ['http://quotes.toscrape.com/']
def parse(self, response):
quotes = response.css('.quote')
for quote in quotes:
item = QuoteItem()
item['text'] = quote.css('.text::text').extract_first()
item['author'] = quote.css('.author::text').extract_first()
item['tags'] = quote.css('.tags .tag::text').extract()
yield item
如此一來,首頁的全部內容被解析出來,並被賦值成了一個個QuoteItem
。
上面的操做實現了從初始頁面抓取內容。那麼,下一頁的內容該如何抓取?這就須要咱們從當前頁面中找到信息來生成下一個請求,而後在下一個請求的頁面裏找到信息再構造再下一個請求。這樣循環往復迭代,從而實現整站的爬取。
將剛纔的頁面拉到最底部,以下圖所示。
這裏有一個Next按鈕。查看它的源代碼,能夠發現它的連接是/page/2/,全連接就是:http://quotes.toscrape.com/page/2,經過這個連接咱們就能夠構造下一個請求。
構造請求時須要用到scrapy.Request。這裏咱們傳遞兩個參數——url
和callback
,這兩個參數的說明以下。
url
:它是請求連接。
callback
:它是回調函數。當指定了該回調函數的請求完成以後,獲取到響應,引擎會將該響應做爲參數傳遞給這個回調函數。回調函數進行解析或生成下一個請求,回調函數如上文的parse()
所示。
因爲parse()
就是解析text
、author
、tags
的方法,而下一頁的結構和剛纔已經解析的頁面結構是同樣的,因此咱們能夠再次使用parse()
方法來作頁面解析。
接下來咱們要作的就是利用選擇器獲得下一頁連接並生成請求,在parse()
方法後追加以下的代碼:
next = response.css('.pager .next a::attr(href)').extract_first()
url = response.urljoin(next)
yield scrapy.Request(url=url, callback=self.parse)
第一句代碼首先經過CSS選擇器獲取下一個頁面的連接,即要獲取a超連接中的href
屬性。這裏用到了::attr(href)
操做。而後再調用extract_first()
方法獲取內容。
第二句代碼調用了urljoin()
方法,urljoin()
方法能夠將相對URL構形成一個絕對的URL。例如,獲取到的下一頁地址是/page/2,urljoin()
方法處理後獲得的結果就是:http://quotes.toscrape.com/page/2/。
第三句代碼經過url
和callback
變量構造了一個新的請求,回調函數callback
依然使用parse()
方法。這個請求完成後,響應會從新通過parse
方法處理,獲得第二頁的解析結果,而後生成第二頁的下一頁,也就是第三頁的請求。這樣爬蟲就進入了一個循環,直到最後一頁。
經過幾行代碼,咱們就輕鬆實現了一個抓取循環,將每一個頁面的結果抓取下來了。
如今,改寫以後的整個Spider
類以下所示:
import scrapy
from tutorial.items import QuoteItem
class QuotesSpider(scrapy.Spider):
name = "quotes"
allowed_domains = ["quotes.toscrape.com"]
start_urls = ['http://quotes.toscrape.com/']
def parse(self, response):
quotes = response.css('.quote')
for quote in quotes:
item = QuoteItem()
item['text'] = quote.css('.text::text').extract_first()
item['author'] = quote.css('.author::text').extract_first()
item['tags'] = quote.css('.tags .tag::text').extract()
yield item
next = response.css('.pager .next a::attr("href")').extract_first()
url = response.urljoin(next)
yield scrapy.Request(url=url, callback=self.parse)
接下來,進入目錄,運行以下命令:
scrapy crawl quotes
就能夠看到Scrapy的運行結果了。
2017-02-19 13:37:20 [scrapy.utils.log] INFO: Scrapy 1.3.0 started (bot: tutorial) 2017-02-19 13:37:20 [scrapy.utils.log] INFO: Overridden settings: {'NEWSPIDER_MODULE': 'tutorial.spiders', 'SPIDER_MODULES': ['tutorial.spiders'], 'ROBOTSTXT_OBEY': True, 'BOT_NAME': 'tutorial'} 2017-02-19 13:37:20 [scrapy.middleware] INFO: Enabled extensions: ['scrapy.extensions.logstats.LogStats', 'scrapy.extensions.telnet.TelnetConsole', 'scrapy.extensions.corestats.CoreStats'] 2017-02-19 13:37:20 [scrapy.middleware] INFO: Enabled downloader middlewares: ['scrapy.downloadermiddlewares.robotstxt.RobotsTxtMiddleware', 'scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware', 'scrapy.downloadermiddlewares.downloadtimeout.DownloadTimeoutMiddleware', 'scrapy.downloadermiddlewares.defaultheaders.DefaultHeadersMiddleware', 'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware', 'scrapy.downloadermiddlewares.retry.RetryMiddleware', 'scrapy.downloadermiddlewares.redirect.MetaRefreshMiddleware', 'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware', 'scrapy.downloadermiddlewares.redirect.RedirectMiddleware', 'scrapy.downloadermiddlewares.cookies.CookiesMiddleware', 'scrapy.downloadermiddlewares.stats.DownloaderStats'] 2017-02-19 13:37:20 [scrapy.middleware] INFO: Enabled spider middlewares: ['scrapy.spidermiddlewares.httperror.HttpErrorMiddleware', 'scrapy.spidermiddlewares.offsite.OffsiteMiddleware', 'scrapy.spidermiddlewares.referer.RefererMiddleware', 'scrapy.spidermiddlewares.urllength.UrlLengthMiddleware', 'scrapy.spidermiddlewares.depth.DepthMiddleware'] 2017-02-19 13:37:20 [scrapy.middleware] INFO: Enabled item pipelines: [] 2017-02-19 13:37:20 [scrapy.core.engine] INFO: Spider opened 2017-02-19 13:37:20 [scrapy.extensions.logstats] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min) 2017-02-19 13:37:20 [scrapy.extensions.telnet] DEBUG: Telnet console listening on 127.0.0.1:6023 2017-02-19 13:37:21 [scrapy.core.engine] DEBUG: Crawled (404) <GET http://quotes.toscrape.com/robots.txt> (referer: None) 2017-02-19 13:37:21 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://quotes.toscrape.com/> (referer: None) 2017-02-19 13:37:21 [scrapy.core.scraper] DEBUG: Scraped from <200 http://quotes.toscrape.com/> {'author': u'Albert Einstein', 'tags': [u'change', u'deep-thoughts', u'thinking', u'world'], 'text': u'\u201cThe world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.\u201d'} 2017-02-19 13:37:21 [scrapy.core.scraper] DEBUG: Scraped from <200 http://quotes.toscrape.com/> {'author': u'J.K. Rowling', 'tags': [u'abilities', u'choices'], 'text': u'\u201cIt is our choices, Harry, that show what we truly are, far more than our abilities.\u201d'} ... 2017-02-19 13:37:27 [scrapy.core.engine] INFO: Closing spider (finished) 2017-02-19 13:37:27 [scrapy.statscollectors] INFO: Dumping Scrapy stats: {'downloader/request_bytes': 2859, 'downloader/request_count': 11, 'downloader/request_method_count/GET': 11, 'downloader/response_bytes': 24871, 'downloader/response_count': 11, 'downloader/response_status_count/200': 10, 'downloader/response_status_count/404': 1, 'dupefilter/filtered': 1, 'finish_reason': 'finished', 'finish_time': datetime.datetime(2017, 2, 19, 5, 37, 27, 227438), 'item_scraped_count': 100, 'log_count/DEBUG': 113, 'log_count/INFO': 7, 'request_depth_max': 10, 'response_received_count': 11, 'scheduler/dequeued': 10, 'scheduler/dequeued/memory': 10, 'scheduler/enqueued': 10, 'scheduler/enqueued/memory': 10, 'start_time': datetime.datetime(2017, 2, 19, 5, 37, 20, 321557)} 2017-02-19 13:37:27 [scrapy.core.engine] INFO: Spider closed (finished)
這裏只是部分運行結果,中間一些抓取結果已省略。
首先,Scrapy輸出了當前的版本號以及正在啓動的項目名稱。接着輸出了當前settings.py中一些重寫後的配置。而後輸出了當前所應用的Middlewares和Pipelines。Middlewares默認是啓用的,能夠在settings.py中修改。Pipelines默認是空,一樣也能夠在settings.py中配置。後面會對它們進行講解。
接下來就是輸出各個頁面的抓取結果了,能夠看到爬蟲一邊解析,一邊翻頁,直至將全部內容抓取完畢,而後終止。
最後,Scrapy輸出了整個抓取過程的統計信息,如請求的字節數、請求次數、響應次數、完成緣由等。
整個Scrapy程序成功運行。咱們經過很是簡單的代碼就完成了一個網站內容的爬取,這樣相比以前一點點寫程序簡潔不少。
運行完Scrapy後,咱們只在控制檯看到了輸出結果。若是想保存結果該怎麼辦呢?
要完成這個任務其實不須要任何額外的代碼,Scrapy提供的Feed Exports能夠輕鬆將抓取結果輸出。例如,咱們想將上面的結果保存成JSON文件,能夠執行以下命令:
scrapy crawl quotes -o quotes.json
命令運行後,項目內多了一個quotes.json文件,文件包含了剛纔抓取的全部內容,內容是JSON格式。
另外咱們還能夠每個Item輸出一行JSON,輸出後綴爲jl,爲jsonline的縮寫,命令以下所示:
scrapy crawl quotes -o quotes.jl
或
scrapy crawl quotes -o quotes.jsonlines
輸出格式還支持不少種,例如csv、xml、pickle、marshal等,還支持ftp、s3等遠程輸出,另外還能夠經過自定義ItemExporter來實現其餘的輸出。
例如,下面命令對應的輸出分別爲csv、xml、pickle、marshal格式以及ftp遠程輸出:
scrapy crawl quotes -o quotes.csv scrapy crawl quotes -o quotes.xml scrapy crawl quotes -o quotes.pickle scrapy crawl quotes -o quotes.marshal scrapy crawl quotes -o ftp://user:pass@ftp.example.com/path/to/quotes.csv
其中,ftp輸出須要正確配置用戶名、密碼、地址、輸出路徑,不然會報錯。
經過Scrapy提供的Feed Exports,咱們能夠輕鬆地輸出抓取結果到文件。對於一些小型項目來講,這應該足夠了。不過若是想要更復雜的輸出,如輸出到數據庫等,咱們可使用Item Pileline來完成。
若是想進行更復雜的操做,如將結果保存到MongoDB數據庫,或者篩選某些有用的Item,則咱們能夠定義Item Pileline來實現。
Item Pipeline爲項目管道。當Item生成後,它會自動被送到Item Pipeline進行處理,咱們經常使用Item Pipeline來作以下操做。
清理HTML數據。
驗證爬取數據,檢查爬取字段。
查重並丟棄重複內容。
將爬取結果保存到數據庫。
要實現Item Pipeline很簡單,只須要定義一個類並實現process_item()
方法便可。啓用Item Pipeline後,Item Pipeline會自動調用這個方法。process_item()
方法必須返回包含數據的字典或Item對象,或者拋出DropItem異常。
process_item()
方法有兩個參數。一個參數是item
,每次Spider生成的Item都會做爲參數傳遞過來。另外一個參數是spider
,就是Spider的實例。
接下來,咱們實現一個Item Pipeline,篩掉text
長度大於50的Item,並將結果保存到MongoDB。
修改項目裏的pipelines.py文件,以前用命令行自動生成的文件內容能夠刪掉,增長一個TextPipeline
類,內容以下所示:
from scrapy.exceptions import DropItem
class TextPipeline(object):
def __init__(self):
self.limit = 50
def process_item(self, item, spider):
if item['text']:
if len(item['text']) > self.limit:
item['text'] = item['text'][0:self.limit].rstrip() + '...'
return item
else:
return DropItem('Missing Text')
這段代碼在構造方法裏定義了限制長度爲50,實現了process_item()
方法,其參數是item
和spide
r。首先該方法判斷item
的text
屬性是否存在,若是不存在,則拋出DropItem
異常;若是存在,再判斷長度是否大於50,若是大於,那就截斷而後拼接省略號,再將item
返回便可。
接下來,咱們將處理後的item
存入MongoDB,定義另一個Pipeline。一樣在pipelines.py中,咱們實現另外一個類MongoPipeline
,內容以下所示:
import pymongo
class MongoPipeline(object):
def __init__(self, mongo_uri, mongo_db):
self.mongo_uri = mongo_uri
self.mongo_db = mongo_db
@classmethod
def from_crawler(cls, crawler):
return cls(
mongo_uri=crawler.settings.get('MONGO_URI'),
mongo_db=crawler.settings.get('MONGO_DB')
)
def open_spider(self, spider):
self.client = pymongo.MongoClient(self.mongo_uri)
self.db = self.client[self.mongo_db]
def process_item(self, item, spider):
name = item.__class__.__name__
self.db[name].insert(dict(item))
return item
def close_spider(self, spider):
self.client.close()
MongoPipeline
類實現了API定義的另外幾個方法。
from_crawler
。它是一個類方法,用@classmethod
標識,是一種依賴注入的方式。它的參數就是crawler
,經過crawler
咱們能夠拿到全局配置的每一個配置信息。在全局配置settings.py中,咱們能夠定義MONGO_URI
和MONGO_DB
來指定MongoDB鏈接須要的地址和數據庫名稱,拿到配置信息以後返回類對象便可。因此這個方法的定義主要是用來獲取settings.py中的配置的。
open_spider
。當Spider開啓時,這個方法被調用。上文程序中主要進行了一些初始化操做。
close_spider
。當Spider關閉時,這個方法會調用。上文程序中將數據庫鏈接關閉。
最主要的process_item()
方法則執行了數據插入操做。
定義好TextPipeline
和MongoPipeline
這兩個類後,咱們須要在settings.py中使用它們。MongoDB的鏈接信息還須要定義。
咱們在settings.py中加入以下內容:
ITEM_PIPELINES = {
'tutorial.pipelines.TextPipeline': 300,
'tutorial.pipelines.MongoPipeline': 400,
}
MONGO_URI='localhost'
MONGO_DB='tutorial'
賦值ITEM_PIPELINES
字典,鍵名是Pipeline的類名稱,鍵值是調用優先級,是一個數字,數字越小則對應的Pipeline越先被調用。
再從新執行爬取,命令以下所示:
scrapy crawl quotes
爬取結束後,MongoDB中建立了一個tutorial的數據庫、QuoteItem的表,以下圖所示。
長的text
已經被處理並追加了省略號,短的text
保持不變,author
和tags
也都相應保存。