外行學(xué) Python 爬蟲 第十篇 爬蟲框架Scrapy

前面幾個(gè)章節(jié)利用 python 的基礎(chǔ)庫實(shí)現(xiàn)網(wǎng)絡(luò)數(shù)據(jù)的獲取、解構(gòu)以及存儲(chǔ),同時(shí)也完成了簡單的數(shù)據(jù)讀取操作。在這個(gè)過程中使用了其他人完成的功能庫來加快我們的爬蟲實(shí)現(xiàn)過程,對(duì)于爬蟲也有相應(yīng)的 python 框架供我們使用「不重復(fù)造輪子是程序員的一大特點(diǎn)」,當(dāng)我們了解爬蟲的實(shí)現(xiàn)過程以后就可以嘗試使用框架來完成自己的爬蟲,加快開發(fā)速度。

在 python 中比較常用的爬蟲框架有 Scrapy 和 PySpider,今天針對(duì) Scrapy 爬蟲框架來實(shí)現(xiàn)前面幾篇所實(shí)現(xiàn)的功能。

準(zhǔn)備工作

首先需要在系統(tǒng)中安裝 Scrapy 「也可以使用 virtualenv 創(chuàng)建一個(gè)虛擬環(huán)境」,可以通過以下方式來安裝 Scrapy。

#使用 pip 來安裝 Scrapy
pip install Scrapy

Scrapy 安裝完成以后,通過以下方式來創(chuàng)建一個(gè)基本的 Scrapy 項(xiàng)目。

scrapy startproject project

編寫你的爬蟲

在 Scrapy 中所有的爬蟲類必須是 scrapy.Spider 的子類,你可以自定義要發(fā)出的初始請(qǐng)求,選擇如何跟蹤頁面中的鏈接,以及如何解析下載的頁面內(nèi)容以提取數(shù)據(jù)。

一個(gè)基礎(chǔ)爬蟲

第一個(gè)爬蟲我們選擇使用 scrapy.Spider 作為父類,建立一個(gè)簡單的單頁面爬蟲。建立一個(gè) Scrapy 爬蟲文件可以直接在 spider 目錄下新建文件然后手動(dòng)編寫相關(guān)內(nèi)容,也可以使用 scrapy genspider [options] <name> <domain> 命令來建立一個(gè)空白模板的爬蟲文件,文件內(nèi)容如下:

# -*- coding: utf-8 -*-
import scrapy


class TestSpider(scrapy.Spider):
    name = 'test'
    allowed_domains = ['domain.com']
    start_urls = ['http://domain.com/']

    def parse(self, response):
        pass

如上所示 TestSpider 繼承自 scrapy.Spider,并定義了一些屬性和方法:

  • name:當(dāng)前爬蟲的名稱,用來標(biāo)識(shí)該爬蟲。
  • allowed_domains:當(dāng)前爬蟲所爬取的域名。
  • start_urls:爬蟲將順序爬取其中的 url。
  • parse:爬蟲的回調(diào)函數(shù),用來處理請(qǐng)求的響應(yīng)內(nèi)容,數(shù)據(jù)解析通常在該函數(shù)內(nèi)完成。

我們使用 scrapy.Spider 來建立一個(gè)爬取「立創(chuàng)商城」上所有元件分類的爬蟲,爬蟲名稱命名為 catalog,將 start_urls 更換為 https://www.szlcsc.com/catalog.html,下面貼出解析函數(shù)的代碼

    def parse(self, response):
        catalogs = response.xpath('//div[@class="catalog_a"]')
        for catalog in catalogs:
            catalog_dl = catalog.xpath('dl')
            for tag in catalog_dl:
                parent = tag.xpath('dt/a/text()').extract()
                childs = tag.xpath('dd/a/text()').extract()
                parent = self.catalog_filter_left(self.catalog_filter_right(parent[0]))
                yield CatalogItem(
                    parent = parent,
                    child = None,
                )
                for child in childs:
                    yield CatalogItem(
                        parent = parent,
                        child = self.catalog_filter_right(child),
                    )

通過以下命令來啟動(dòng)爬蟲,觀察爬蟲的爬取過程及結(jié)果。

scrapy crawl catalog

遞歸爬蟲

上一小節(jié)中實(shí)現(xiàn)了一個(gè)簡單的單頁面爬蟲,它僅能訪問在 start_urls 中列明的頁面,無法從獲取的頁面中提取出鏈接并跟進(jìn)。scrapy 通過 CrawlSpider 來實(shí)現(xiàn)按照一定的規(guī)則從當(dāng)前頁面中提取出 url,并跟進(jìn)爬取??梢酝ㄟ^命令 scrapy genspider -t crawl test domain.com 來指定使用 CrawlSpider 建立爬蟲。生產(chǎn)文件內(nèi)容如下:

mport scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule


class TestSpider(CrawlSpider):
    name = 'test'
    allowed_domains = ['domain.com']
    start_urls = ['http://domain.com/']

    rules = (
        Rule(LinkExtractor(allow=r'Items/'), callback='parse_item', follow=True),
    )

    def parse_item(self, response):
        item = {}
        #item['domain_id'] = response.xpath('//input[@id="sid"]/@value').get()
        #item['name'] = response.xpath('//div[@id="name"]').get()
        #item['description'] = response.xpath('//div[@id="description"]').get()
        return item

基于 CrawlerSpider 的爬蟲不同之處在于多了一個(gè) rules 的屬性,該屬性定義了如何從網(wǎng)頁中提取 url,并使用指定的回調(diào)函數(shù)來處理爬取結(jié)果。

使用遞歸爬蟲來實(shí)現(xiàn)「立創(chuàng)商城」中生產(chǎn)商的爬取在合適不過了,以下貼出相應(yīng)的鏈接提取規(guī)則和處理函數(shù)。

    rules = (
        Rule(LinkExtractor(allow=(r'https://list.szlcsc.com/brand/[0-9]+.html', )), callback='parse_item', follow=False),
    )

    def parse_item(self, response):
        brand_info_logo = response.xpath('//div[@class="brand-info-logo"]')
        brand_info_text = response.xpath('//div[@class="brand-info-text"]')
        name = brand_info_logo.xpath('h1[@class="brand-info-name"]/text()').extract()
        url = brand_info_logo.xpath('descendant::a[@class="blue"]/@href').extract()
        desc = brand_info_text.xpath('div[@class="introduce_txt"]//text()').extract()
        ...
        return BrandItem(
            name = name,
            url = url,
            desc = desc_text,
        )

動(dòng)態(tài)數(shù)據(jù)處理

爬蟲在處理的過程中不可避免的會(huì)遇到動(dòng)態(tài)數(shù)據(jù)的處理,「立創(chuàng)商城」中元件的列表頁面的翻頁即是通過 ajax 來實(shí)現(xiàn)的,如果僅僅使用上一節(jié)中的遞歸爬取的方法,有很多的元件將會(huì)被漏掉,在這里可以使用 scrapy 模擬 post 方法來實(shí)現(xiàn)翻頁的效果。

在 scrapy 中向網(wǎng)站中提交數(shù)據(jù)使用 scrapy.FormRequest 來實(shí)現(xiàn)。FormRequest 類擴(kuò)展了基 Request 具有處理HTML表單的功能。通過 FormReques 向翻頁 API 上提交新的頁面信息,從而獲取新頁面中的 Json 數(shù)據(jù),通過解析 Json 數(shù)據(jù)來獲取整個(gè)網(wǎng)站中的元件信息。

動(dòng)態(tài)翻頁所需要的 API 及提交數(shù)據(jù)的格式在 外行學(xué) Python 爬蟲 第六篇 動(dòng)態(tài)翻頁 中做過分析,可以在那里找到相關(guān)的信息。

通過 FormRequest 來指定 url、提交數(shù)據(jù)、返回?cái)?shù)據(jù)的回調(diào)函數(shù)等,具體實(shí)現(xiàn)如下:

            yield scrapy.FormRequest(url=product_post_url,
                        formdata=post_data,
                        callback=self.json_callback,
                        dont_filter = True)

由于 Scrapy 中自帶了 url 去重功能,因此需在 FormRequest 中設(shè)置 dont_filter = True,否則 FormRequest 只會(huì)執(zhí)行一次。

數(shù)據(jù)的存儲(chǔ)

Scrapy 使用 Item 來定義通用的輸出數(shù)據(jù)格式,數(shù)據(jù)通過 Item 在 Scrapy 的各個(gè)模塊中進(jìn)行傳遞,以下是一個(gè)簡單的 Item 定義:

class BrandItem(scrapy.Item):
    name = scrapy.Field()
    url = scrapy.Field()
    desc = scrapy.Field()

數(shù)據(jù)的處理通常在 Pipeline 中進(jìn)行,在爬蟲中獲取的數(shù)據(jù)將通過 Item 傳遞到 Pipeline 的 process_item 方法中進(jìn)行處理,以下代碼實(shí)現(xiàn)了將數(shù)據(jù)存在 sqlite 數(shù)據(jù)庫中。

class BrandPipeline(object):
    def __init__(self, database_uri):
        db.init_url(url=database_uri)
        self.save = SaveData()

    @classmethod
    def from_crawler(cls, crawler):
        return cls(
            database_uri=crawler.settings.get('SQLALCHEMY_DATABASE_URI'),
        )

    def process_item(self, item, spider):
        if spider.name == 'brand':
            self.save.save_brand(brand=item)
            raise DropItem('Drop Item: %s' % item)

        return item

其中 from_crawler 方法用來沖 setting 文件中獲取數(shù)據(jù)庫鏈接。

Item 按照在 setting 中定義的優(yōu)先級(jí)在各個(gè) Pipeline 中進(jìn)行傳遞,如果在某個(gè) Pipeline 中對(duì)該 Item 處理完成后續(xù)無需處理,可以使用 DropItem 來終止 Item 向其他的 Pipeline 傳遞。

反爬處理

爬蟲不可避免的會(huì)遇到網(wǎng)站的反爬策略,一般的反爬策略是限制 IP 的訪問間隔,判斷當(dāng)前的訪問代理是否總是爬蟲等。

針對(duì)以上策略,可以通過設(shè)置兩個(gè)請(qǐng)求之間間隔隨機(jī)的時(shí)間,并設(shè)置 User-Agent 來規(guī)避一部分的反爬策略。

設(shè)置請(qǐng)求間隔隨機(jī)時(shí)間的中間件實(shí)現(xiàn)如下:

class ScrapyTestRandomDelayMiddleware(object):
    def __init__(self, crawler):
        self.delay = crawler.spider.settings.get("RANDOM_DELAY")

    @classmethod
    def from_crawler(cls, crawler):
        return cls(crawler)

    def process_request(self, request, spider):
        delay = random.randint(0, self.delay)
        spider.logger.debug("### random delay: %s s ###" % delay)
        time.sleep(delay)

然后在 setting 文件中啟用該中間件。

RANDOM_DELAY = 3
DOWNLOADER_MIDDLEWARES = {
   'scrapy_test.middlewares.ScrapyTestDownloaderMiddleware': None,
   'scrapy_test.middlewares.ScrapyTestRandomDelayMiddleware': 999,
}

User-Agent 可以直接在 setting 文件中修改,在我們的瀏覽器中查看當(dāng)前瀏覽器的 User-Agent,將 Scrapy 的 User-Agent 設(shè)置為瀏覽器的 User-Agent。以下是 Chrome 流量中 User-Agent 的查找方法。


前面都沒有提到過網(wǎng)站的反爬蟲,這次提到的原因是真的被「立創(chuàng)商城」給限制訪問了。

運(yùn)行爬蟲

今天將前面所完成的爬蟲功能使用 Scrapy 進(jìn)行了一個(gè)重構(gòu),catalog 使用的是單頁爬蟲用來獲取原件的分類信息,brand 是一個(gè)遞歸爬蟲用來獲取原件生產(chǎn)商信息,product 是一個(gè)通過 post 動(dòng)態(tài)獲取 json 并解析的爬蟲,主要用來獲取所有元件的信息。

有多個(gè)爬蟲需要運(yùn)行,可以使用以下方法逐個(gè)運(yùn)行爬蟲

# -*- coding:utf-8 -*-
import os

os.system("scrapy crawl brand")
os.system("scrapy crawl catalog")
os.system("scrapy crawl product")

如果想同時(shí)運(yùn)行多個(gè)爬蟲,以下方法是個(gè)不錯(cuò)的選擇

# -*- coding:utf-8 -*-

from scrapy.utils.project import get_project_settings
from scrapy.crawler import CrawlerProcess

def main():
    setting = get_project_settings()
    process = CrawlerProcess(setting)
    process.crawl(brand)
    process.crawl(catalog)
    process.crawl(product)
    process.start()
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容