爬蟲之scrapy框架

1.scrapy框架介紹

  Scrapy是用純Python實現的一個爲了爬取網站數據、提取結構性數據而編寫的應用框架html

  Scrapy 特點是使用了 Twisted異步網絡框架來處理網絡通信,加快了下載速度,不用本身去實現異步框架,而且包含了各類中間件接口,能夠靈活的完成各類需求python

1.1 scrapy框架架構圖

Scrapy Engine(引擎): 負責Spider、ItemPipeline、Downloader、Scheduler中間的通信,信號、數據傳遞等。git

Scheduler(調度器): 它負責接受引擎發送過來的Request請求,並按照必定的方式進行整理排列,入隊,當引擎須要時,交還給引擎。github

Downloader(下載器):負責下載Scrapy Engine(引擎)發送的全部Requests請求,並將其獲取到的Responses交還給Scrapy Engine(引擎),由引擎交給Spider來處理,ajax

Spider(爬蟲):它負責處理全部Responses,從中分析提取數據,獲取Item字段須要的數據,並將須要跟進的URL提交給引擎,再次進入Scheduler(調度器),mongodb

Item Pipeline(管道):它負責處理Spider中獲取到的Item,並進行進行後期處理(詳細分析、過濾、存儲等)的地方.shell

Downloader Middlewares(下載中間件):你能夠看成是一個能夠自定義擴展下載功能的組件。數據庫

Spider Middlewares(Spider中間件):你能夠理解爲是一個能夠自定擴展和操做引擎和Spider中間通訊的功能組件(好比進入Spider的Responses;和從Spider出去的Requests)segmentfault

注意!只有當調度器中不存在任何request了,整個程序纔會中止,(也就是說,對於下載失敗的URL,Scrapy也會從新下載。)瀏覽器

官方文檔0.25連接

最新官方文檔1.5連接

以上參考連接

1.2 scrapy安裝

#Windows平臺
    1、pip3 install wheel 
    #安裝後,便支持經過wheel文件安裝軟件
    3、pip3 install lxml
    4、pip3 install pyopenssl
    五、下載並安裝pywin32:https://sourceforge.net/projects/pywin32/files/pywin32/
    # 根據電腦Python版本和位數下載並安裝最新版的pywin32,它會自動尋找Python的安裝路徑,因此不須要作任何修改,一直單擊【下一步】便可。
    # 這裏有時候會報中止工做,可是經過pip3 list 命令能夠看到他已經存在
    六、下載twisted的wheel文件:http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
    七、執行pip3 install 下載目錄路徑\Twisted-17.9.0-cp36-cp36m-win_amd64.whl
    # Scrapy須要依賴Twisted。Twisted是Python下面一個很是重要的基於事件驅動的IO引擎。Twisted的安裝依賴於pywin32
    8、pip3 install scrapy
  
#Linux平臺
    一、pip3 install scrapy

# 這裏安裝好pywin32後,後期可能仍是沒法運行(報ImportError: DLL load failed錯誤),這裏通常都是咱們該模塊安裝的有問題致使的,這裏能夠參考知乎或者參考Stack Overflow

個人是參考Stack Overflow下的說明,執行了 pip install win10toast --ignore-installed 纔好的

1.3 命令行相關指令

# 1 查看幫助
    scrapy -h
    scrapy <command> -h

# 2 有兩種命令:其中Project-only必須切到項目文件夾下才能執行,而Global的命令則不須要
    Global commands:
        startproject #建立項目
        genspider    #建立爬蟲程序
        settings     #若是是在項目目錄下,則獲得的是該項目的配置
        runspider    #運行一個獨立的python文件,沒必要建立項目
        shell        #scrapy shell url地址  在交互式調試,如選擇器規則正確與否
        fetch        #獨立於程單純地爬取一個頁面,能夠拿到請求頭
        view         #下載完畢後直接彈出瀏覽器,以此能夠分辨出哪些數據是ajax請求
        version      #scrapy version 查看scrapy的版本,scrapy version -v查看scrapy依賴庫的版本
    Project-only commands:
        crawl        #運行爬蟲,必須建立項目才行,確保配置文件中ROBOTSTXT_OBEY = False
        check        #檢測項目中有無語法錯誤
        list         #列出項目中所包含的爬蟲名
        edit         #編輯器,通常不用
        parse        #scrapy parse url地址 --callback 回調函數  #以此能夠驗證咱們的回調函數是否正確
        bench        #scrapy bentch壓力測試

# 3 官網連接
    https://docs.scrapy.org/en/latest/topics/commands.html

1.4 框架結構

'''
project_name/                
   scrapy.cfg              # 項目的主配置信息,用來部署scrapy時使用
   project_name/
       __init__.py
       items.py            # 設置數據存儲模板,用於結構化數據,如:Django的Model
       pipelines.py        #  數據處理行爲,如:通常結構化的數據持久化
       settings.py         # 配置文件,如:遞歸的層數、併發數,延遲下載等。強調:配置文件的選項必須大寫不然視爲無效,正確寫法USER_AGENT='xxxx'
       spiders/            # 爬蟲目錄,如:建立文件,編寫爬蟲規則
           __init__.py
           爬蟲1.py      # 爬蟲程序1
           爬蟲2.py
           爬蟲3.py

'''

1.5 項目流程

新建項目 (scrapy startproject xxx):新建一個新的爬蟲項目
明確目標 (編寫items.py):明確你想要抓取的目標
製做爬蟲 (spiders/xxspider.py):製做爬蟲開始爬取網頁
存儲內容 (pipelines.py):設計管道存儲爬取內容

1.6 啓動一個項目

1 scrapy startproject DianShang   # 建立爬蟲項目
2 scrapy genspider jd jd.com      # 生成一個爬蟲程序
3 scrapy crawl jd                 # 運行scrapy項目

如今咱們有了基本的項目骨架

1.7 spider類的說明

  Spiders是爲站點爬網和解析頁面定義自定義行爲的地方

spiders下的jd.py文件

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


class JdSpider(scrapy.Spider):
    name = 'jd'
    allowed_domains = ['jd.com']
    start_urls = ['http://jd.com/']

    def parse(self, response):
        pass

Spider類中的start_requests方法:

    def start_requests(self):
        cls = self.__class__
        if method_is_overridden(cls, Spider, 'make_requests_from_url'):
            warnings.warn(
                "Spider.make_requests_from_url method is deprecated; it "
                "won't be called in future Scrapy releases. Please "
                "override Spider.start_requests method instead (see %s.%s)." % (
                    cls.__module__, cls.__name__
                ),
            )
            for url in self.start_urls:
                yield self.make_requests_from_url(url)
        else:
            for url in self.start_urls:
                yield Request(url, dont_filter=True)
View Code

  有時候後臺的start_requests方法會訪問不到咱們的目標站點,咱們大多數狀況須要本身構造此方法,它的最終返回值是用生成器yield返回的,咱們本身寫也建議使用生成器。

start_requests方法它默認的回調函數就是parse

2. 建立項目

  咱們的目標是爬取亞馬遜商城iphoex的名稱,價格以及配送方,注意的是:咱們須要的這些信息都在手機詳情頁面,而在手機列表頁面只有咱們點擊它的圖片或者文字纔會看到手機詳細信息

  想要獲取手機的信息,如今咱們須要進行分佈爬取,第一次先獲取每個手機詳情頁面的url,能夠經過手機列表頁面的圖片進行獲取,也能經過文字獲取,而後經過二次解析去拿到咱們須要的信息

  拿到信息後,經過MongoDB對咱們的信息作持久化處理

2.1 建立項目文件

爬蟲Scrapy命令:
    1 scrapy startproject Amazon         # 建立爬蟲項目
    2 scrapy genspider amazon amazon.cn      # 生成一個爬蟲程序
    3 scrapy crawl amazon                   # 運行scrapy項目

建立好後 spiders下自動生成的amazon.py文件

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


class AmazonSpider(scrapy.Spider):
    name = 'amazon'
    allowed_domains = ['amazon.cn']
    start_urls = ['http://amazon.cn/']

    def parse(self, response):
        pass
View Code

2.2 建立啓動文件

  建立好後,它默認只能在終端運行,咱們能夠在它的根目錄下建立一個bin.py文件,來做爲它的執行文件

from scrapy.cmdline import execute

execute(["scrapy","crawl","amazon",'--nolog'])  # 不打印日誌信息

  若是不須要相關日誌信息,,能夠在列表後面追加一個參數:'--nolog'

2.3 關閉ROBOTSTXT_OBEY命令

  關閉setting下的ROBOTSTXT_OBEY命令,該命令的做用是讓你遵循爬蟲協議的狀況下爬取相關內容,咱們爲了不它對咱們爬取時的影響,能夠把他修改成False

 2.4 獲取商品信息

 獲取商品列表的詳情連接

amazon.py

import scrapy
from scrapy import Request  # 導入模塊

class AmazonSpider(scrapy.Spider):
    name = 'amazon'
    allowed_domains = ['amazon.cn']
    # 自定義配置,在Spider中custom_settings設置的是None
    custom_settings = {
        'REQUEST_HEADERS': {
            'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.75 Safari/537.36',
        }
    }

    def start_requests(self):
        r1 = Request(
                    url="https://www.amazon.cn/s/ref=nb_sb_ss_i_3_6?field-keywords=iphonex",
                     headers=self.settings.get('REQUEST_HEADERS'),
                     )
        yield r1

    def parse(self, response):
        # 獲取商品名
        # detail_urls = response.xpath('//*[@id="result_0"]/div/div[3]/div[1]/a/h2').extract()
        # 商品單個商品詳情連接
        # detail_urls = response.xpath('//*[@id="result_0"]/div/div[3]/div[1]/a/@href').extract()
        # 獲取整個頁面商品詳情連接
        # detail_urls = response.xpath('//li[contains(@id,"result_")]/div/div[3]/div[1]/a/@href').extract()
        detail_urls = response.xpath('//*[starts-with(@id,"result")]/div/div[3]/div[1]/a/@href').extract()

        print(detail_urls)

如今讓scrapy去訪問這些連接,只要parse函數返回一個Request對象,它就會放到異步請求列表裏面,並由twisted發送異步請求

import scrapy
from scrapy import Request  # 導入模塊


class AmazonSpider(scrapy.Spider):
    name = 'amazon'
    allowed_domains = ['amazon.cn']
    # 自定義配置,在Spider中custom_settings設置的是None
    custom_settings = {
        'REQUEST_HEADERS': {
            'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.75 Safari/537.36',
        }
    }

    def start_requests(self):
        r1 = Request(
            url="https://www.amazon.cn/s/ref=nb_sb_ss_i_3_6?field-keywords=iphonex",
            headers=self.settings.get('REQUEST_HEADERS'),
        )
        yield r1

    def parse(self, response):
        # 獲取商品名
        # detail_urls = response.xpath('//*[@id="result_0"]/div/div[3]/div[1]/a/h2').extract()
        # 商品單個商品詳情連接
        # detail_urls = response.xpath('//*[@id="result_0"]/div/div[3]/div[1]/a/@href').extract()
        # 獲取整個頁面商品詳情連接
        # detail_urls = response.xpath('//li[contains(@id,"result_")]/div/div[3]/div[1]/a/@href').extract()
        detail_urls = response.xpath('//*[starts-with(@id,"result")]/div/div[3]/div[1]/a/@href').extract()

        for url in detail_urls:
            yield Request(url=url,
                          headers=self.settings.get('REQUEST_HEADERS'),  # 請求頭
                          callback=self.parse_detail,  # 回調函數
                          dont_filter=True  # 不去重
                          )

    def parse_detail(self, response):  # 獲取商品詳細信息
        # 商品名,獲取第一個結果
        name = response.xpath('//*[@id="productTitle"]/text()').extract_first()
        if name:
            name = name.strip()
        # 商品價格
        price = response.xpath('//*[@id="priceblock_ourprice"]/text()').extract_first()
        # 配送方式
        delivery = response.xpath('//*[@id="ddmMerchantMessage"]/*[1]/text()').extract_first()

        print(name, price, delivery)

2.5 存儲商品信息到MongoDB

  咱們須要使用items.py文件

import scrapy

# 獲取你想要的字段
class AmazonItem(scrapy.Item):
    # define the fields for your item here 
    name = scrapy.Field()
    price= scrapy.Field()
    delivery=scrapy.Field()

  並修改amazon.py最後的數據返回值

amazon.py

import scrapy
from scrapy import Request  # 導入模塊
from Amazon.items import AmazonItem

class AmazonSpider(scrapy.Spider):
    name = 'amazon'
    allowed_domains = ['amazon.cn']
    # 自定義配置,在Spider中custom_settings設置的是None
    custom_settings = {
        'REQUEST_HEADERS': {
            'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.75 Safari/537.36',
        }
    }

    def start_requests(self):
        r1 = Request(
            url="https://www.amazon.cn/s/ref=nb_sb_ss_i_3_6?field-keywords=iphonex",
            headers=self.settings.get('REQUEST_HEADERS'),
        )
        yield r1

    def parse(self, response):
        # 獲取商品名
        # detail_urls = response.xpath('//*[@id="result_0"]/div/div[3]/div[1]/a/h2').extract()
        # 商品單個商品詳情連接
        # detail_urls = response.xpath('//*[@id="result_0"]/div/div[3]/div[1]/a/@href').extract()
        # 獲取整個頁面商品詳情連接
        # detail_urls = response.xpath('//li[contains(@id,"result_")]/div/div[3]/div[1]/a/@href').extract()
        detail_urls = response.xpath('//*[starts-with(@id,"result")]/div/div[3]/div[1]/a/@href').extract()

        for url in detail_urls:
            yield Request(url=url,
                          headers=self.settings.get('REQUEST_HEADERS'),  # 請求頭
                          callback=self.parse_detail,  # 回調函數
                          dont_filter=True  # 不去重
                          )

    def parse_detail(self, response):  # 獲取商品詳細信息
        # 商品名,獲取第一個結果
        name = response.xpath('//*[@id="productTitle"]/text()').extract_first()
        if name:
            name = name.strip()
        # 商品價格
        price = response.xpath('//*[@id="priceblock_ourprice"]/text()').extract_first()
        # 配送方式
        delivery = response.xpath('//*[@id="ddmMerchantMessage"]/*[1]/text()').extract_first()

        # 生成標準化數據

        item = AmazonItem()  # 實例化
        # 增長鍵值對
        item["name"] = name
        item["price"] = price
        item["delivery"] = delivery

        return item     # 返回的是一個字典

  處理Spider中獲取到的Item ,須要PipeLine將數據儲存到MongoDB中

pipelines.py

from pymongo import MongoClient

class MongodbPipeline(object):

    def __init__(self, host, port, db, table):
        self.host = host
        self.port = port
        self.db = db
        self.table = table

    @classmethod
    def from_crawler(cls, crawler):
        """
        Scrapy會先經過getattr判斷咱們是否自定義了from_crawler,有則調它來完
        成實例化
        """
        HOST = crawler.settings.get('HOST')
        PORT = crawler.settings.get('PORT')
        DB = crawler.settings.get('DB')
        TABLE = crawler.settings.get('TABLE')
        return cls(HOST, PORT, DB, TABLE)

    def open_spider(self, spider):
        """
        爬蟲剛啓動時執行一次
        """
        # self.client = MongoClient('mongodb://%s:%s@%s:%s' %(self.user,self.pwd,self.host,self.port))
        self.client = MongoClient(host=self.host, port=self.port)

    def close_spider(self, spider):
        """
        爬蟲關閉時執行一次
        """
        self.client.close()

    def process_item(self, item, spider):
        # 操做並進行持久化
        d = dict(item)
        if all(d.values()):
            self.client[self.db][self.table].insert(d)
            print("添加成功一條")

  修改settings.py,在下面增長MongoDB鏈接信息

# MongoDB鏈接信息
HOST="127.0.0.1"
PORT=27017
DB="amazon"  # 數據庫名
TABLE="goods"  # 表名

  同時開啓MongoDB的PipeLine信息,注意這裏開啓後還須要進行修改,咱們pipelines下的名稱是MongodbPipeline

ITEM_PIPELINES = {
   'Amazon.pipelines.MongodbPipeline': 300,
}

此時在cmd下啓動咱們的mongodb(mongod),進入咱們的數據庫(mongo),並自行建立數據庫

執行咱們的bin文件,此時會在你會發現數據都存儲進咱們的數據庫內

2.6 存儲商品信息到本地

  咱們只須要在pipelines.py裏面再添加一個相關的類便可

class FilePipeline(object):

    def __init__(self, file_path):
        self.file_path=file_path

    @classmethod
    def from_crawler(cls, crawler):
        """
        Scrapy會先經過getattr判斷咱們是否自定義了from_crawler,有則調它來完
        成實例化
        """
        file_path = crawler.settings.get('FILE_PATH')
        return cls(file_path)


    def open_spider(self, spider):
        """
        爬蟲剛啓動時執行一次
        """
        print('==============>爬蟲程序剛剛啓動')
        self.fileobj=open(self.file_path,'w',encoding='utf-8')

    def close_spider(self, spider):
        """
        爬蟲關閉時執行一次
        """
        print('==============>爬蟲程序運行完畢')
        self.fileobj.close()

    def process_item(self, item, spider):


        # 操做並進行持久化
        print("items----->",item)
        # return表示會被後續的pipeline繼續處理
        d = dict(item)
        if all(d.values()):

            self.fileobj.write("%s\n" %str(d))

        return item

        # 表示將item丟棄,不會被後續pipeline處理
        # raise DropItem()

  而後再在setting配置文件中配置ITEM_PIPELINES信息以及FILE_PATH信息便可

ITEM_PIPELINES = {
   'DianShang.pipelines.MongodbPipeline': 300,  # 優先級300
   'DianShang.pipelines.FilePipeline': 500,     # 優先級500先執行
}

FILE_PATH="pipe.txt"    # 文件名稱

 2.7 利用代理池進行目標爬取

   這裏主要是考慮目標網站檢測咱們ip問題,通常咱們須要大量爬取相關信息,須要使用代理池(在github上搜索下載),每次更換咱們的ip地址,下載後放入本地,根據readme文件進行相關配置

  這裏還須要使用中間件。從咱們開始的那張圖能夠看到咱們整個流程兩個地方具備中間件:爬取內容的時候通過SpiderMidderware,存儲的時候通過DownerMidderware,須要進行以下操做:

#一、與middlewares.py同級目錄下新建proxy_handle.py
import requests

def get_proxy():
    return requests.get("http://127.0.0.1:5010/get/").text

def delete_proxy(proxy):
    requests.get("http://127.0.0.1:5010/delete/?proxy={}".format(proxy))
    
    

#二、middlewares.py
from Amazon.proxy_handle import get_proxy,delete_proxy

class DownMiddleware1(object):
    def process_request(self, request, spider):
        """
        請求須要被下載時,通過全部下載器中間件的process_request調用
        :param request:
        :param spider:
        :return:
            None,繼續後續中間件去下載;
            Response對象,中止process_request的執行,開始執行process_response
            Request對象,中止中間件的執行,將Request從新調度器
            raise IgnoreRequest異常,中止process_request的執行,開始執行process_exception
        """
        proxy="http://" + get_proxy()
        request.meta['download_timeout']=20
        request.meta["proxy"] = proxy
        print('爲%s 添加代理%s ' % (request.url, proxy),end='')
        print('元數據爲',request.meta)

    def process_response(self, request, response, spider):
        """
        spider處理完成,返回時調用
        :param response:
        :param result:
        :param spider:
        :return:
            Response 對象:轉交給其餘中間件process_response
            Request 對象:中止中間件,request會被從新調度下載
            raise IgnoreRequest 異常:調用Request.errback
        """
        print('返回狀態嗎',response.status)
        return response


    def process_exception(self, request, exception, spider):
        """
        當下載處理器(download handler)或 process_request() (下載中間件)拋出異常
        :param response:
        :param exception:
        :param spider:
        :return:
            None:繼續交給後續中間件處理異常;
            Response對象:中止後續process_exception方法
            Request對象:中止中間件,request將會被從新調用下載
        """
        print('代理%s,訪問%s出現異常:%s' %(request.meta['proxy'],request.url,exception))
        import time
        time.sleep(5)
        delete_proxy(request.meta['proxy'].split("//")[-1])
        request.meta['proxy']='http://'+get_proxy()

        return request

具體操做能夠參考博客

項目最終骨架:

相關文章
相關標籤/搜索