App下載

干貨! 一文教會(huì)你 scrapy 爬蟲框架的基本使用

猿友 2021-02-03 19:04:40 瀏覽數(shù) (3596)
反饋

一、scrapy爬蟲框架介紹

在編寫爬蟲的時(shí)候,如果我們使用 requests、aiohttp 等庫(kù),需要從頭至尾把爬蟲完整地實(shí)現(xiàn)一遍,比如說(shuō)異常處理、爬取調(diào)度等,如果寫的多了,的確會(huì)比較麻煩。利用現(xiàn)有的爬蟲框架,可以提高編寫爬蟲的效率,而說(shuō)到 Python 的爬蟲框架,Scrapy 當(dāng)之無(wú)愧是最流行最強(qiáng)大的爬蟲框架了。

scrapy介紹

Scrapy 是一個(gè)基于 Twisted 的異步處理框架,是純 Python 實(shí)現(xiàn)的爬蟲框架,其架構(gòu)清晰,模塊之間的耦合程度低,可擴(kuò)展性極強(qiáng),可以靈活完成各種需求。我們只需要定制開發(fā)幾個(gè)模塊就可以輕松實(shí)現(xiàn)一個(gè)爬蟲。

scrapy爬蟲框架的架構(gòu)如下圖所示:

scrapy爬蟲框架的架構(gòu)

它有如下幾個(gè)部分:

  • Scrapy Engine(引擎):用來(lái)處理整個(gè)系統(tǒng)的數(shù)據(jù)流處理、觸發(fā)事務(wù),是整個(gè)框架的核心。
  • Item(項(xiàng)目):定義了爬取結(jié)果的數(shù)據(jù)結(jié)構(gòu),爬取的數(shù)據(jù)會(huì)被賦值成該對(duì)象。
  • Scheduler(調(diào)度器):用來(lái)接受引擎發(fā)過(guò)來(lái)的請(qǐng)求并加入隊(duì)列中,并在引擎再次請(qǐng)求的時(shí)候提供給引擎。
  • Item Pipeline(項(xiàng)目管道):負(fù)責(zé)處理由蜘蛛從網(wǎng)頁(yè)中抽取的項(xiàng)目,它的主要任務(wù)是清洗、驗(yàn)證和存儲(chǔ)數(shù)據(jù)。
  • Downloader(下載器):用于下載網(wǎng)頁(yè)內(nèi)容,并將網(wǎng)頁(yè)內(nèi)容返回給Spiders。
  • Spiders(蜘蛛):其內(nèi)定義了爬取的邏輯和網(wǎng)頁(yè)的解析規(guī)則,它主要負(fù)責(zé)解析響應(yīng)并生成提取結(jié)果和新的請(qǐng)求。
  • Downloader Middlewares(下載器中間件):位于引擎和下載器之間的鉤子框架,主要是處理引擎與下載器之間的請(qǐng)求及響應(yīng)。
  • Spider Middlewares(Spiders中間件):位于引擎和蜘蛛之間的鉤子框架,主要工作是處理蜘蛛輸入的響應(yīng)和輸出的結(jié)果及新的請(qǐng)求。

Scrapy數(shù)據(jù)流機(jī)制

scrapy 中的數(shù)據(jù)流由引擎控制,其過(guò)程如下:

  • Engine 首先打開一個(gè)網(wǎng)站,找到處理該網(wǎng)站的 Spider 并向該 Spider 請(qǐng)求第一個(gè)要爬取的 URL。
  • Engine 從 Spider 中獲取到第一個(gè)要爬取的 URL 并通過(guò) Scheduler 以 Request 的形式調(diào)度。
  • Engine 向 Scheduler 請(qǐng)求下一個(gè)要爬取的 URL。
  • Scheduler 返回下一個(gè)要爬取的 URL 給 Engine,Engine 將 URL 通過(guò) Downloader Middlewares 轉(zhuǎn)發(fā)給 Downloader 下載。
  • 一旦頁(yè)面下載完畢, Downloader 生成一個(gè)該頁(yè)面的 Response,并將其通過(guò) Downloader Middlewares 發(fā)送給 Engine。
  • Engine 從下載器中接收到 Response 并通過(guò) Spider Middlewares 發(fā)送給 Spider 處理。
  • Spider 處理 Response 并返回爬取到的 Item 及新的 Request 給 Engine。
  • Engine 將 Spider 返回的 Item 給 Item Pipeline,將新的 Request 給 Scheduler。
  • 重復(fù)第二步到最后一步,直到 Scheduler 中沒(méi)有更多的 Request,Engine 關(guān)閉該網(wǎng)站,爬取結(jié)束。

通過(guò)多個(gè)組件的相互協(xié)作、不同組件完成工作的不同、組件很好地支持異步處理,scrapy 最大限度地利用了網(wǎng)絡(luò)帶寬,大大提高了數(shù)據(jù)爬取和處理的效率。

二、scrapy的安裝和創(chuàng)建項(xiàng)目

pip install Scrapy -i http://pypi.douban.com/simple --trusted-host pypi.douban.com

安裝方法參考官方文檔:https://docs.scrapy.org/en/latest/intro/install.html

安裝完成之后,如果可以正常使用 scrapy 命令,那就是安裝成功了。

Scrapy是框架,已經(jīng)幫我們預(yù)先配置好了很多可用的組件和編寫爬蟲時(shí)所用的腳手架,也就是預(yù)生成一個(gè)項(xiàng)目框架,我們可以基于這個(gè)框架來(lái)快速編寫爬蟲。

Scrapy框架是通過(guò)命令行來(lái)創(chuàng)建項(xiàng)目的,創(chuàng)建項(xiàng)目的命令如下:

scrapy startproject practice

命令執(zhí)行后,在當(dāng)前運(yùn)行目錄下便會(huì)出現(xiàn)一個(gè)文件夾,叫作practice,這就是一個(gè)Scrapy 項(xiàng)目框架,我們可以基于這個(gè)項(xiàng)目框架來(lái)編寫爬蟲。

project/
    __pycache__
     spiders/
         __pycache__
        __init__.py
        spider1.py
        spider2.py
        ...
    __init__.py
    items.py
    middlewares.py
    pipelines.py
    settings.py
scrapy.cfg

各個(gè)文件的功能描述如下:

  • scrapy.cfg:它是 Scrapy 項(xiàng)目的配置文件,其內(nèi)定義了項(xiàng)目的配置文件路徑、部署相關(guān)信息等內(nèi)容。
  • items.py:它定義 Item 數(shù)據(jù)結(jié)構(gòu),所有的 Item 的定義都可以放這里。
  • pipelines.py:它定義 Item Pipeline 的實(shí)現(xiàn),所有的 Item Pipeline 的實(shí)現(xiàn)都可以放這里。
  • settings.py:它定義項(xiàng)目的全局配置。
  • middlewares.py:它定義 Spider Middlewares 和 Downloader Middlewares 的實(shí)現(xiàn)。
  • spiders:其內(nèi)包含一個(gè)個(gè) Spider 的實(shí)現(xiàn),每個(gè) Spider 都有一個(gè)文件。

三、scrapy的基本使用

實(shí)例1:爬取 Quotes

  • 創(chuàng)建一個(gè) Scrapy 項(xiàng)目。
  • 創(chuàng)建一個(gè) Spider 來(lái)抓取站點(diǎn)和處理數(shù)據(jù)。
  • 通過(guò)命令行運(yùn)行,將抓取的內(nèi)容導(dǎo)出。

目標(biāo)URL:http://quotes.toscrape.com/

創(chuàng)建項(xiàng)目

創(chuàng)建一個(gè) scrapy 項(xiàng)目,項(xiàng)目文件可以直接用 scrapy 命令生成,命令如下所示:

scrapy startproject practice    

創(chuàng)建Spider

Spider是自己定義的類,scrapy用它從網(wǎng)頁(yè)里抓取內(nèi)容,并解析抓取的結(jié)果。這個(gè)類必須繼承 Scrapy 提供的Spider類scrapy.Spider,還要定義Spider的名稱和起始請(qǐng)求,以及怎樣處理爬取后的結(jié)果的方法。

使用命令行創(chuàng)建一個(gè)Spider,命令如下:

cd practice
scrapy genspider quotes quotes.toscrape.com

切換路徑到剛才創(chuàng)建的practice文件夾,然后執(zhí)行g(shù)enspider命令。第一個(gè)參數(shù)是Spider的名稱,第二個(gè)參數(shù)是網(wǎng)站域名。執(zhí)行完畢之后,spiders 文件夾中多了一個(gè)quotes.py,它就是剛剛創(chuàng)建的Spider,內(nèi)容如下:

import scrapy


class QuotesSpider(scrapy.Spider):
    name = "quotes"
    allowed_domains = ["quotes.toscrape.com"]
    start_urls = ['http://quotes.toscrape.com/']


    def parse(self, response):
        pass

可以看到quotes.py里有三個(gè)屬性——name、allowed_domains 和 start_urls,還有一個(gè)方法 parse。

  • name:它是每個(gè)項(xiàng)目唯一的名字,用來(lái)區(qū)分不同的 Spider。
  • allowed_domains:它是允許爬取的域名,如果初始或后續(xù)的請(qǐng)求鏈接不是這個(gè)域名下的,則請(qǐng)求鏈接會(huì)被過(guò)濾掉。
  • start_urls:它包含了 Spider 在啟動(dòng)時(shí)爬取的 url 列表,初始請(qǐng)求是由它來(lái)定義的。
  • parse:它是 Spider 的一個(gè)方法。默認(rèn)情況下,被調(diào)用時(shí) start_urls 里面的鏈接構(gòu)成的請(qǐng)求完成下載執(zhí)行后,返回的響應(yīng)就會(huì)作為唯一的參數(shù)傳遞給這個(gè)函數(shù)。該方法負(fù)責(zé)解析返回的響應(yīng)、提取數(shù)據(jù)或者進(jìn)一步生成要處理的請(qǐng)求。

創(chuàng)建 Item

Item 是保存爬取數(shù)據(jù)的容器,它的使用方法和字典類似。不過(guò),相比字典,Item 多了額外的保護(hù)機(jī)制,可以避免拼寫錯(cuò)誤或者定義字段錯(cuò)誤。

創(chuàng)建 Item 需要繼承 scrapy.Item 類,并且定義類型為 scrapy.Field 的字段。觀察目標(biāo)網(wǎng)站,我們可以獲取到的內(nèi)容有 text、author、tags。

定義Item,此時(shí)進(jìn)入items.py修改如下:

import scrapy


class QuoteItem(scrapy.Item):
    text = scrapy.Field()
    author = scrapy.Field()
    tags = scrapy.Field()

定義了三個(gè)字段,并將類的名稱修改為QuoteItem,接下來(lái)爬取時(shí)會(huì)使用到這個(gè) Item。

解析 Response

parse 方法的參數(shù) response 是 start_urls 里面的鏈接爬取后的結(jié)果。所以在 parse 方法中,我們可以直接對(duì) response 變量包含的內(nèi)容進(jìn)行解析,比如瀏覽請(qǐng)求結(jié)果的網(wǎng)頁(yè)源代碼,或者進(jìn)一步分析源代碼內(nèi)容,或者找出結(jié)果中的鏈接而得到下一個(gè)請(qǐng)求。

解析 Response

可以看到網(wǎng)頁(yè)中既有想要提取的數(shù)據(jù),又有下一頁(yè)的鏈接,這兩部分內(nèi)容都可以進(jìn)行處理。

首先看看網(wǎng)頁(yè)結(jié)構(gòu),如圖所示。每一頁(yè)都有多個(gè) class 為 quote 的區(qū)塊,每個(gè)區(qū)塊內(nèi)都包含 text、author、tags。那么我們先找出所有的 quote,然后提取每一個(gè) quote 中的內(nèi)容。

提取數(shù)據(jù)的方式可以是 CSS 選擇器 或 XPath 選擇器

使用 Item

上文定義了 Item,接下來(lái)就要使用它了。Item 可以理解為一個(gè)字典,不過(guò)在聲明的時(shí)候需要實(shí)例化。然后依次用剛才解析的結(jié)果賦值 Item 的每一個(gè)字段,最后將 Item 返回即可。

import scrapy
from practice.items import QuoteItem


class QuotesSpider(scrapy.Spider):
    name = 'quotes'
    allowed_domains = ['quotes.toscrape.com']
    start_urls = ['http://quotes.toscrape.com/']


    def parse(self, response, **kwargs):
        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

后續(xù) Request

上面的操作實(shí)現(xiàn)了從初始頁(yè)面抓取內(nèi)容。實(shí)現(xiàn)翻頁(yè)爬取,這就需要從當(dāng)前頁(yè)面中找到信息來(lái)生成下一個(gè)請(qǐng)求,然后在下一個(gè)請(qǐng)求的頁(yè)面里找到信息再構(gòu)造下一個(gè)請(qǐng)求。這樣循環(huán)往復(fù)迭代,從而實(shí)現(xiàn)整站的爬取。

后續(xù) Request

查看網(wǎng)頁(yè)源代碼,可以發(fā)現(xiàn)下一頁(yè)的鏈接是 /page/2/,但實(shí)際上全鏈接為:http://quotes.toscrape.com/page/2/ ,通過(guò)這個(gè)鏈接就可以構(gòu)造下一個(gè)請(qǐng)求。

構(gòu)造請(qǐng)求時(shí)需要用到 scrapy.Request。這里我們傳遞兩個(gè)參數(shù)—— url 和 callback,這兩個(gè)參數(shù)的說(shuō)明如下:

  • url:它是請(qǐng)求鏈接
  • callback:它是回調(diào)函數(shù)。當(dāng)指定了該回調(diào)函數(shù)的請(qǐng)求完成之后,獲取到響應(yīng),引擎會(huì)將該響應(yīng)作為參數(shù)傳遞給這個(gè)回調(diào)函數(shù)?;卣{(diào)函數(shù)進(jìn)行解析或生成下一個(gè)請(qǐng)求,回調(diào)函數(shù)如上文的 parse() 所示。

由于 parse 就是解析 text、author、tags 的方法,而下一頁(yè)的結(jié)構(gòu)和剛才已經(jīng)解析的頁(yè)面結(jié)構(gòu)是一樣的,所以我們可以再次使用 parse 方法來(lái)做頁(yè)面解析。

"""
@Author  :葉庭云
@Date    :2020/10/2 11:40
@CSDN    :https://blog.csdn.net/fyfugoyfa
"""
import scrapy
from practice.items import QuoteItem


class QuotesSpider(scrapy.Spider):
    name = 'quotes'
    allowed_domains = ['quotes.toscrape.com']
    start_urls = ['http://quotes.toscrape.com/']


    def parse(self, response, **kwargs):
        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_page = response.css('.pager .next a::attr("href")').extract_first()
        next_url = response.urljoin(next_page)
        yield scrapy.Request(url=next_url, callback=self.parse)

運(yùn)行 接下來(lái),進(jìn)入目錄,運(yùn)行如下命令:

scrapy crawl quotes -o quotes.csv

命令運(yùn)行后,項(xiàng)目?jī)?nèi)多了一個(gè) quotes.csv 文件,文件包含了剛才抓取的所有內(nèi)容。

quotes.csv文件

輸出格式還支持很多種,例如 json、xml、pickle、marshal 等,還支持 ftp、s3 等遠(yuǎn)程輸出,另外還可以通過(guò)自定義 ItemExporter 來(lái)實(shí)現(xiàn)其他的輸出。

scrapy crawl quotes -o quotes.json
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 輸出需要正確配置用戶名、密碼、地址、輸出路徑,否則會(huì)報(bào)錯(cuò)。

通過(guò) scrapy 提供的 Feed Exports,我們可以輕松地輸出抓取結(jié)果到文件,對(duì)于一些小型項(xiàng)目來(lái)說(shuō),這應(yīng)該足夠了。不過(guò)如果想要更復(fù)雜的輸出,如輸出到數(shù)據(jù)庫(kù)等,可以靈活使用 Item Pileline 來(lái)完成。

實(shí)例2:爬取圖片

目標(biāo)URL:http://sc.chinaz.com/tupian/dangaotupian.html

創(chuàng)建項(xiàng)目

scrapy startproject get_img
cd get_img
scrapy genspider img_spider sc.chinaz.com

構(gòu)造請(qǐng)求

img_spider.py 中定義 start_requests() 方法,比如爬取這個(gè)網(wǎng)站里的蛋糕圖片,爬取頁(yè)數(shù)為 10,生成 10 次請(qǐng)求,如下所示:

    def start_requests(self):
        for i in range(1, 11):
            if i == 1:
                url = 'http://sc.chinaz.com/tupian/dangaotupian.html'
            else:
                url = f'http://sc.chinaz.com/tupian/dangaotupian_{i}.html'
            yield scrapy.Request(url, self.parse)

編寫 items.py

import scrapy


class GetImgItem(scrapy.Item):
    img_url = scrapy.Field()
    img_name = scrapy.Field()

編寫 img_spider.py Spider 類定義了如何爬取某個(gè)(或某些)網(wǎng)站,包括了爬取的動(dòng)作(例如:是否跟進(jìn)鏈接)以及如何從網(wǎng)頁(yè)的內(nèi)容中提取結(jié)構(gòu)化數(shù)據(jù)(抓取item)

"""
@Author  :葉庭云
@Date    :2020/10/2 11:40
@CSDN    :https://blog.csdn.net/fyfugoyfa
"""
import scrapy
from get_img.items import GetImgItem


class ImgSpiderSpider(scrapy.Spider):
    name = 'img_spider'


    def start_requests(self):
        for i in range(1, 11):
            if i == 1:
                url = 'http://sc.chinaz.com/tupian/dangaotupian.html'
            else:
                url = f'http://sc.chinaz.com/tupian/dangaotupian_{i}.html'
            yield scrapy.Request(url, self.parse)


    def parse(self, response, **kwargs):
        src_list = response.xpath('//div[@id="container"]/div/div/a/img/@src2').extract()
        alt_list = response.xpath('//div[@id="container"]/div/div/a/img/@alt').extract()
        for alt, src in zip(alt_list, src_list):
            item = GetImgItem()       # 生成item對(duì)象
            # 賦值
            item['img_url'] = src
            item['img_name'] = alt
            yield item

編寫管道文件 pipelines.py

Scrapy 提供了專門處理下載的 Pipeline,包括文件下載和圖片下載。下載文件和圖片的原理與抓取頁(yè)面的原理一樣,因此下載過(guò)程支持異步和多線程,十分高效。

from scrapy.pipelines.images import ImagesPipeline  # scrapy圖片下載器
from scrapy import Request
from scrapy.exceptions import DropItem


class GetImgPipeline(ImagesPipeline):
    # 請(qǐng)求下載圖片
    def get_media_requests(self, item, info):
        yield Request(item['img_url'], meta={'name': item['img_name']})


    def item_completed(self, results, item, info):
        # 分析下載結(jié)果并剔除下載失敗的圖片
        image_paths = [x['path'] for ok, x in results if ok]
        if not image_paths:
            raise DropItem("Item contains no images")
        return item


    # 重寫file_path方法,將圖片以原來(lái)的名稱和格式進(jìn)行保存
    def file_path(self, request, response=None, info=None):
        name = request.meta['name']  # 接收上面meta傳遞過(guò)來(lái)的圖片名稱
        file_name = name + '.jpg'       # 添加圖片后綴名
        return file_name

在這里實(shí)現(xiàn)了 GetImagPipeline,繼承 Scrapy 內(nèi)置的 ImagesPipeline,重寫了下面幾個(gè)方法:

  • get_media_requests()。它的第一個(gè)參數(shù) item 是爬取生成的 Item 對(duì)象。我們將它的 url 字段取出來(lái),然后直接生成 Request 對(duì)象。此 Request 加入調(diào)度隊(duì)列,等待被調(diào)度,執(zhí)行下載。
  • item_completed(),它是當(dāng)單個(gè) Item 完成下載時(shí)的處理方法。因?yàn)榭赡苡袀€(gè)別圖片未成功下載,所以需要分析下載結(jié)果并剔除下載失敗的圖片。該方法的第一個(gè)參數(shù) results 就是該 Item 對(duì)應(yīng)的下載結(jié)果,它是一個(gè)列表形式,列表每一個(gè)元素是一個(gè)元組,其中包含了下載成功或失敗的信息。這里我們遍歷下載結(jié)果找出所有成功的下載列表。如果列表為空,那么說(shuō)明該 Item 對(duì)應(yīng)的圖片下載失敗了,隨即拋出異常DropItem,該 Item 忽略。否則返回該 Item,說(shuō)明此 Item 有效。
  • file_path(),它的第一個(gè)參數(shù) request 就是當(dāng)前下載對(duì)應(yīng)的 Request 對(duì)象。這個(gè)方法用來(lái)返回保存的文件名,接收上面meta傳遞過(guò)來(lái)的圖片名稱,將圖片以原來(lái)的名稱和定義格式進(jìn)行保存。

配置文件 settings.py

# setting.py


BOT_NAME = 'get_img'


SPIDER_MODULES = ['get_img.spiders']
NEWSPIDER_MODULE = 'get_img.spiders'


# Crawl responsibly by identifying yourself (and your website) on the user-agent
USER_AGENT = 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36'


# Obey robots.txt rules
ROBOTSTXT_OBEY = False


# Configure maximum concurrent requests performed by Scrapy (default: 16)
CONCURRENT_REQUESTS = 32


# Configure a delay for requests for the same website (default: 0)
# See https://docs.scrapy.org/en/latest/topics/settings.html#download-delay
# See also autothrottle settings and docs
DOWNLOAD_DELAY = 0.25


# Configure item pipelines
# See https://docs.scrapy.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
   'get_img.pipelines.GetImgPipeline': 300,
}


IMAGES_STORE = './images'   # 設(shè)置保存圖片的路徑 會(huì)自動(dòng)創(chuàng)建

運(yùn)行程序:

# 切換路徑到img_spider的目錄
scrapy crawl img_spider

scrapy框架爬蟲一邊爬取一邊下載,下載速度非??臁?/p>

查看本地 images 文件夾,發(fā)現(xiàn)圖片都已經(jīng)成功下載,如圖所示:

scrapy框架爬蟲

到現(xiàn)在為止我們就大體知道了 Scrapy 的基本架構(gòu)并實(shí)操創(chuàng)建了一個(gè) Scrapy 項(xiàng)目,編寫代碼進(jìn)行了實(shí)例抓取,熟悉了 scrapy 爬蟲框架的基本使用。之后還需要更加詳細(xì)地了解和學(xué)習(xí) scrapy 的用法,感受它的強(qiáng)大。

作者:葉庭云

原文鏈接:https://yetingyun.blog.csdn.net/article/details/108217479

1 人點(diǎn)贊