第42讲:scrapy框架的基本使用
接下來介紹一個簡單的項目,完成一遍 Scrapy 抓取流程。通過這個過程,我們可以對 Scrapy 的基本用法和原理有大體了解。
本節目標
本節要完成的任務如下。
- 創建一個 Scrapy 項目。
- 創建一個 Spider 來抓取站點和處理數據。
- 通過命令行將抓取的內容導出。
- 將抓取的內容保存到 MongoDB 數據庫。
本節抓取的目標站點為 http://quotes.toscrape.com/。
準備工作
我們需要安裝好 Scrapy 框架、MongoDB 和 PyMongo 庫。如果尚未安裝,請參照之前幾節的安裝說明。
創建項目
創建一個 Scrapy 項目,項目文件可以直接用 scrapy 命令生成,命令如下所示:
scrapy startproject tutorial這個命令可以在任意文件夾運行。如果提示權限問題,可以加 sudo 運行該命令。這個命令將會創建一個名為 tutorial 的文件夾,文件夾結構如下所示:
scrapy.cfg # Scrapy 部署時的配置文件 tutorial # 項目的模塊,引入的時候需要從這里引入__init__.py items.py # Items 的定義,定義爬取的數據結構middlewares.py # Middlewares 的定義,定義爬取時的中間件pipelines.py # Pipelines 的定義,定義數據管道settings.py # 配置文件spiders # 放置 Spiders 的文件夾__init__.py創建 Spider
Spider 是自己定義的類,Scrapy 用它從網頁里抓取內容,并解析抓取的結果。不過這個類必須繼承 Scrapy 提供的 Spider 類 scrapy.Spider,還要定義 Spider 的名稱和起始請求,以及怎樣處理爬取后的結果的方法。
你也可以使用命令行創建一個 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 多了額外的保護機制,可以避免拼寫錯誤或者定義字段錯誤。
創建 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()這里定義了三個字段,將類的名稱修改為 QuoteItem,接下來爬取時我們會使用到這個 Item。
解析 Response
前面我們看到,parse 方法的參數 response 是 start_urls 里面的鏈接爬取后的結果。所以在 parse 方法中,我們可以直接對 response 變量包含的內容進行解析,比如瀏覽請求結果的網頁源代碼,或者進一步分析源代碼內容,或者找出結果中的鏈接而得到下一個請求。
我們可以看到網頁中既有我們想要的結果,又有下一頁的鏈接,這兩部分內容我們都要進行處理。
首先看看網頁結構,如圖所示。每一頁都有多個 class 為 quote 的區塊,每個區塊內都包含 text、author、tags。那么我們先找出所有的 quote,然后提取每一個 quote 中的內容。
提取的方式可以是 CSS 選擇器或 XPath 選擇器。在這里我們使用 CSS 選擇器進行選擇,parse 方法的改寫如下所示:
這里首先利用選擇器選取所有的 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.”使用 Item
上文定義了 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。
后續 Request
上面的操作實現了從初始頁面抓取內容。那么,下一頁的內容該如何抓取?這就需要我們從當前頁面中找到信息來生成下一個請求,然后在下一個請求的頁面里找到信息再構造下一個請求。這樣循環往復迭代,從而實現整站的爬取。
將剛才的頁面拉到最底部,如圖所示。
有一個 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 的運行結果了。
2020-02-19 13:37:20 [scrapy.utils.log] INFO: Scrapy 1.3.0 started (bot: tutorial) 2020-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'} 2020-02-19 13:37:20 [scrapy.middleware] INFO: Enabled extensions: ['scrapy.extensions.logstats.LogStats','scrapy.extensions.telnet.TelnetConsole','scrapy.extensions.corestats.CoreStats'] 2020-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 輸出了當前的版本號,以及正在啟動的項目名稱。接著輸出了當前 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 來完成。
使用 Item Pipeline
如果想進行更復雜的操作,如將結果保存到 MongoDB 數據庫,或者篩選某些有用的 Item,則我們可以定義 Item Pipeline 來實現。
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 = 50def process_item(self, item, spider):if item['text']:if len(item['text']) > self.limit:item['text'] = item['text'][0:self.limit].rstrip() + '...'return itemelse:return DropItem('Missing Text')這段代碼在構造方法里定義了限制長度為 50,實現了 process_item 方法,其參數是 item 和 spider。首先該方法判斷 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_uriself.mongo_db = mongo_db ?@classmethoddef 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 的表,如圖所示。
代碼
本節代碼地址:https://github.com/Python3WebSpider/ScrapyTutorial。
結語
我們通過抓取 Quotes 網站完成了整個 Scrapy 的簡單入門。但這只是冰山一角,還有很多內容等待我們去探索。
總結
以上是生活随笔為你收集整理的第42讲:scrapy框架的基本使用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 第43讲:灵活好用的 Spider 的用
- 下一篇: 第41讲:Scrapy框架的介绍