在本教程中,咱們假設您已經安裝了Scrapy。若是沒有,請參閱安裝指南。css
咱們將要抓取 quotes.toscrape.com,一個列出著名做家的名言(quote)的網站。html
本教程將引導您完成如下任務:python
Scrapy 是用 Python 編寫的。若是你沒學過 Python,你可能須要瞭解一下這個語言,以充分利用 Scrapy。程序員
若是您已經熟悉其餘語言,並但願快速學習 Python,咱們建議您閱讀 Dive Into Python 3。或者,您能夠學習 Python 教程。正則表達式
若是您剛開始編程,並但願從 Python 開始,在線電子書《Learn Python The Hard Way》很是有用。您也能夠查看非程序員的 Python 資源列表。shell
在開始抓取以前,您必須建立一個新的 Scrapy 項目。 進入您要存儲代碼的目錄,而後運行:編程
scrapy startproject tutorial
這將建立一個包含如下內容的 tutorial 目錄:json
tutorial/ scrapy.cfg # 項目配置文件 tutorial/ # 項目的 Python 模塊,放置您的代碼的地方 __init__.py items.py # 項目項(item)定義文件 pipelines.py # 項目管道(piplines)文件 settings.py # 項目設置文件 spiders/ # 一個你之後會放置 spider 的目錄 __init__.py
Spider 是您定義的類,Scrapy 用它從網站(或一組網站)中抓取信息。 他們必須是 scrapy.Spider 的子類並定義初始請求,和如何獲取要繼續抓取的頁面的連接,以及如何解析下載的頁面來提取數據。api
這是咱們第一個爬蟲的代碼。 將其保存在項目中的 tutorial/spiders 目錄下的名爲 quotes_spider.py 的文件中:瀏覽器
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 的子類並定義了一些屬性和方法:
parse() 方法一般解析響應,將抓取的數據提取爲字典,而且還能夠查找新的 URL 來跟蹤並從中建立新的請求(Request)。
要使咱們的爬蟲工做,請進入項目的根目錄並運行:
scrapy crawl quotes
這個命令運行咱們剛剛添加的名稱爲 quotes 的爬蟲,它將向 quotes.toscrape.com 發送一些請求。 你將獲得相似於這樣的輸出:
... (omitted for brevity) 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) ...
如今,查看當前目錄下的文件。 您會發現已經建立了兩個新文件:quotes-1.html 和 quotes-2.html,其中包含各個URL的內容,就像咱們的 parse 方法指示同樣。
注意
若是您想知道爲何咱們尚未解析 HTML,請繼續,咱們將盡快介紹。
Spider 的 start_requests 方法返回 scrapy.Request 對象,Scrapy 對其發起請求 。而後將收到的響應實例化爲 Response 對象,以響應爲參數調用請求對象中定義的回調方法(在這裏爲 parse 方法)。
用於代替實現一個從 URL 生成 scrapy.Request 對象的 start_requests() 方法,您能夠用 URL 列表定義一個 start_urls 類屬性。 此列表將默認替代 start_requests() 方法爲您的爬蟲建立初始請求:
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)
Scrapy 將調用 parse() 方法來處理每一個 URL 的請求,即便咱們沒有明確告訴 Scrapy 這樣作。 這是由於 parse() 是 Scrapy 的默認回調方法,沒有明確分配回調方法的請求默認調用此方法。
學習如何使用 Scrapy 提取數據的最佳方式是在 Scrapy shell 中嘗試一下選擇器。 運行:
scrapy shell 'http://quotes.toscrape.com/page/1/'
注意
在從命令行運行 Scrapy shell 時必須給 url 加上引號,不然包含參數(例如 &符號)的 url 將不起做用。
在Windows上,要使用雙引號:
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 >>>
使用 shell,您能夠嘗試使用 CSS 選擇器選擇元素:
>>> response.css('title') [<Selector xpath='descendant-or-self::title' data='<title>Quotes to Scrape</title>'>]
運行 response.css('title') 返回的結果是一個 SelectorList 類列表對象,它是一個指向 XML/HTML 元素的 Selector 對象的列表,容許您進行進一步的查詢來細分選擇或提取數據。
要從上面的 title 中提取文本,您能夠執行如下操做:
>>> response.css('title::text').extract() ['Quotes to Scrape']
這裏有兩件事情要注意:一個是咱們在 CSS 查詢中添加了 ::text,這意味着咱們只想要 <title> 元素中的文本。 若是咱們不指定 ::text,咱們將獲得完整的 title 元素,包括其標籤:
>>> response.css('title').extract() ['<title>Quotes to Scrape</title>']
另外一件事是調用 .extract() 返回的結果是一個列表,由於咱們在處理 SelectorList。 當你明確你只是想要第一個結果時,你能夠這樣作:
>>> response.css('title::text').extract_first() 'Quotes to Scrape'
或者你能夠這樣寫:
>>> response.css('title::text')[0].extract() 'Quotes to Scrape'
可是,若是沒有找到匹配選擇的元素,.extract_first() 返回 None,避免了 IndexError
這裏有一個教訓:對於大多數爬蟲代碼,您但願它具備容錯性,若是在頁面上找不到指定的元素致使沒法獲取某些項,至少其它的數據能夠被抓取。
除了 extract() 和 extract_first() 方法以外,還可使用 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']
爲了獲得正確的 CSS 選擇器語句,您能夠在瀏覽器中打開頁面並查看源代碼。 您也可使用瀏覽器的開發工具或擴展(如 Firebug)(請參閱有關 Using Firebug for scraping 和 Using Firefox for scraping 的部分)。
Selector Gadget 也是一個很好的工具,能夠快速找到元素的 CSS 選擇器語句,它能夠在許多瀏覽器中運行。
除了 CSS,Scrapy 選擇器還支持使用 XPath 表達式:
>>> response.xpath('//title') [<Selector xpath='//title' data='<title>Quotes to Scrape</title>'>] >>> response.xpath('//title/text()').extract_first() 'Quotes to Scrape'
XPath 表達式很是強大,是 Scrapy 選擇器的基礎。 實際上,若是你查看相關的源代碼就能夠發現,CSS 選擇器被轉換爲 XPath。
雖然也許不像 CSS 選擇器那麼受歡迎,但 XPath 表達式提供更多的功能,由於除了導航結構以外,它還能夠查看內容。 使用 XPath,您能夠選擇如下內容:包含文本「下一頁」的連接。 這使得 XPath 很是適合抓取任務,咱們鼓勵您學習 XPath,即便您已經知道如何使用 CSS 選擇器,這會使抓取更容易。
咱們不會在這裏講太多關於 XPath 的內容,但您能夠閱讀 using XPath with Scrapy Selectors 獲取更多有關 XPath 的信息。 咱們推薦教程 to learn XPath through examples,和教程 「how to think in XPath」。
如今你知道了如何選擇和提取,讓咱們來完成咱們的爬蟲,編寫代碼從網頁中提取名言(quote)。
http://quotes.toscrape.com 中的每一個名言都由 HTML 元素表示,以下所示:
<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'
獲得 quote 元素的 selector 列表:
>>> response.css("div.quote")
經過上述查詢返回的每一個 selector 容許咱們對其子元素運行進一步的查詢。 讓咱們將第一個 selector 分配給一個變量,以便咱們能夠直接在特定的 quote 上運行咱們的 CSS 選擇器:
>>> quote = response.css("div.quote")[0]
如今,咱們使用剛剛建立的 quote 對象,從該 quote 中提取 title,author 和 tags:
>>> 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 = quote.css("small.author::text").extract_first() >>> author 'Albert Einstein'
鑑於標籤是字符串列表,咱們可使用 .extract() 方法將它們所有提取出來:
>>> tags = quote.css("div.tags a.tag::text").extract() >>> tags ['change', 'deep-thoughts', 'thinking', 'world']
如今已經弄清楚瞭如何提取每個信息,接下來遍歷全部 quote 元素,並把它們放在一個 Python 字典中:
>>> 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 >>>
讓咱們回到咱們的爬蟲上。 到目前爲止,它並無提取任何數據,只將整個 HTML 頁面保存到本地文件。 讓咱們將上述提取邏輯整合到咱們的爬蟲中。
Scrapy 爬蟲一般生成許多包含提取到的數據的字典。 爲此,咱們在回調方法中使用 yield Python 關鍵字,以下所示:
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(), }
若是您運行此爬蟲,它將輸出提取的數據與日誌:
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.」"}
存儲抓取數據的最簡單的方法是使用 Feed exports,使用如下命令:
scrapy crawl quotes -o quotes.json
這將生成一個 quotes.json 文件,其中包含全部抓取到的 JSON 序列化的數據。
因爲歷史緣由,Scrapy 追加內容到給定的文件,而不是覆蓋其內容。 若是您在第二次以前刪除該文件兩次運行此命令,那麼最終會出現一個破壞的 JSON 文件。您還可使用其餘格式,如 JSON 行(JSON Lines):
scrapy crawl quotes -o quotes.jl
JSON 行格式頗有用,由於它像流同樣,您能夠輕鬆地將新記錄附加到文件。 當運行兩次時,它不會發生 JSON 那樣的問題。 另外,因爲每條記錄都是單獨的行,因此您在處理大文件時無需將全部內容放到內存中,還有 JQ 等工具能夠幫助您在命令行中執行此操做。
在小項目(如本教程中的一個)中,這應該是足夠的。 可是,若是要使用已抓取的項目執行更復雜的操做,則能夠編寫項目管道(Item Pipeline)。 在工程的建立過程當中已經爲您建立了項目管道的佔位符文件 tutorial/pipelines.py, 雖然您只須要存儲已抓取的項目,不須要任何項目管道。
或許你但願獲取網站全部頁面的 quotes,而不是從 http://quotes.toscrape.com 的前兩頁抓取。
如今您已經知道如何從頁面中提取數據,咱們來看看如何跟蹤連接。
首先是提取咱們想要跟蹤的頁面的連接。 檢查咱們的頁面,咱們能夠看到連接到下一個頁面的URL在下面的元素中:
<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>'
這獲得了超連接元素,可是咱們須要其屬性 href。 爲此,Scrapy 支持 CSS 擴展,您能夠選擇屬性內容,以下所示:
>>> response.css('li.next a::attr(href)').extract_first() '/page/2/'
如今修改咱們的爬蟲,改成遞歸地跟蹤下一頁的連接,從中提取數據:
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)
如今,在提取數據以後,parse() 方法查找到下一頁的連接,使用 urljoin() 方法構建一個完整的絕對 URL(由於連接能夠是相對的),並生成(yield)一個到下一頁的新的請求, 其中包括回調方法(parse)。
您在這裏看到的是 Scrapy 的連接跟蹤機制:當您在一個回調方法中生成(yield)請求(request)時,Scrapy 將安排發起該請求,並註冊該請求完成時執行的回調方法。
使用它,您能夠根據您定義的規則構建複雜的跟蹤連接機制,並根據訪問頁面提取不一樣類型的數據。
在咱們的示例中,它建立一個循環,跟蹤全部到下一頁的連接,直到它找不到要抓取的博客,論壇或其餘站點分頁。
做爲建立請求對象的快捷方式,您可使用 response.follow:
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)
不像 scrapy.Request,response.follow 支持相對 URL - 不須要調用urljoin。請注意,response.follow 只是返回一個 Request 實例,您仍然須要生成請求(yield request)。
您也能夠將選擇器傳遞給 response.follow,該選擇器應該提取必要的屬性:
for href in response.css('li.next a::attr(href)'): yield response.follow(href, callback=self.parse)
對於<a>元素,有一個快捷方式:response.follow 自動使用它們的 href 屬性。 因此代碼能夠進一步縮短:
for a in response.css('li.next a'): yield response.follow(a, callback=self.parse)
注意
response.follow(response.css('li.next a')) 無效,由於 response.css 返回的是一個相似列表的對象,其中包含全部結果的選擇器,而不是單個選擇器。for 循環或者 response.follow(response.css('li.next a')[0]) 則能夠正常工做。
這是另一個爬蟲,示例了回調和跟蹤連接,此次是爲了抓取做者信息:
import scrapy class AuthorSpider(scrapy.Spider): name = 'author' start_urls = ['http://quotes.toscrape.com/'] def parse(self, response): # 連接到做者頁面 for href in response.css('.author + a::attr(href)'): yield response.follow(href, self.parse_author) # 連接到下一頁 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_author 回調方法跟蹤全部到做者頁面的連接,以 parse 回調方法跟蹤其它頁面。
這裏咱們將回調方法做爲參數直接傳遞給 response.follow,這樣代碼更短,也能夠傳遞給 scrapy.Request。
parse_author 回調方法裏定義了另一個函數來根據 CSS 查詢語句(query)來提取數據,而後生成包含做者數據的 Python 字典。
這個爬蟲演示的另外一個有趣的事是,即便同一做者有許多名言,咱們也不用擔憂屢次訪問同一做者的頁面。默認狀況下,Scrapy 會將重複的請求過濾出來,避免了因爲編程錯誤而致使的重複服務器的問題。能夠經過 DUPEFILTER_CLASS 進行相關的設置。
但願如今您已經瞭解了 Scrapy 的跟蹤連接和回調方法機制。
CrawlSpider 類是一個小規模的通用爬蟲引擎,只須要修改其跟蹤連接的機制等,就能夠在它之上實現你本身的爬蟲程序。
另外,一個常見的模式是從多個頁面據構建一個包含數據的項(item),有一個將附加數據傳遞給回調方法的技巧。
在運行爬蟲時,能夠經過 -a 選項爲您的爬蟲提供命令行參數:
scrapy crawl quotes -o quotes-humor.json -a tag=humor
默認狀況下,這些參數將傳遞給 Spider 的 __init__ 方法併成爲爬蟲的屬性。
在此示例中,經過 self.tag 獲取命令行中參數 tag 的值。您能夠根據命令行參數構建 URL,使您的爬蟲只爬取特色標籤的名言:
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)
若是您將 tag = humor 傳遞給爬蟲,您會注意到它只會訪問標籤爲 humor 的 URL,例如 http://quotes.toscrape.com/tag/humor。您能夠在這裏瞭解更多關於爬蟲參數的信息。
本教程僅涵蓋了 Scrapy 的基礎知識,還有不少其餘功能未在此說起。 查看初窺 Scrapy 中的「還有什麼?」部分能夠快速瞭解有哪些重要的內容。
您能夠經過目錄瞭解更多有關命令行工具、爬蟲、選擇器以及本教程未涵蓋的其餘內容的信息。下一章是示例項目。