前言
筆者打算寫一系列的文章,記錄自己在學習並使用 Scrapy 的點滴;作者打算使用 python 3.6 作爲 Scrapy 的基礎運行環境;
本文爲作者的原創作品,轉載需註明出處;
備註:本文轉載自本人的博客,傷神的博客:http://www.shangyang.me/2017/06/29/scrapy-learning-1-tutorial/
Scrapy 安裝
我本地安裝有兩個版本的 python, 2.7 和 3.6;而正如前言所描述的那樣,筆者打算使用 Python 3.6 的環境來搭建 Scrapy;
$ pip install Scrapy
默認安裝的支持 Python 2.7 版本的 Scrapy;
$ pip3 install Scrap
安裝的是支持 python 3.x 版本的 Scrapy;不過安裝過程中,遇到了些問題,<font color='red'>HTTPSConnectionPool(host='pypi.python.org', port=443): Read timed out.</font>解決辦法是,在安裝的過程中,延長超時的時間,
$ pip3 install -U --timeout 1000 Scrapy
Scrapy Tutorial
創建 tutorial 項目
使用
$ scrapy startproject tutorial
New Scrapy project 'tutorial', using template directory '/usr/local/lib/python2.7/site-packages/scrapy/templates/project', created in:
/Users/mac/workspace/scrapy/tutorial
可見默認使用的 python 2.7,但是如果需要創建一個支持 python 3.x 版本的 tutoiral 項目呢?如下所示,使用 python3 -m
$ python3 -m scrapy startproject tutorial
New Scrapy project 'tutorial', using template directory '/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/scrapy/templates/project', created in:
/Users/mac/workspace/scrapy/tutorial
導入 PyCharm
直接 open 項目工程 /Users/mac/workspace/scrapy/tutorial;這裏需要注意的是默認的 PyCharm 使用的解釋器 Interpretor 是我本地的 Python 2.7;這裏需要將解釋器改爲 Python 3.6;下面記錄下修改的步驟,
- 點擊左上角 PyCharm Community Edition,進入 Preferences
- 點擊 Project:tutorial,然後選擇 Project Interpreter,然後設置解釋器的版本,如下
工程結構
通過命令構建出來的項目骨架如圖所示
第一個 Spider
我們來新建一個 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)
可以看到,我們新建的 QuotesSpider 類是繼承自 scrapy.Spider 類的;下面看看其屬性和方法的意義,
- name
是 Spider 的標識符,用於唯一標識該 Spider;它必須在整個項目中是全局唯一的; - start_requests()
必須定義並返回一組可以被 Spider 爬取的 Requests,Request 對象由一個 URL 和一個回調函數構成; - parse()
就是 Request 對象中的回調方法,用來解析每一個 Request 之後的 Response;所以,parse() 方法就是用來解析返回的內容,通過解析得到的 URL 同樣可以創建對應的 Requests 進而繼續爬取;
再來看看具體的實現,
- start_request(self) 方法分別針對 http://quotes.toscrape.com/page/1/ 和 http://quotes.toscrape.com/page/2/ 創建了兩個需要被爬取的 Requests 對象;並通過 yield 進行迭代返回;備註,yield 是迭代生成器,是一個 Generator;
- parse(self, response) 方法既是對 Request 的反饋的內容 Response 進行解析,這裏的解析的邏輯很簡單,就是分別創建兩個本地文件,然後將 response.body 的內容放入這兩個文件當中。
如何執行
執行的過程需要使用到命令行,注意,這裏需要使用到scrapy
命令來執行;
$ cd /Users/mac/workspace/scrapy/tutorial
$ python3 -m scrapy crawl quotes
大致會輸出如下內容
...
2016-12-16 21:24:05 [scrapy.core.engine] INFO: Spider opened
2016-12-16 21:24:05 [scrapy.extensions.logstats] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2016-12-16 21:24:05 [scrapy.extensions.telnet] DEBUG: Telnet console listening on 127.0.0.1:6023
2016-12-16 21:24:05 [scrapy.core.engine] DEBUG: Crawled (404) <GET http://quotes.toscrape.com/robots.txt> (referer: None)
2016-12-16 21:24:05 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://quotes.toscrape.com/page/1/> (referer: None)
2016-12-16 21:24:05 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://quotes.toscrape.com/page/2/> (referer: None)
2016-12-16 21:24:05 [quotes] DEBUG: Saved file quotes-1.html
2016-12-16 21:24:05 [quotes] DEBUG: Saved file quotes-2.html
2016-12-16 21:24:05 [scrapy.core.engine] INFO: Closing spider (finished)
...
可以看到,通過爬取,我們在本地生成了兩個 html 文件 quotes-1.html 和 quotes-2.html
如何提取
通過命令行的方式提取
Scrapy 提供了命令行的方式可以對需要被爬取的內容進行高效的調試,通過使用Scrapy shell
進入命令行,然後在命令行中可以快速的對要爬取的內容進行提取;
如何進入 Scrapy shell 環境
我們試着通過 Scrapy shell 來提取下 "http://quotes.toscrape.com/page/1/" 中的數據,通過執行如下命令,進入 shell
$ scrapy shell "http://quotes.toscrape.com/page/1/"
輸出
[ ... Scrapy log here ... ]
2016-09-19 12:09:27 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://quotes.toscrape.com/page/1/> (referer: None)
[s] Available Scrapy objects:
[s] scrapy scrapy module (contains scrapy.Request, scrapy.Selector, etc)
[s] crawler <scrapy.crawler.Crawler object at 0x7fa91d888c90>
[s] item {}
[s] request <GET http://quotes.toscrape.com/page/1/>
[s] response <200 http://quotes.toscrape.com/page/1/>
[s] settings <scrapy.settings.Settings object at 0x7fa91d888c10>
[s] spider <DefaultSpider 'default' at 0x7fa91c8af990>
[s] Useful shortcuts:
[s] shelp() Shell help (print this help)
[s] fetch(req_or_url) Fetch request (or URL) and update local objects
[s] view(response) View response in a browser
>>>
這樣,我們就進入了 Scrapy shell 的環境,上面顯示了連接請求和返回的相關信息,response 返回 status code 200 表示成功返回;
通過 CSS 標準進行提取
這裏主要是遵循 CSS 標準 https://www.w3.org/TR/selectors/ 來對網頁的元素進行提取,
-
通過使用 css() 選擇我們要提取的元素;下面演示一下如何提取元素 <title/>
>>> response.css('title') [<Selector xpath=u'descendant-or-self::title' data=u'<title>Quotes to Scrape</title>'>]
可以看到,它通過返回一個類似 SelectorList 的對象成功的獲取到了 http://quotes.toscrape.com/page/1/ 頁面中的 <title/> 的信息,該信息是封裝在
Selector
對象中的 data 屬性中的; -
提取
Selector
元素的文本內容,一般有兩種方式用來提取,-
通過使用 extract() 或者 extract_first() 方法來提取元素的內容;下面演示如何提取 #1 返回的元素 <title/> 中的文本內容 text;
>>> response.css('title::text').extract_first() 'Quotes to Scrape'
extract_first() 表示提取返回隊列中的第一個
Selector
對象;同樣也可以使用如下的方式,>>> response.css('title::text')[0].extract() 'Quotes to Scrape'
不過 extract_first() 方法可以在當頁面沒有找到的情況下,避免出現
IndexError
的錯誤; - 通過 re() 方法來使用正則表達式的方式來進行提取元素的文本內容
>>> response.css('title::text').re(r'Quotes.*') ['Quotes to Scrape'] >>> response.css('title::text').re(r'Q\w+') ['Quotes'] >>> response.css('title::text').re(r'(\w+) to (\w+)') ['Quotes', 'Scrape']
備註,最後一個正則表示式返回了兩個匹配的 Group;
-
使用 XPath
除了使用 CSS 標準來提取元素意外,我們還可以使用 XPath 標準來提取元素,比如,
>>> response.xpath('//title')
[<Selector xpath='//title' data='<title>Quotes to Scrape</title>'>]
>>> response.xpath('//title/text()').extract_first()
'Quotes to Scrape'
XPath 比 CSS 的爬取方式更爲強大,因爲它不僅僅是根據 HTML 的結構元素去進行檢索(Navigating),並且它可以順帶的對文本(text)進行檢索;所以它可以支持 CSS 標準不能做到的場景,比如,檢索一個 包含文本內容"Next Page"的 link 元素;這就使得通過 XPath 去構建爬蟲更爲簡單;
提取 quotes 和 authors
下面我們將來演示如何提取 http://quotes.toscrape.com 首頁中的內容,先來看看首頁的結構
<img src="content-of-quotes-toscrape-com.png" width="65%" height="65%" />
可以看到,裏面每個段落包含了一個名人的一段語錄,那麼我們如何來提取所有的相關信息呢?
我們從提取第一個名人的信息入手,看看如何提取第一個名人的名言信息;可以看到,第一個名人的語句是愛因斯坦的,那麼我們試着來提取名言
、作者
以及相關的tags
;
<div class="quote">
<span class="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">Albert Einstein</small>
<a href="/author/Albert-Einstein">(about)</a>
</span>
<div class="tags">
Tags:
<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>
下面我們就來試着一步一步的去提取相關的信息,
首先,進入 Scrapy Shell,
$ scrapy shell 'http://quotes.toscrape.com'
然後,獲取 <div class="quote" />
元素列表
>>> response.css("div.quote")
這裏會返回一系列的相關的 Selectors,不過因爲這裏我們僅僅是對第一個名言進行解析,所以我們只取第一個元素,並將其保存在 quote 變量中
>>> quote = response.css("div.quote")[0]
然後,我們來分別提取title
、author
和tags
- 提取
title
>>> title = quote.css("span.text::text").extract_first() >>> title '“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”'
- 提取
author
>>> author = quote.css("small.author::text").extract_first() >>> author 'Albert Einstein'
- 提取
tags
,這裏需要注意的是,tags 是一系列的文本,>>> tags = quote.css("div.tags a.tag::text").extract() >>> tags ['change', 'deep-thoughts', 'thinking', 'world']
Ok,上述完成了針對其中一個名言信息的提取,那麼,我們如何提取完所有名人的名言信息呢?
>>> for quote in response.css("div.quote"):
... text = quote.css("span.text::text").extract_first()
... author = quote.css("small.author::text").extract_first()
... tags = quote.css("div.tags a.tag::text").extract()
... 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
寫個循環,將所有的信息的信息放入 Python dictionary;
通過 Python 程序來進行提取
本小計繼續沿用提取 quotes 和 authors 小節的例子,來看看如何通過 python 程序來做相同的爬取動作;
提取數據
修改該之前的 quotes_spider.py 內容,如下,
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').extract_first(),
'author': quote.css('small.author::text').extract_first(),
'tags': quote.css('div.tags a.tag::text').extract(),
}
執行上述的名爲 quotes
的爬蟲,
$ scrapy crawl quotes
執行結果如下,
2016-09-19 18:57:19 [scrapy.core.scraper] DEBUG: Scraped from <200 http://quotes.toscrape.com/page/1/>
{'tags': ['life', 'love'], 'author': 'André Gide', 'text': '“It is better to be hated for what you are than to be loved for what you are not.”'}
2016-09-19 18:57:19 [scrapy.core.scraper] DEBUG: Scraped from <200 http://quotes.toscrape.com/page/1/>
{'tags': ['edison', 'failure', 'inspirational', 'paraphrased'], 'author': 'Thomas A. Edison', 'text': "“I have not failed. I've just found 10,000 ways that won't work.”"}
可以看到,我們通過 python 創建的爬蟲 quotes
一條一條的返回了爬取的信息;
保存數據
最簡單保存方式被爬取的數據是通過使用 Feed exports,通過使用如下的命令,
使用 JSON 格式
$ scrapy crawl quotes -o quotes.json
上述命令會生成一個文件quotes.json
,該文件中包含了所有被爬取的數據;不過由於歷史的原因,Scrapy 是往一個文件中追加被爬取的信息,而不是覆蓋更新,所以如果你執行上述命令兩次,將會得到一個損壞了的 json 文件;
使用 JSON Lines 格式
$ scrapy crawl quotes -o quotes.jl
這樣,保存的文件就是 JSON Lines 的格式了,注意,這裏的唯一變化是文件的後綴名改爲了.jl
;
補充,JSON Lines 是另一種 JSON 格式的定義,基本設計是每行是一個有效的 JSON Value;比如它的格式比 CSV 格式更友好,
["Name", "Session", "Score", "Completed"]
["Gilbert", "2013", 24, true]
["Alexa", "2013", 29, true]
["May", "2012B", 14, false]
["Deloise", "2012A", 19, true]
同時也可以支持內嵌數據,
{"name": "Gilbert", "wins": [["straight", "7♣"], ["one pair", "10♥"]]}
{"name": "Alexa", "wins": [["two pair", "4♠"], ["two pair", "9♠"]]}
{"name": "May", "wins": []}
{"name": "Deloise", "wins": [["three of a kind", "5♣"]]}
JSON Lines 格式非常適合處理含有大量數據的文件,通過迭代,每行處理一個數據對象;不過,要注意
的是,使用 JSON lines 的方式,Scrapy 同樣的是以追加
的方式添加內容,只是因爲 JSON Lines 逐行的方式添加被爬取的數據,所以以追加的方式並不會想使用 JSON 格式那樣導致文件格式錯誤;
如果是一個小型的項目,使用 JSON Lines 的方式就足夠了;但是,如果你面臨的是一個更復雜的項目,而且有更復雜的數據需要爬取,那麼你就可以使用 Item Pipeline;一個 demo Pipelines 已經幫你創建好了,tutorial/pipelines.py
;
提取下一頁(提取鏈接信息)
如何提取章節詳細的描述瞭如何爬取頁面的信息,那麼,如何爬取該網站的所有信息呢?那麼就必須爬取相關的鏈接信息;那麼我們依然以 http://quotes.toscrape.com 爲例,來看看我們該如何爬取鏈接信息,
我們可以看到,下一頁的鏈接 HTML 元素,
<ul class="pager">
<li class="next">
<a href="/page/2/">Next <span aria-hidden="true">→</span></a>
</li>
</ul>
我們可以通過 shell 來抓取它,
>>> response.css('li.next a').extract_first()
'<a href="/page/2/">Next <span aria-hidden="true">→</span></a>'
這樣,我們得到了這個anchor
元素,但是我們想要得到的是其href
屬性;Scrapy 支持 CSS 擴展的方式,因此我們可以直接爬取其屬性值,
>>> response.css('li.next a::attr(href)').extract_first()
'/page/2/'
好的,我們現在已經知道該如何獲取下一頁鏈接的相對地址了,那麼我們如何修改我們的 python 程序使得我們可以自動的爬取所有頁面的數據呢?
使用 scrapy.Request
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').extract_first(),
'author': quote.css('small.author::text').extract_first(),
'tags': quote.css('div.tags a.tag::text').extract(),
}
next_page = response.css('li.next a::attr(href)').extract_first()
if next_page is not None:
next_page = response.urljoin(next_page)
yield scrapy.Request(next_page, callback=self.parse)
這裏簡單的描述下程序的執行邏輯,通過 for 循環處理完當前頁面的爬取操作,然後執行獲取下一頁的相關操作,首先獲得下一頁的_相對路徑_並保存到變量 next_page 中,然後通過 response.urljon(next_page) 方法得到絕對路徑;最後,通過該絕對路徑再生成一個 scrapy.Request 對象返回,並加入 爬蟲隊列中,等待下一次的爬取;由此,你就可以動態的去爬取所有相關頁面的信息了;
基於此,你就可以建立起非常複雜的爬蟲了,同樣,可以根據不同鏈接的類型,構建不同的 Parser,那麼就可以對不同類型的返回頁面進行分別處理;
使用 response.follow
不同於使用 scrapy Request,需要通過相對路徑構造出絕對路徑,response.follow 可以直接使用相對路徑,因此就不需要調用 urljoin 方法了;注意,response.follow 直接返回一個 Request 實例,可以直接通過 yield 進行返回;所以,上述代碼可以簡化爲
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').extract_first(),
'author': quote.css('span small::text').extract_first(),
'tags': quote.css('div.tags a.tag::text').extract(),
}
next_page = response.css('li.next a::attr(href)').extract_first()
if next_page is not None:
yield response.follow(next_page, callback=self.parse)
另外,response.follow 在處理 <a>
元素的時候,會直接使用它們的 href 屬性;所以上述代碼還可以簡化爲,
next_page = response.css('li.next a').extract_first()
if next_page is not None:
yield response.follow(next_page, callback=self.parse)
因此匹配的時候不需要顯示的聲明 <a> 的屬性值了;
定義更多的 Parser
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).extract_first().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'),
}
該例子創建了兩個解析方法 parse() 和 parse_author(),一個是用來控制整個爬取流程,一個是用來解析 author 信息的;首先,我們來分析一下執行的流程,
-
進入 parse(),從當前的頁面中爬取得到所有相關的 author href 屬性值既是一個鏈接,然後針對該鏈接,通過 response.follow 創建一個新的 Request 繼續進行爬取,通過回調 parse_author() 方法對爬取的內容進行進一步的解析,這裏就是對爬取到的 Author 的信息進行提取;
- 當 #1 有關當前頁面所有的 Author 信息都已經爬取成功以後,便開始對下一頁進行爬取;
從這個例子中,我們需要注意的是,當爬取當前頁面的時候,我們依然可以通過創建子的 Requests 對子鏈接進行爬取直到所有有關當前頁面的信息都已經被爬取完畢以後,方可進入下一個頁面繼續進行爬取;
另外,需要注意的是,在爬取整個網站信息的時候,必然會有多個相同 Author 的名言,那麼勢必要爬取到許多的重複的 Author 的信息;這無疑是增加了爬取的壓力同時也需要處理大量的冗餘數據,基於此,Scrapy 默認實現了對重複的
已經爬取過的鏈接在下次爬取的時候自動過濾掉
了;不過,你也可以通過 DUPEFILTER_CLASS
來進行設置是否啓用該默認行爲;
使用 Spider 參數
你可以通過 commond line 的方式爲你的 Spider 提供參數,
$ scrapy crawl quotes -o quotes-humor.json -a tag=humor
該參數將會被傳入 Spider 的 __init__ 方法中,並默認成爲當前 Spider quotes 的屬性;在 quotes Spider 的 python 應用程序中,可以通過使用 self.tag 來獲取該參數信息;
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').extract_first(),
'author': quote.css('small.author::text').extract_first(),
}
next_page = response.css('li.next a::attr(href)').extract_first()
if next_page is not None:
yield response.follow(next_page, self.parse)
通過 getattr(self, 'tag', None)
便可以獲取從 common line 中傳入的 tag 參數,並構造出需要爬取的 URL 鏈接 http://quotes.toscrape.com/tag/humor
Reference
Scrapy 爬蟲框架:http://python.jobbole.com/86405/
Installation Guide: https://doc.scrapy.org/en/latest/intro/install.html
explain how virtualenv used: https://stackoverflow.com/questions/41151141/how-to-get-scrapy-to-use-python-3-when-both-python-versions-are-installed
tutorial guide: https://doc.scrapy.org/en/latest/intro/tutorial.html
Scrapy Clusters: http://scrapy-cluster.readthedocs.io/en/latest/
Scrapy Deployment: https://scrapyd.readthedocs.io/en/latest/deploy.html
Scrapy 0.24 文檔:http://scrapy-chs.readthedocs.io/zh_CN/0.24/index.html