將以 'quotes.toscrape.com' 網(wǎng)站作為爬取的對象。
在這個教程中將圍繞如下內(nèi)容展開:
- 創(chuàng)建一個新的 Scrapy 項目
- 編寫一個 spider 去爬網(wǎng)站薄湿,提取數(shù)據(jù)
- 使用命令行導(dǎo)出抓取數(shù)據(jù)
- 修改爬蟲遞歸下一個鏈接
- 使用 spider 屬性
創(chuàng)建項目
進入目標(biāo)項目文件夾坯汤,執(zhí)行以下代碼:
scrapy startproject tutorial
這會創(chuàng)建一個 tutorial 路徑骗奖,包含以下內(nèi)容
turorial/
scrapy.cfg # 部署配置的文件
tutorial/ # 項目的 Python 模塊嫩舟,import 導(dǎo)入的代碼在模塊中
__init__.py
items.py # 項目的 items(條目)自定義文件
middlewares.py # 項目的中間件文件
pipelines.py # 項目的管道文件
settings.py # 配置文件
spiders/ # 后期放 spider 的文件
__init__.py
第一個項目
自定義的 Spider 類用于從網(wǎng)站(一組網(wǎng)站)中抓取數(shù)據(jù),它們必須為 scrapy.Spider 子類叛复,初始化的請求為必選項仔引,可選項包括對下一頁的相關(guān)處理、從解析下載的頁面內(nèi)容提取數(shù)據(jù)的細節(jié)褐奥。
我們的第一個 Spider 保存在 quotes_spider.py 文件中咖耘,在 tutorial/spiders 路徑下:
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:唯一識別名鲤看,在當(dāng)前項目中必須唯一缘揪。
- start_requests():必須返回請求的迭代(可以是請求列表或生成器函數(shù))耍群,Spider 從這里開始爬。
- parse(): 每個請求發(fā)起后調(diào)用找筝,用于處理響應(yīng)蹈垢。response 參數(shù)是 TextResponse 的實例,它包含了頁面內(nèi)容袖裕,內(nèi)容隨后會被處理曹抬。parse() 方法實現(xiàn)許多功能,包括解析響應(yīng)急鳄,提取爬取的數(shù)據(jù)并解析為字典谤民,尋找下一個爬取的 URL 并對此鏈接發(fā)起新的請求。
如何執(zhí)行我們的 Spider
為了使 Spider 生效疾宏,需要進入項目頂層路徑张足,并執(zhí)行如下命令:
scrapy crawl quotes
這個命令以 quotes 名稱作為參數(shù)執(zhí)行,并且會對 qotes.toscrape.com 域名的網(wǎng)站發(fā)起請求坎藐。
執(zhí)行命令后为牍,可以發(fā)現(xiàn)在當(dāng)前路徑下,有兩個新文件: quotes-1.html quotes-2.html
剛才發(fā)生了什么
Scrapy 會規(guī)劃 Spider 中 start_requests 返回的 scrapy.Request岩馍。一旦接受到每個請求的響應(yīng)碉咆,就會實例化 Response 對象,將響應(yīng)實例作為回調(diào)方法的參數(shù)(在這個例子中蛀恩,是 parse 方法)疫铜,回調(diào)與請求 Request 關(guān)聯(lián)。
start_requests 方法的簡寫
我們可以僅僅定義 start_urls 類屬性双谆,它是 URL 的列表組合壳咕,用于替代 start_requests() 中通過生成 scrapy.Reuqest 對象的做法励稳。定義了 start_urls 后,會使用默認的 start_requests() 來創(chuàng)建 Spider 中的初始化請求囱井。
于是代碼可以改成:
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):
page = response.url.split("/")[-2]
filename = 'quotes-%s.html' % page
with open(filename, 'wb') as f:
f.write(response.body)
變化在于 start_urls 替代了 start_requests() 方法驹尼,只提供 URL 列表,其余的初始化請求操作交給父類中默認的 start_requests() 方法庞呕。
parse() 方法會被在每個 URL 被請求時調(diào)用新翎,即使我們沒有明確的告訴 Scrapy 這么去做,這是因為 parse() 是 Scrapy 默認自動調(diào)用的方法住练。
提取數(shù)據(jù)
使用 Scrapy 提取數(shù)據(jù)最好的調(diào)試方式:Scrapy shell地啰,執(zhí)行:
scrapy shell 'http://quotes.toscrape.com/page/1'
使用 shell,你可以嘗試從響應(yīng)中選擇元素讲逛,以 CSS 選擇器為例:
In [4]: response.css('title')
Out[4]: [<Selector xpath='descendant-or-self::title' data='<title>Quotes to Scrape</title>'>]
返回的結(jié)果 SelectorList亏吝,類列表對象,它擴展了list功能盏混,可以理解為 Selector 的列表蔚鸥。這個列表對象被 XML/HTML 元素包裹,允許你執(zhí)行進一步的查詢和更細微力度的選擇许赃。
提取標(biāo)簽文本
比如止喷,從剛才的 title 對象中提取文本,可以這么做:
>>> response.css('title::text').getall()
['Quotes to Scrape']
這里有兩點要說明:
- 在 CSS 查詢中添加了 '::text'混聊,這意味著只提取 <title> 中的文本弹谁。如果定義 '::text',會返回整個 title 元素內(nèi)容(包括標(biāo)簽):
>>> response.css('title').getall()
['<title>Quotes to Scrape</title>']
- 調(diào)用 '.getall()'句喜,它返回一個列表:這意味著選擇器可能返回一個或多個結(jié)果预愤;如果你只需要第一個結(jié)果,可以使用 '.get()'咳胃,也可以通過 python 代碼先選擇第一個結(jié)果:
response.css("title::text")[0].get()
但是植康,如果直接對 SelectorList 實例使用 get() 方法可以避免 'IndexError' 錯誤,它會處理成 None 返回拙绊,所以推薦直接使用 '.get()'
正則匹配
除了使用 'getall()' 和 'get()' 方法向图,可以使用 're()' 方法來提取數(shù)據(jù)
瀏覽器查看響應(yīng)結(jié)果
為了更好的使用合適的 CSS 選擇器,可以使用 view(response) 來查看響應(yīng)标沪。它會在瀏覽器中顯示響應(yīng)內(nèi)容榄攀,你可以使用瀏覽器開發(fā)者工具來檢測 HTML 并選擇合適的選擇器。
XPath 介紹
除了 CSS金句,Scrapy 選擇器還支持使用 XPath 表達式:
>>> response.xpath('//title')
[<Selector xpath='//title' data='<title>Quotes to Scrape</title>'>]
>>> response.xpath('//title/text()').get()
'Quotes to Scrape'
XPath 表達式非常強大檩赢,是 Scrapy 選擇器的基石。實際上,CSS選擇器最終會被轉(zhuǎn)換成 XPath贞瞒。
盡管 XPath 表達式?jīng)]有 CSS 活躍偶房,但是它更有用,因為除了導(dǎo)航定位文件結(jié)構(gòu)军浆,它還能獲取文本內(nèi)容棕洋。
使用 XPath,你可以選擇這樣的內(nèi)容:選擇一個包含 "Next Page" 的鏈接乒融。
這使得 XPath 足以勝任爬蟲的任務(wù)掰盘,我們鼓勵你去學(xué)習(xí) XPath ,即使你已經(jīng)了解了如何構(gòu)建 CSS 選擇器赞季。
更多 XPath 使用方法可以查看 http://zvon.org/comp/r/tut-XPath_1.html 和 http://plasmasturm.org/log/xpath101/
提取 quotes 和 authors
現(xiàn)在你已經(jīng)知道了一點選擇和提取數(shù)據(jù)的方法愧捕,繼續(xù)完善代碼。
>>> for quote in response.css("div.quote"):
... text = quote.css("span.text::text").get()
... author = quote.css("small.author::text").get()
... tags = quote.css("div.tags a.tag::text").getall()
... print(dict(text=text, author=author, tags=tags))
{'tags': ['change', 'deep-thoughts', 'thinking', 'world'], 'author': 'Albert Einstein', 'text': '“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”'}
{'tags': ['abilities', 'choices'], 'author': 'J.K. Rowling', 'text': '“It is our choices, Harry, that show what we truly are, far more than our abilities.”'}
... a few more of these, omitted for brevity
>>>
使用 spider 提取數(shù)據(jù)
上面都是在 shell 中調(diào)試申钩,在返回 spider 中次绘。目前 Spider 沒有提取任何數(shù)據(jù),只是保存了整個 HTML 文件到本地撒遣。
接下來整合提取數(shù)據(jù)的邏輯到 spider 中邮偎。
一個 Scrapy spider 可以生成許多包含已提取數(shù)據(jù)的字典。
為做到這個目的愉舔,我們使用 yield 關(guān)鍵字來構(gòu)建生成器:
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').get(),
'author': quote.css('small.author::text').get(),
'tags': quote.css('div.tags a.tag::text').getall(),
}
對比之前的 spider 代碼钢猛,在這里主要發(fā)生變化的是 yield 子句。是一個調(diào)試的三個選擇器轩缤,我們獲取了每個 quote 的指定內(nèi)容。
存儲爬取的數(shù)據(jù)
最簡單的存儲方式是使用 'Feed exports'贩绕,命令如下:
scrapy crawl -o quotes.json
這會生成 quotes.json 文件火的,包含爬取的項目,以 JSON 序列化淑倾。
由于歷史原因馏鹤,Scrapy 使用附加的方式而非替代原文件內(nèi)容生成文件。也就是說娇哆,如果你執(zhí)行兩次存儲名命令湃累,會再添加內(nèi)容到文件中。由于 JSON 格式問題碍讨,存儲兩次治力,將破壞 JSON 的格式。
其他格式 JSON Lines
scrapy crawl -o quotes.jl
'JSON Lines' 是一種流類型結(jié)構(gòu)勃黍,可以輕易的添加新內(nèi)容到文件中宵统,而不用擔(dān)心執(zhí)行了兩次破壞文件格式。
因為每個記錄都以單獨的一行記錄覆获。
JL 當(dāng)可以搭配工具马澈,如 'JQ' 來輔助執(zhí)行這樣的命令瓢省。
項目管道 Item Pipeline
在小項目中,上面提到才這些內(nèi)容都足夠了痊班。然而勤婚,如果你想執(zhí)行更加復(fù)雜的爬取,你可以寫一個 'tiem pipleline'
「下一頁」鏈接
除了僅僅抓取第一頁和第二頁的內(nèi)容涤伐,你想要網(wǎng)頁中所有的內(nèi)容也可以蛔六。
在上面已經(jīng)了解到如何提取頁面上的鏈接,接下來看看如何獲取并進入下一頁鏈接
提取下一頁標(biāo)簽
首先觀察一下頁面中下一頁內(nèi)容
<ul class="pager">
<li class="next">
<a href="/page/2/">Next <span aria-hidden="true">→</span></a>
</li>
</ul>
需要提取的內(nèi)容是 href 的屬性值废亭,它告訴我們下一頁的相對地址
>>> response.css('li.next a::attr(href)').get()
'/page/2/'
或
>>> response.css('li.next a').attrib['href']
'/page/2'
那么如何遞歸所有的下一頁鏈接呢国章,spider 內(nèi)容可以這樣改動:
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
start_urls = [
'http://quotes.toscrape.com/page/1/',
]
def parse(self, response):
for quote in response.css('div.quote'):
yield {
'text': quote.css('span.text::text').get(),
'author': quote.css('small.author::text').get(),
'tags': quote.css('div.tags a.tag::text').getall(),
}
next_page = response.css('li.next a::attr(href)').get()
if next_page is not None:
next_page = response.urljoin(next_page)
yield scrapy.Request(next_page, callback=self.parse)
這里的關(guān)鍵在于:一、判斷是否有下一頁豆村,二液兽、如果有,如何拼接 URL 遞歸請求訪問和解析新頁面內(nèi)容掌动。
現(xiàn)在四啰,已經(jīng)提取了數(shù)據(jù),parse() 中拼接絕對路徑的 URL 使用了 urljoin() 方法粗恢,并且生成一個新的請求到下一頁柑晒;它將自身注冊作為回調(diào),以處理下一頁的數(shù)據(jù)眷射,并且持續(xù)爬取新頁面匙赞。
總結(jié)一下 Scrapy 的下一頁獲取機制:如果你在回調(diào)函數(shù)中生成了一個請求,Scrapy 會適配已發(fā)送的請求妖碉,并在請求完成注冊一個要執(zhí)行的回調(diào)方法涌庭。
基于這個機制,你可以構(gòu)建復(fù)雜的爬蟲自定義點擊超鏈接欧宜,也可以根據(jù)訪問的頁面不同自定義提取規(guī)則坐榆。
在這個例子中,我們創(chuàng)建了一個循環(huán)冗茸,一直進入下一頁界面直到最后一頁席镀。這個功能非常適合爬取博客、論壇或者擁有分頁的網(wǎng)站夏漱。
一個簡單的方式創(chuàng)建請求
我們可以使用 response.follow 來請求下一個頁面
...
next_page = response.css('li.next a::attr(href)').get()
if next_page is not None:
yield response.follow(next_page, callback=self.parse)
將上面例子的最后兩行改成 response.follow() 請求網(wǎng)站豪诲,不同于 scrapy.Request,response.follow 提供相對路徑請求麻蹋,這意味著你不需要再添加 urljoin 拼接 URL跛溉。
記住,response.follow 返回的還是請求實例,所以需要 yield 生成這個請求芳室。
由于 <a> 標(biāo)簽是一個鏈接专肪,所以 response.follow 自動使用 href 屬性,所以代碼還可以簡寫為:
for a in response.css('li.next a'):
yield response.follow(a, callback=self.parse)
更多的例子和模型
這里繼續(xù)將回調(diào)和更多操作的內(nèi)容堪侯,這次爬取作者信息
import scrapy
class AuthorSpider(scrapy.Spider):
name = 'author'
start_urls = ['http://quotes.toscrape.com/']
def parse(self, response):
# follow links to author pages
for href in response.css('.author + a::attr(href)'):
yield response.follow(href, self.parse_author)
# follow pagination links
for href in response.css('li.next a::attr(href)'):
yield response.follow(href, self.parse)
def parse_author(self, response):
def extract_with_css(query):
return response.css(query).get(default='').strip()
yield {
'name': extract_with_css('h3.author-title::text'),
'birthdate': extract_with_css('.author-born-date::text'),
'bio': extract_with_css('.author-description::text'),
}
這個 spider 開始于主頁面嚎尤,為所有的作家頁面調(diào)用 parse_author 回調(diào),分頁鏈接的 parse 回調(diào)伍宦。
parse_author 回調(diào)定義了一個方法從 CSS 查詢中提取和清理數(shù)據(jù)芽死,再產(chǎn)生作者資料的字典。
在這個 spider 中展示的另一個有趣的事情:即使有許多 quotes 對應(yīng)一個 author次洼,我們也不用擔(dān)心會多次訪問同一個 author 界面关贵。
默認情況下,Scrapy 過濾重復(fù)URL請求卖毁,避免因訪問服務(wù)器過多而產(chǎn)生問題揖曾。
可以在設(shè)置中配置參數(shù) DUPFILTER_CLASS
使用 spider 參數(shù)
提供命令行參數(shù)給 spider,添加 '-a' 選項即可:
scrapy crawl quotes -o quotes-humor.json -a tag=humor
這些參數(shù)會傳遞給 Spider 類的 '__init__' 方法亥啦,并作為 spider 實例的默認屬性炭剪。
在這個例子中,提供給 tag 屬性的值可以使用 self.tag 獲取翔脱。
添加 tag 選項后奴拦,spider 抓取的內(nèi)容被限制在特定的標(biāo)簽中:
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
def start_requests(self):
url = 'http://quotes.toscrape.com/'
tag = getattr(self, 'tag', None)
if tag is not None:
url = url + 'tag/' + tag
yield scrapy.Request(url, self.parse)
def parse(self, response):
for quote in response.css('div.quote'):
yield {
'text': quote.css('span.text::text').get(),
'author': quote.css('small.author::text').get(),
}
next_page = response.css('li.next a::attr(href)').get()
if next_page is not None:
yield response.follow(next_page, self.parse)
在這里,添加了 humor 標(biāo)簽/tag 的 spider 訪問的 URL 變成了 'http://quotes.toscrape.com/tag/humor'