崔老哥python scrapy爬蟲框架入門

Scrapy 爬蟲框架入門案例詳解

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_domainsstart_urls,另外還有一個方法parse

  • name,每個項目里名字是唯一的,用來區分不同的Spider。
  • allowed_domains允許爬取的域名,如果初始或後續的請求鏈接不是這個域名下的,就會被過濾掉。
  • start_urls,包含了Spider在啓動時爬取的url列表,初始請求是由它來定義的。
  • parse,是Spider的一個方法,默認情況下,被調用時start_urls裏面的鏈接構成的請求完成下載後,返回的response就會作爲唯一的參數傳遞給這個函數,該方法負責解析返回的response,提取數據或者進一步生成要處理的請求。

創建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:pass@ftp.example.com/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,這是一個類方法,用@classmethod標識,是一種依賴注入的方式,方法的參數就是crawler,通過crawler這個我們可以拿到全局配置的每個配置信息,在全局配置settings.py中我們可以定義MONGO_URIMONGO_DB來指定MongoDB連接需要的地址和數據庫名稱,拿到配置信息之後返回類對象即可。所以這個方法的定義主要是用來獲取settings.py中的配置的。
  • open_spider,當spider被開啓時,這個方法被調用。在這裏主要進行了一些初始化操作。
  • close_spider,當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

相關推薦

騰訊雲主機Python3環境安裝Scrapy爬蟲框架過程及常見錯誤

利用Scrapy爬取所有知乎用戶詳細信息並存至MongoDB


轉自:https://cloud.tencent.com/developer/article/1004914

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章