Scrapy框架

介紹

Scrapy是一個基於Twisted的異步處理框架, 是純Python實現的爬蟲框架, 其架構清晰, 模塊之間的耦合程度低, 可擴展性極強, 能夠靈活完成各類需求. 咱們只須要定製開發幾個模塊就能夠輕鬆實現一個爬蟲.
Scrapy依賴twistedpython

安裝

linux下, linux

pip3 install scrapy

目錄結構

scrapy.cfg      項目的主配置信息。(真正爬蟲相關的配置信息在settings.py文件中)
items.py        設置數據存儲模板,用於結構化數據,如:Django的Model
pipelines        數據處理行爲,如:通常結構化的數據持久化
settings.py     配置文件,如:遞歸的層數、併發數,延遲下載等
spiders         爬蟲目錄,如:建立文件,編寫爬蟲規則    

Scrapy主要包括瞭如下組件

引擎(Scrapy)        用來處理整個系統的數據流處理, 觸發事務(框架核心)
調度器(Scheduler)     用來接受引擎發過來的請求, 壓入隊列中, 並在引擎再次請求的時候返回. 能夠想像成一個URL(抓取網頁的網址或者說是連接)的優先隊列, 由它來決定下一個要抓取的網址是什麼, 同時去除重複的網址
下載器(Downloader)     用於下載網頁內容, 並將網頁內容返回給蜘蛛(Scrapy下載器是創建在twisted這個高效的異步模型上的)
爬蟲(Spiders)         爬蟲是主要幹活的, 用於從特定的網頁中提取本身須要的信息, 即所謂的實體(Item)。用戶也能夠從中提取出連接,讓Scrapy繼續抓取下一個頁面
項目管道(Pipeline)     負責處理爬蟲從網頁中抽取的實體,主要的功能是持久化實體、驗證明體的有效性、清除不須要的信息。當頁面被爬蟲解析後,將被髮送到項目管道,並通過幾個特定的次序處理數據。
下載器中間件(Downloader Middlewares)     位於Scrapy引擎和下載器之間的框架,主要是處理Scrapy引擎與下載器之間的請求及響應。
爬蟲中間件(Spider Middlewares)             介於Scrapy引擎和爬蟲之間的框架,主要工做是處理蜘蛛的響應輸入和請求輸出。
調度中間件(Scheduler Middewares)         介於Scrapy引擎和調度之間的中間件,從Scrapy引擎發送到調度的請求和響應。

 

執行流程

1. 【引擎】找到要執行的爬蟲,並執行爬蟲的 start_requests 方法,並的到一個 迭代器。
2. 迭代器循環時會獲取Request對象,而request對象中封裝了要訪問的URL和回調函數。
3. 將全部的request對象(任務)放到【調度器】中,用於之後被【下載器】下載。
4. 【下載器】去【調度器】中獲取要下載任務(就是Request對象),下載完成後執行回調函數。
5. 回到spider的回調函數中
    yield Request()        # 再次發起請求
    yield Item()        # 觸發一個信號去pipeline

一. 簡單使用

1 建立project
    scrapy startproject <項目名稱>

2 建立爬蟲
    cd <項目名稱>
    scrapy genspider <爬蟲名> <網址>        # 如 scrapy genspider chouti chouti.com
    
3 啓動爬蟲
    scrapy crawl <爬蟲名> --nolog        # 不打印日誌

二. 回調函數中經常使用用法

def parse(self, response):        # response, 封裝了響應相關的全部數據
    # 1 響應相關
    # response.text
    # response.encoding
    # response.body
    # response.requeset        # 當前響應時有那個請求發起, 請求中封裝了(要訪問的url, 回調函數)

    # 2 解析相關
    response.xpath('//div[@href="x1"]/a').extract().extract_first()
    response.xpath('//div[@href="x1"]/a/text()').extract()
    tag_list = response.xpath('//div[@href="x1"]/a')
    for tag in tag_list:
        tag.xpath('.//p/text()').extract_first()

    # 3 再次發起請求
    yield Request(url='xx', callback=self.parse)

三. pipeline

當Spider解析完Response以後,Item就會傳遞到Item Pipeline,被定義的Item Pipeline組件會順次調用,完成一連串的處理過程,好比數據清洗、存儲等。redis

Item Pipeline的主要功能有以下4點。數據庫

- 清理HTML數據。
- 驗證爬取數據,檢查爬取字段。
- 查重並丟棄重複內容。
- 將爬取結果保存到數據庫。json

主要的方法

1. process_item(item, spider)。        # 核心方法
    process_item()是必需要實現的方法,它必須返回Item類型的值或者拋出一個DropItem異常。
        返回的是Item對象,那麼此Item會被低優先級的Item Pipeline的process_item()方法處理,直到全部的方法被調用完畢。
        若是它拋出的是DropItem異常,那麼此Item會被丟棄,再也不進行處理。
        
    process_item()方法的參數有以下兩個。
        item,是Item對象,即被處理的Item。
        spider,是Spider對象,即生成該Item的Spider。
    
2. open_spider(spider)。
    在Spider開啓的時候被自動調用的。在這裏咱們能夠作一些初始化操做,如開啓數據庫鏈接等。其中,參數spider就是被開啓的Spider對象。

3. close_spider(spider)。
    在Spider關閉的時候自動調用的。在這裏咱們能夠作一些收尾工做,如關閉數據庫鏈接等。其中,參數spider就是被關閉的Spider對象。

4. from_crawler(cls, crawler)。
    是一個類方法,用@classmethod標識,是一種依賴注入的方式。它的參數是crawler,經過crawler對象,咱們能夠拿到Scrapy的全部核心組件,如全局配置的每一個信息,而後建立一個Pipeline實例。
    
pipeline主要方法

使用

1. 先寫Item類cookie

import scrapy

class TxtItem(scrapy.Item):
    content = scrapy.Field()
    href = scrapy.Field()
items.py

2. 再寫pipeline類架構

# pipeline是全部爬蟲公用的, 給某個爬蟲定製pipeline, 使用spider.name併發

from scrapy.exceptions import DropItem

class TxtPipeline(object):
    """
    源碼內容:
        1. 判斷當前XdbPipeline類中是否有from_crawler
            有:
                obj = XdbPipeline.from_crawler(....)
            否:
                obj = XdbPipeline()
        2. obj.open_spider()

        3. obj.process_item()/obj.process_item()/obj.process_item()/obj.process_item()/obj.process_item()

        4. obj.close_spider()
    """

    def __init__(self, path):
        self.f = None
        self.path = path

    @classmethod
    def from_crawler(cls, crawler):
        # 初始化時, 用於建立pipeline對象
        path = crawler.settings.get('TXTPATH')
        return cls(path)

    def open_spider(self, spider):
        # 爬蟲開始執行
        print('爬蟲開始了')
        self.f = open(self.path, 'a+', encoding='utf-8')

    def process_item(self, item, spider):
        # item爲字典數據格式
        # yield item對象時, 執行
        print('保存在到數據庫中...')
        self.f.write(json.dumps(dict(item), ensure_ascii=False)+'\n')
        # return item        # 交給下一個pipeline的process_item()方法
        raise DropItem()    # 下一個pipeline的process_item方法中斷不執行

    def close_spider(self, spider):
        # 爬蟲結束時執行
        print('爬蟲結束了')
        self.f.close()
pipelines.py

3. 配置pipelineapp

# 能夠配置多個pipeline, 值越小, 優先級越高框架

ITEM_PIPELINES = {
   'scrapy_env.pipelines.TxtPipeline': 300,
}
setting.py

4. 爬蟲中

# yield每執行一次,process_item就調用一次。

yield TxtItem(content=content, href=href)    # 會自動執行pipeline裏的process_item方法
chouti.py

四. url去重規則

原理:  把爬取請求放到調度器的爬取隊列前先去配置文件裏指定的【去重規則類】裏檢驗

1. 系統默認的url去重規則

# dont_filter=False    (聽從去重規則, 默認配置)
# dont_filter=True     (不聽從去重規則)
yield Request(url=page, callback=self.parse, dont_filter=False)    

2. 自定義去重規則

1. 編寫類DupeFilter

from scrapy.dupefilter import BaseDupeFilter
from scrapy.utils.request import request_fingerprint

class CustomizeDupeFilter(BaseDupeFilter):

    """
    自定義的url去重規則
    :return: True表示已經訪問過;False表示未訪問過
    """

    def __init__(self):
        self.visited_fd = set()

    def request_seen(self, request):
        print('去重中')
        fd = request_fingerprint(request)       # url的惟一標示
        if fd in self.visited_fd:
            return True
        self.visited_fd.add(fd)

    def open(self):
        print('引擎開始///')

    def close(self, reason):
        print('引擎結束///')
dupefilter.py

2. 配置

DUPEFILTER_CLASS = 'scrapy_env.dupefilter.CustomizeDupeFilter'

3. 爬蟲中

yield Request(url=page, callback=self.parse)        # 默認是使用去重規則的

五. 控制爬蟲爬取的url深度

1 配置文件中

DEPTH_LIMIT = 4        # 爬取深度爲4的url

# 爬蟲中看一下深度

print(response.meta.get('depth', 0))

六. cookie

方式一:手動攜帶cookie

import scrapy
from scrapy.http.request import Request
from scrapy.http.cookies import CookieJar

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

    cookie_dict = {}

    def parse(self, response):

        # 6.1 去響應頭中獲取cookie, 保存在cookie_jar對象中
        cookie_jar = CookieJar()
        cookie_jar.extract_cookies(response, response.request)

        # 6.2 去cookie_jar對象中將cookie解析到字典中
        for k, v in cookie_jar._cookies.items():
            for i, j in v.items():
                for m, n in j.items():
                    self.cookie_dict[m] = n.value

        print(self.cookie_dict)

        # 6.3 攜帶cookie去登陸
        yield Request(url='https://dig.chouti.com/login',
                      method='post',
                      headers={
                          'content-type': 'application/x-www-form-urlencoded; charset=UTF-8'
                      },
                      body='phone=8618648957737&password=/***/&oneMonth=1',
                      cookies=self.cookie_dict,
                      callback=self.check_result
                      )
                      
    def check_result(self, response):
        print(response.text)
login.py

方式二:經過設置meta自動攜帶cookie

import scrapy
from scrapy.http.request import Request

class ChoutiSpider(scrapy.Spider):
    name = 'chouti'
    allowed_domains = ['chouti.com']
    start_urls = ['http://chouti.com/']
    
    def start_requests(self):
        url = 'http://dig.chouti.com/'
        # 1 設置了meta的cookiejar=True
        yield Request(url=url, callback=self.login, meta={'cookiejar': True})

    def login(self, response):
        # print(response.headers.getlist('Set-Cookie'))
        yield Request(
            url='http://dig.chouti.com/login',
            method='POST',
            headers={'content-type': 'application/x-www-form-urlencoded; charset=UTF-8'},
            body='phone=8618648957737&password=woshiniba&oneMonth=1',
            callback=self.check_result,
            meta={'cookiejar': True}        # 2 會自動攜帶cookie過去
        )

    def check_result(self, response):
        print('檢查結果')
        print(response.text)
login.py

七. scrapy設置代理

內置實現

1 環境變量設置

# 在爬蟲啓動時,提早在os.envrion中設置代理便可。
def start_requests(self):
    import os
    os.environ['HTTPS_PROXY'] = "http://root:woshiniba@192.168.11.11:9999/"
    os.environ['HTTP_PROXY'] = '19.11.2.32'

 

2 經過meta傳參

# 粒度更細
yield Request(url=url,callback=self.parse,meta={'proxy':'"http://root:woshiniba@192.168.11.11:9999/"'})

自定義

3 經過自定義下載中間件

import base64
import random
from six.moves.urllib.parse import unquote
try:
    from urllib2 import _parse_proxy
except ImportError:
    from urllib.request import _parse_proxy
from six.moves.urllib.parse import urlunparse
from scrapy.utils.python import to_bytes

class XdbProxyMiddleware(object):

    def _basic_auth_header(self, username, password):
        user_pass = to_bytes(
            '%s:%s' % (unquote(username), unquote(password)),
            encoding='latin-1')
        return base64.b64encode(user_pass).strip()

    def process_request(self, request, spider):
        PROXIES = [
            "http://root:woshiniba@192.168.11.11:9999/",
            "http://root:woshiniba@192.168.11.12:9999/",
            "http://root:woshiniba@192.168.11.13:9999/",
            "http://root:woshiniba@192.168.11.14:9999/",
            "http://root:woshiniba@192.168.11.15:9999/",
            "http://root:woshiniba@192.168.11.16:9999/",
        ]
        url = random.choice(PROXIES)

        orig_type = ""
        proxy_type, user, password, hostport = _parse_proxy(url)
        proxy_url = urlunparse((proxy_type or orig_type, hostport, '', '', '', ''))

        if user:
            creds = self._basic_auth_header(user, password)
        else:
            creds = None
        request.meta['proxy'] = proxy_url
        if creds:
            request.headers['Proxy-Authorization'] = b'Basic ' + creds
middleware.py

八. 中間件

爬蟲中間件

# 應用: 深度,優先級

1. 寫中間件

class SpiderMiddleware(object):

    def process_spider_input(self,response, spider):
        """
        下載完成,執行,而後交給parse處理
        :param response: 
        :param spider: 
        :return: 
        """
        pass

    def process_spider_output(self,response, result, spider):
        """
        spider處理完成,返回時調用
        :param response:
        :param result:
        :param spider:
        :return: 必須返回包含 Request 或 Item 對象的可迭代對象(iterable)
        """
        return result

    def process_spider_exception(self,response, exception, spider):
        """
        異常調用
        :param response:
        :param exception:
        :param spider:
        :return: None,繼續交給後續中間件處理異常;含 Response 或 Item 的可迭代對象(iterable),交給調度器或pipeline
        """
        return None


    def process_start_requests(self,start_requests, spider):
        """
        爬蟲啓動時調用
        :param start_requests:
        :param spider:
        :return: 包含 Request 對象的可迭代對象
        """
        return start_requests
middleware.py

2. 配置

SPIDER_MIDDLEWARES = {
   # 'xdb.middlewares.XdbSpiderMiddleware': 543,
    'xdb.sd.Sd1': 666,
    'xdb.sd.Sd2': 667,
}

下載中間件

# 應用: 更換代理IP,更換Cookies,更換User-Agent,自動重試。

1. 寫中間件

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
        """
        pass



    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('response1')
        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將會被從新調用下載
        """
        return None
middleware.py

2. 配置

DOWNLOADER_MIDDLEWARES = {
   #'xdb.middlewares.XdbDownloaderMiddleware': 543,
    # 'xdb.proxy.XdbProxyMiddleware':751,
    'xdb.md.Md1':666,
    'xdb.md.Md2':667,
}

九. 執行腳本

1. 單爬蟲運行

import sys
from scrapy.cmdline import execute

if __name__ == '__main__':
    execute(["scrapy","crawl","chouti","--nolog"])
run.py

2. 全部爬蟲

a,在spiders同級建立任意目錄,如:commands
b,在其中建立 crawlall.py 文件 (此處文件名就是自定義的命令)

from scrapy.commands import ScrapyCommand
from scrapy.utils.project import get_project_settings


class Command(ScrapyCommand):

    requires_project = True

    def syntax(self):
        return '[options]'

    def short_desc(self):
        return 'Runs all of the spiders'

    def run(self, args, opts):
        spider_list = self.crawler_process.spiders.list()
        for name in spider_list:
            self.crawler_process.crawl(name, **opts.__dict__)
        self.crawler_process.start()
crawlall.py

c,在settings.py 中添加配置 COMMANDS_MODULE = '項目名稱.目錄名稱'
d,在項目目錄執行命令

scrapy crawlall

十. 自定義擴展

自定義擴展時,利用信號在指定位置註冊制定操做

1. 自定義類

from scrapy import signals


class MyExtension(object):
    def __init__(self, value):
        self.value = value

    @classmethod
    def from_crawler(cls, crawler):
        val = crawler.settings.getint('MMMM')
        ext = cls(val)

        crawler.signals.connect(ext.spider_opened, signal=signals.spider_opened)
        crawler.signals.connect(ext.spider_closed, signal=signals.spider_closed)

        return ext

    def spider_opened(self, spider):
        print('open')

    def spider_closed(self, spider):
        print('close')
ext.py

2. 配置自定義擴展

EXTENSIONS = {
   # 'scrapy.extensions.telnet.TelnetConsole': None,
   'xdb.ext.MyExtend': 666,
}
settings.py

十一. scrapy-redis

scrapy-redis是一個基於redis的scrapy組件,經過它能夠快速實現簡單分佈式爬蟲程序,該組件本質上提供的功能:

1. dupefilter - URL去重規則(被調度器使用)
2. scheduler - 調度器
3. pipeline - 數據持久化
4. 定製起始URL

URL去重規則的三個方案: (推薦方案3)

1. 徹底自定義

from scrapy.dupefilters import BaseDupeFilter

class DdbDuperFiler(BaseDupeFilter):
    """
    基於redis的去重: 1 徹底自定義
    """

    def __init__(self):
        # 第一步: 鏈接redis
        self.conn = Redis(host='127.0.0.1', port='6379')

    def request_seen(self, request):
        """
        第二步: 檢測當前請求是否已經被訪問過
        :return: True表示已經訪問過;False表示未訪問過
        """
        # 方式一   直接將url添加進集合中做爲判斷
        # result = self.conn.sadd('urls', request.url)
        # if result == 1:
        #     return False
        # return True

        # 方式二   先fingerprint再將url添加進集合中做爲判斷
        fd = request_fingerprint(Request(url=request.url))
        result = self.conn.sadd('urls', fd)
        if result == 1:
            return False
        return True
dup.py
DUPEFILTER_CLASS = 'ddb.dup.DdbDuperFiler'
settings.py

2. 使用scrapy-redis

# 鏈接方式1
# REDIS_HOST = '127.0.0.1'
# REDIS_PORT = '6379'
# REDIS_PARAMS  = {'password':'beta'}         # redis鏈接參數
# 鏈接方式2(優先級高於1)
REDIS_URL = 'redis://127.0.0.1:6379'   # 'redis://user:pass@hostname:9001'

REDIS_ENCODING = 'utf-8'
DUPEFILTER_CLASS = 'scrapy_redis.dupefilter.RFPDupeFilter'
DUPEFILTER_KEY = 'dupefilter:%(timestamp)s'     # 這是默認配置
settings.py

3. 繼承scrapy-redis並實現自定義

from scrapy_redis.connection import get_redis_from_settings
from scrapy_redis.dupefilter import RFPDupeFilter
from scrapy_redis import defaults


class RedisDuperFiler(RFPDupeFilter):
    """
    基於redis_redis的去重: 3 實現自定義redis中存儲的鍵的名字
    """

    @classmethod
    def from_settings(cls, settings):
        """Returns an instance from given settings.
        """
        server = get_redis_from_settings(settings)
        # XXX: This creates one-time key. needed to support to use this
        # class as standalone dupefilter with scrapy's default scheduler
        # if scrapy passes spider on open() method this wouldn't be needed
        # TODO: Use SCRAPY_JOB env as default and fallback to timestamp.
        key = defaults.DUPEFILTER_KEY % {'timestamp': 'xiaodongbei'}
        debug = settings.getbool('DUPEFILTER_DEBUG')
        return cls(server, key=key, debug=debug)
dup.py
REDIS_HOST = '127.0.0.1'
REDIS_PORT = '6379'
# REDIS_PARAMS  = {'password':'beta'}         # redis鏈接參數
REDIS_ENCODING = 'utf-8'
DUPEFILTER_CLASS = 'ddb.dup.RedisDuperFiler'
DUPEFILTER_KEY = 'dupefilter:%(timestamp)s'     # 這是默認配置
settings.py

調度器

# scrapy_redis爲咱們實現了配合Queue、DupFilter使用的調度器Scheduler
源碼地址: from scrapy_redis.scheduler import Scheduler
# enqueue_request: 向調度器中添加任務
# next_request: 去調度器中獲取一個任務

優先級隊列時:

DEPTH_PRIORITY = 1        # 廣度優先
DEPTH_PRIORITY = -1        # 深度優先

使用

# 1 鏈接redis配置
REDIS_HOST = '127.0.0.1'
REDIS_PORT = '6379'
REDIS_ENCODING = 'utf-8'
# 2 去重的配置
DUPEFILTER_CLASS = 'scrapy_redis.dupefilter.RFPDupeFilter'
# 3 調度器的配置
SCHEDULER = "scrapy_redis.scheduler.Scheduler"

# 調度器的其餘配置
SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.PriorityQueue'  # 默認使用優先級隊列(默認),其餘:PriorityQueue(有序集合),FifoQueue(列表)、LifoQueue(列表)
SCHEDULER_QUEUE_KEY = '%(spider)s:requests'  # 調度器中請求存放在redis中的key

SCHEDULER_SERIALIZER = "scrapy_redis.picklecompat"  # 對保存到redis中的數據進行序列化,默認使用pickle

SCHEDULER_PERSIST = True  # 是否在關閉時候保留原來的調度器隊列和去重記錄,True=保留,False=清空(默認)
SCHEDULER_FLUSH_ON_START = True  # 是否在開始以前清空 調度器隊列和去重記錄,True=清空,False=不清空(默認)
SCHEDULER_IDLE_BEFORE_CLOSE = 2  # 去調度器隊列中獲取數據時,若是爲空,最多等待時間(最後沒數據,未獲取到)。

SCHEDULER_DUPEFILTER_KEY = '%(spider)s:dupefilter'  # 去重規則,在redis中保存時對應的key
SCHEDULER_DUPEFILTER_CLASS = 'scrapy_redis.dupefilter.RFPDupeFilter'  # 去重規則對應處理的類

起始URL

a. 獲取起始URL時,去集合中獲取仍是去列表中獲取?True,集合;False,列表
    REDIS_START_URLS_AS_SET = False    # 獲取起始URL時,若是爲True,則使用self.server.spop;若是爲False,則使用self.server.lpop
b. 編寫爬蟲時,起始URL從redis的Key中獲取
    REDIS_START_URLS_KEY = '%(name)s:start_urls'
相關文章
相關標籤/搜索