Scrapy 1.4 文檔 03 Scrapy 教程

在本教程中,咱們假設您已經安裝了Scrapy。若是沒有,請參閱安裝指南css

咱們將要抓取 quotes.toscrape.com,一個列出著名做家的名言(quote)的網站。html

本教程將引導您完成如下任務:python

  1. 建立一個新的 Scrapy 項目
  2. 編寫一個爬蟲來爬取站點並提取數據
  3. 使用命令行導出抓取的數據
  4. 改寫爬蟲以遞歸地跟蹤連接
  5. 使用爬蟲參數

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 的子類並定義了一些屬性和方法:

  • name:用於識別 Spider。 它在項目中必須是惟一的,也就是說,您不能爲不一樣的 Spider 設置相同的名稱。
  • start_requests():必須返回一個 Requests 的迭代(您能夠返回一個 requests 列表或者寫一個生成器函數),Spider 將從這裏開始抓取。 隨後的請求將從這些初始請求連續生成。
  • parse():用來處理每一個請求獲得的響應的方法。 響應參數是 TextResponse 的一個實例,它保存頁面內容,而且還有其餘有用的方法來處理它。

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 方法)。

start_requests 方法的快捷方式

用於代替實現一個從 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 scrapingUsing Firefox for scraping 的部分)。

Selector Gadget 也是一個很好的工具,能夠快速找到元素的 CSS 選擇器語句,它能夠在許多瀏覽器中運行。

XPath:簡要介紹

除了 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 中提取 titleauthortags

>>> 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">&rarr;</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 中的「還有什麼?」部分能夠快速瞭解有哪些重要的內容。

您能夠經過目錄瞭解更多有關命令行工具、爬蟲、選擇器以及本教程未涵蓋的其餘內容的信息。下一章是示例項目。

相關文章
相關標籤/搜索