本篇會通過介紹一個簡單的項目,走一遍Scrapy抓取流程,通過這個過程,可以對Scrapy對基本用法和原理有大體的了解,作為入門。
歡迎大家關注騰訊雲技術社群-部落格園官方首頁,我們将持續在部落格園為大家推薦技術精品文章哦~
作者:崔慶才
Scrapy入門
在本篇開始之前,假設已經安裝成功了Scrapy,如果尚未安裝,請參照上一節安裝課程。
本節要完成的任務有:
- 建立一個Scrapy項目
- 建立一個Spider來抓取站點和處理資料
- 通過指令行将抓取的内容導出
建立項目
在抓取之前,你必須要先建立一個Scrapy項目,可以直接用
scrapy
指令生成,指令如下:
scrapy startproject tutorial
在任意檔案夾運作都可以,如果提示權限問題,可以加
sudo
運作。這個指令将會建立一個名字為tutorial的檔案夾,檔案夾結構如下:
|____scrapy.cfg # Scrapy部署時的配置檔案
|____tutorial # 項目的子產品,引入的時候需要從這裡引入
| |______init__.py
| |______pycache__
| |____items.py # Items的定義,定義爬取的資料結構
| |____middlewares.py # Middlewares的定義,定義爬取時的中間件
| |____pipelines.py # Pipelines的定義,定義資料管道
| |____settings.py # 配置檔案
| |____spiders # 放置Spiders的檔案夾
| | |______init__.py
| | |______pycache__
建立Spider
Spider是由你來定義的Class,Scrapy用它來從網頁裡抓取内容,并将抓取的結果解析。不過這個Class必須要繼承Scrapy提供的Spider類
scrapy.Spider
,并且你還要定義Spider的名稱和起始請求以及怎樣處理爬取後的結果的方法。
建立一個Spider也可以用指令生成,比如要生成Quotes這個Spider,可以執行指令。
cd tutorial
scrapy genspider quotes
首先進入到剛才建立的tutorial檔案夾,然後執行
genspider
這個指令,第一個參數是Spider的名稱,第二個參數是網站域名。執行完畢之後,你會發現在spiders檔案夾中多了一個
quotes.py
,這就是你剛剛建立的Spider,内容如下:
# -*- coding: utf-8 -*-
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
-
,包含了Spider在啟動時爬取的url清單,初始請求是由它來定義的。start_urls
- parse,是Spider的一個方法,預設情況下,被調用時
裡面的連結構成的請求完成下載下傳後,傳回的response就會作為唯一的參數傳遞給這個函數,該方法負責解析傳回的response,提取資料或者進一步生成要處理的請求。start_urls
建立Item
Item是儲存爬取資料的容器,它的使用方法和字典類似,雖然你可以用字典來表示,不過Item相比字典多了額外的保護機制,可以避免拼寫錯誤或者為定義字段錯誤。
建立Item需要繼承
scrapy.Item
類,并且定義類型為
scrapy.Field
的類屬性來定義一個Item。觀察目标網站,我們可以擷取到到内容有text, author, tags
是以可以定義如下的Item,修改
items.py
如下:
import scrapy
class QuoteItem(scrapy.Item):
text = scrapy.Field()
author = scrapy.Field()
tags = scrapy.Field()
定義了三個Field,接下來爬取時我們會使用它。
解析Response
在上文中說明了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()
在這裡使用了CSS選擇器的文法,首先利用選擇器選取所有的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,接下來就要輪到使用它了,你可以把它了解為一個字典,不過在聲明的時候需要執行個體化。然後依次對剛才解析的結果指派,傳回即可。
接下來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,回調函數,當這個請求完成之後,擷取到response,會将response作為參數傳遞給這個回調函數,回調函數進行解析或生成下一個請求,如上文的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)
操作,通過
::attr
加屬性名稱我們可以擷取屬性的值。然後再調用extract_first方法擷取内容。
第二句是調用了urljoin方法,它可以将相對url構造成一個絕對的url,例如擷取到的下一頁的位址是
/page/2
,通過urljoin方法處理後得到的結果就是
http://quotes.toscrape.com/page/2/
第三句是通過url和callback構造了一個新的請求,回調函數callback依然使用的parse方法。這樣在完成這個請求後,response會重新經過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後,我們隻在控制台看到了輸出結果,如果想将結果儲存該怎麼辦呢?
比如最簡單的形式,将結果儲存成Json檔案。
要完成這個其實不需要你寫任何額外的代碼,Scrapy提供了Feed Exports可以輕松地将抓取結果輸出,例如我們想将上面的結果儲存成Json檔案,可以執行如下指令:
scrapy crawl quotes -o quotes.json
運作後發現項目内就會多了一個quotes.json檔案,裡面包含的就是剛才抓取的所有内容,是一個Json格式,多個項目由中括号包圍,是一個合法的Json格式。
另外你還可以每一個Item一個Json,最後的結果沒有中括号包圍,一行對應一個Item,指令如下:
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:[email protected]/path/to/quotes.csv
其中ftp輸出需要你正确配置好你的使用者名,密碼,位址,輸出路徑,否則會報錯。
通過Scrapy提供的Feed Exports我們可以輕松地輸出抓取結果到檔案,對于一些小型項目這應該是足夠了,不過如果想要更複雜的輸出,如輸出到資料庫等等,你可以使用Item Pileline更友善地實作。
使用Item Pipeline
至此,你已經可以成功地完成抓取并将結果儲存了,如果你想進行更複雜的操作,如将結果儲存到資料庫,如MongoDB,或者篩選某些有用的Item,可以定義Item Pileline來實作。
Item Pipeline意為項目管道,當生成Item後,它會自動被送到Item Pipeline進行處理,我們常用它來做如下操作:
- 清理HTML資料
- 驗證爬取資料,檢查爬取字段
- 查重并丢棄重複内容
- 将爬取結果儲存到資料庫
要實作一個Item Pipeline很簡單,隻需要定義一個類并實作process_item方法即可,啟用後,Item Pipeline會自動調用這個方法,這個方法必須傳回包含資料的字典或是Item對象,或者抛出DropItem異常。
這個方法由兩個參數,一個是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和spider,首先判斷item的text屬性是否存在,如果不存在,那就跑出DropItem異常,如果存在,再判斷長度是否大于50,如果大于,那就截斷然後拼接省略号,再将item傳回即可。
接下來,我們再将處理後的item存入MongoDB,如果你還沒有安裝,請先安裝好MongoDB。
另外還需要安裝一個MongoDB開發包pymongo,利用pip安裝即可:
pip3 install pymongo
接下來定義另外一個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()
在這個類中,實作了API定義的另外幾個方法。
- from_crawler,這是一個類方法,用
辨別,是一種依賴注入的方式,方法的參數就是crawler,通過crawler這個我們可以拿到全局配置的每個配置資訊,在全局配置@classmethod
中我們可以定義settings.py
和MONGO_URI
來指定MongoDB連接配接需要的位址和資料庫名稱,拿到配置資訊之後傳回類對象即可。是以這個方法的定義主要是用來擷取MONGO_DB
中的配置的。settings.py
-
,當spider被開啟時,這個方法被調用。在這裡主要進行了一些初始化操作。open_spider
-
,當spider被關閉時,這個方法會調用,在這裡将資料庫連接配接關閉。close_spider
那麼最主要的
process_item
方法則執行了資料插入操作。
好,定義好這兩個類後,我們需要在
settings.py
中使用他們,還需要定義MongoDB的連接配接資訊。
在
settings.py
中加入如下内容:
ITEM_PIPELINES = {
'tutorial.pipelines.TextPipeline': 300,
'tutorial.pipelines.MongoPipeline': 400,
}
MONGO_URI='localhost'
MONGO_DB='tutorial'
指派
ITEM_PIPELINES
字典,鍵名是Pipeline的類名稱,鍵值是調用優先級,數字越小越先被調用。
定義好了之後,再重新執行爬取,指令如下:
scrapy crawl quotes
爬取結束後,可以觀察到MongoDB中建立了一個tutorial的資料庫,QuoteItem的表。
到現在,我們就通過抓取quotes完成了整個Scrapy的簡單入門,但這隻是冰山一角,還有很多内容等待我們去探索,後面會進行講解。
源代碼
本節代碼:https://github.com/Germey/ScrapyTutorial
【有獎讨論】程式員,怎麼應對三十歲? 點選檢視詳情
相關閱讀
利用Scrapy爬取所有知乎使用者詳細資訊并存至MongoDB
反爬蟲和抗DDOS攻擊技術實踐
如何輕松爬取網頁資料?
此文已由作者授權騰訊雲技術社群釋出,轉載請注明文章出處
原文連結:https://www.qcloud.com/community/article/592498
擷取更多騰訊海量技術實踐幹貨,歡迎大家前往騰訊雲技術社群
海量技術實踐經驗,盡在雲加社群!
https://cloud.tencent.com/developer