1. 程式人生 > >Spider和CrawlSpider的原始碼分析

Spider和CrawlSpider的原始碼分析

一、Spider原始碼分析

在對CrawlSpider進行原始碼分析之前,先對Spider原始碼進行一個分析。

1.1、Spider介紹及主要函式講解

Spider類定義瞭如何爬取某個(或某些)網站。包括了爬取的動作(是否跟進連結)以及如何從網頁的內容中提取結構化資料(提取Item)。 Spider就是定義爬取的動作以及分析某個(或某些)網頁的地方。
Spider是最基本的類,所有爬蟲必須繼承這個類。
Spider類主要用到的函式及呼叫順序為:
1)init()
初始化爬蟲名字和start_urls列表。
重點:這裡爬蟲名稱是必須的,而且必須是唯一的。

    def __init__(self, name=None, **kwargs):
        if name is not None:
            self.name = name
        elif not getattr(self, 'name', None):
            raise ValueError("%s must have a name" % type(self).__name__)
        self.__dict__.update(kwargs)
        if not hasattr(self, 'start_urls'):
            self.start_urls = []

2)start_requests()呼叫make_requests_from_url()
生成Requests物件交給Scrapy下載並返回Response。
重點:該方法只會呼叫一次。

    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)

    def make_requests_from_url(self, url):
        """ This method is deprecated. """
        return Request(url, dont_filter=True)

3)parse()
解析response,並返回Item或Requests(需指定回撥函式)。Item傳給Item pipline持久化,Requests交由Scrapy下載,並由指定的回撥函式處理,一直進行迴圈,直到處理完所有的資料為止。
重點:這個類需要我們自己去實現。並且是預設的Request物件回撥函式,處理返回的response。

    def parse(self, response):
        raise NotImplementedError

1.2、Spider原始碼分析

因為Spider原始碼不是很多,我直接在它的原始碼加上註釋的方式進行講解,如下:

"""
Base class for Scrapy spiders

See documentation in docs/topics/spiders.rst
"""
import logging
import warnings

from scrapy import signals
from scrapy.http import Request
from scrapy.utils.trackref import object_ref
from scrapy.utils.url import url_is_from_spider
from scrapy.utils.deprecate import create_deprecated_class
from scrapy.exceptions import ScrapyDeprecationWarning
from scrapy.utils.deprecate import method_is_overridden

#所有爬蟲的基類,使用者定義的爬蟲必須從這個類繼承
class Spider(object_ref):
    """Base class for scrapy spiders. All spiders must inherit from this
    class.
    """

    #1、定義spider名字的字串。spider的名字定義了Scrapy如何定位(並初始化)spider,所以其必須是唯一的。
    #2、name是spider最重要的屬性,而且是必須的。一般做法是以該網站的域名來命名spider。例如我們在爬取豆瓣讀書爬蟲時使用‘name = "douban_book_spider"’  
    name = None
    custom_settings = None

    #初始化爬蟲名字和start_urls列表。上面已經提到。
    def __init__(self, name=None, **kwargs):
        #初始化爬蟲名字
        if name is not None:
            self.name = name
        elif not getattr(self, 'name', None):
            raise ValueError("%s must have a name" % type(self).__name__)
        self.__dict__.update(kwargs)

        #初始化start_urls列表,當沒有指定的URL時,spider將從該列表中開始進行爬取。 因此,第一個被獲取到的頁面的URL將是該列表之一,後續的URL將會從獲取到的資料中提取。  
        if not hasattr(self, 'start_urls'):
            self.start_urls = []

    @property
    def logger(self):
        logger = logging.getLogger(self.name)
        return logging.LoggerAdapter(logger, {'spider': self})

    def log(self, message, level=logging.DEBUG, **kw):
        """Log the given message at the given log level

        This helper wraps a log call to the logger within the spider, but you
        can use it directly (e.g. Spider.logger.info('msg')) or use any other
        Python logger too.
        """
        self.logger.log(level, message, **kw)

    @classmethod
    def from_crawler(cls, crawler, *args, **kwargs):
        spider = cls(*args, **kwargs)
        spider._set_crawler(crawler)
        return spider

    def set_crawler(self, crawler):
        warnings.warn("set_crawler is deprecated, instantiate and bound the "
                      "spider to this crawler with from_crawler method "
                      "instead.",
                      category=ScrapyDeprecationWarning, stacklevel=2)
        assert not hasattr(self, 'crawler'), "Spider already bounded to a " \
                                             "crawler"
        self._set_crawler(crawler)

    def _set_crawler(self, crawler):
        self.crawler = crawler
        self.settings = crawler.settings
        crawler.signals.connect(self.close, signals.spider_closed)

    #該方法將讀取start_urls列表內的地址,為每一個地址生成一個Request物件,並返回這些物件的迭代器。
    #注意:該方法只會呼叫一次。
    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)

    #1、start_requests()中呼叫,實際生成Request的函式。
    #2、Request物件預設的回撥函式為parse(),提交的方式為get。
    def make_requests_from_url(self, url):
        """ This method is deprecated. """
        return Request(url, dont_filter=True)

    #預設的Request物件回撥函式,處理返回的response。  
    #生成Item或者Request物件。這個類需要我們自己去實現。
    def parse(self, response):
        raise NotImplementedError

    @classmethod
    def update_settings(cls, settings):
        settings.setdict(cls.custom_settings or {}, priority='spider')

    @classmethod
    def handles_request(cls, request):
        return url_is_from_spider(request.url, cls)

    @staticmethod
    def close(spider, reason):
        closed = getattr(spider, 'closed', None)
        if callable(closed):
            return closed(reason)

    def __str__(self):
        return "<%s %r at 0x%0x>" % (type(self).__name__, self.name, id(self))

    __repr__ = __str__


BaseSpider = create_deprecated_class('BaseSpider', Spider)


class ObsoleteClass(object):
    def __init__(self, message):
        self.message = message

    def __getattr__(self, name):
        raise AttributeError(self.message)

spiders = ObsoleteClass(
    '"from scrapy.spider import spiders" no longer works - use '
    '"from scrapy.spiderloader import SpiderLoader" and instantiate '
    'it with your project settings"'
)

# Top-level imports
from scrapy.spiders.crawl import CrawlSpider, Rule
from scrapy.spiders.feed import XMLFeedSpider, CSVFeedSpider
from scrapy.spiders.sitemap import SitemapSpider

二、CrawlSpider原始碼分析

講解完Spider原始碼分析之後,我再來對CrawlSpider的原始碼進行一個分析。

2.1、CrawlSpider介紹及主要函式講解

CrawlSpider是爬取一般網站常用的spider。它定義了一些規則(rule)來提供跟進link的方便的機制。也許這個spider並不是完全適合特定網站或專案,但它對很多情況都使用。
因此我們可以在它的基礎上,根據需求修改部分方法。當然我們也可以實現自己的spider。除了從Spider繼承過來的(必須提供的)屬性外,它還提供了一個新的屬性:
1)rules
一個包含一個(或多個)Rule物件的集合(list)。 每個Rule對爬取網站的動作定義了特定表現。如果多個Rule匹配了相同的連結,則根據他們在本屬性中被定義的順序,第一個會被使用。
使用方式案例如下:

rules = (
    # 提取匹配 'category.php' (但不匹配 'subsection.php') 的連結並跟進連結(沒有callback意味著follow預設為True)
    Rule(LinkExtractor(allow=('category\.php', ), deny=('subsection\.php', ))),

    # 提取匹配 'item.php' 的連結並使用spider的parse_item方法進行分析
    Rule(LinkExtractor(allow=('item\.php', )), callback='parse_item',  follow=True),
)

2)parse_start_url(response)
當start_url的請求返回時,該方法被呼叫。 該方法分析最初的返回值並必須返回一個Item物件或者一個Request物件或者一個可迭代的包含二者物件。
該spider方法需要使用者自己重寫。

def parse_start_url(self, response):  
    return []  

3)parse(),一定不要重寫這個方法
通過上面的介紹,我們知道Spider中的parse()方法是需要我們重寫的,如下:

def parse(self, response):
        raise NotImplementedError

但是,CrawlSpider中的parse()方法在原始碼中已經實現了一些功能,如下:

def parse(self, response):
        return self._parse_response(response, self.parse_start_url, cb_kwargs={}, follow=True)

所以我們在使用CrawlSpider時,一定一定不要去重寫parse()函式(重點)。可以使用Rule(LinkExtractor(allow=('item\.php', )), callback='parse_item', follow=True)中的callback去指定需要跳轉的parse。
例如我們在爬蟲課堂(二十五)|使用CrawlSpider、LinkExtractors、Rule進行全站爬取中講解簡書全站爬取的時候使用方法,如下:

class JianshuCrawl(CrawlSpider):
    name = "jianshu_spider_crawl"
    # 可選,加上會有一個爬取的範圍
    allowed_domains = ["jianshu.com"]
    start_urls = ['https://www.jianshu.com/']

    # response中提取連結的匹配規則,得出符合條件的連結
    pattern = '.*jianshu.com/u/*.'
    pagelink = LinkExtractor(allow=pattern)

    # 可以寫多個rule規則
    rules = [
        # 只要符合匹配規則,在rule中都會發送請求,同時呼叫回撥函式處理響應。
        # rule就是批量處理請求。
        Rule(pagelink, callback='parse_item', follow=True),
    ]

    # 不能寫parse方法,因為原始碼中已經有了,會覆蓋導致程式不能跑
    def parse_item(self, response):
        for each in response.xpath("//div[@class='main-top']"):
    ......

callback='parse_item'(這裡的parse_item是字串)指定了跳轉的函式def parse_item(self, response)

2.2、CrawlSpider原始碼分析

同樣的,因為CrawlSpider原始碼不是很多,我直接在它的原始碼加上註釋的方式進行講解,如下:

class CrawlSpider(Spider):

    rules = ()

    def __init__(self, *a, **kw):
        super(CrawlSpider, self).__init__(*a, **kw)
        self._compile_rules()

    #1、首先呼叫parse()方法來處理start_urls中返回的response物件。
    #2、parse()將這些response物件傳遞給了_parse_response()函式處理,並設定回撥函式為parse_start_url()。
    #3、設定了跟進標誌位True,即follow=True。
    #4、返回response。
    def parse(self, response):
        return self._parse_response(response, self.parse_start_url, cb_kwargs={}, follow=True)

    #處理start_url中返回的response,需要重寫。
    def parse_start_url(self, response):
        return []

    def process_results(self, response, results):
        return results

    def _build_request(self, rule, link):
         #構造Request物件,並將Rule規則中定義的回撥函式作為這個Request物件的回撥函式。這個‘_build_request’函式在下面呼叫。
        r = Request(url=link.url, callback=self._response_downloaded)
        r.meta.update(rule=rule, link_text=link.text)
        return r

    #從response中抽取符合任一使用者定義'規則'的連結,並構造成Resquest物件返回。
    def _requests_to_follow(self, response):
        if not isinstance(response, HtmlResponse):
            return
        seen = set()
        #抽取所有連結,只要通過任意一個'規則',即表示合法。 
        for n, rule in enumerate(self._rules):
            links = [lnk for lnk in rule.link_extractor.extract_links(response)
                     if lnk not in seen]
            if links and rule.process_links:
                links = rule.process_links(links)
            #將連結加入seen集合,為每個連結生成Request物件,並設定回撥函式為_repsonse_downloaded()。
            for link in links:
                seen.add(link)
                #構造Request物件,並將Rule規則中定義的回撥函式作為這個Request物件的回撥函式。這個‘_build_request’函式在上面定義。
                r = self._build_request(n, link)
                #對每個Request呼叫process_request()函式。該函式預設為indentify,即不做任何處理,直接返回該Request。
                yield rule.process_request(r)

    #處理通過rule提取出的連線,並返回item以及request。
    def _response_downloaded(self, response):
        rule = self._rules[response.meta['rule']]
        return self._parse_response(response, rule.callback, rule.cb_kwargs, rule.follow)

    #解析response物件,使用callback解析處理他,並返回request或Item物件。
    def _parse_response(self, response, callback, cb_kwargs, follow=True):
        #1、首先判斷是否設定了回撥函式。(該回調函式可能是rule中的解析函式,也可能是 parse_start_url函式)  
        #2、如果設定了回撥函式(parse_start_url()),那麼首先用parse_start_url()處理response物件,  
        #3、然後再交給process_results處理。返回cb_res的一個列表。  
        if callback:
            cb_res = callback(response, **cb_kwargs) or ()
            cb_res = self.process_results(response, cb_res)
            for requests_or_item in iterate_spider_output(cb_res):
                yield requests_or_item

        #如果需要跟進,那麼使用定義的Rule規則提取並返回這些Request物件。
        if follow and self._follow_links:
            #返回每個Request物件。
            for request_or_item in self._requests_to_follow(response):
                yield request_or_item

    def _compile_rules(self):
        def get_method(method):
            if callable(method):
                return method
            elif isinstance(method, six.string_types):
                return getattr(self, method, None)

        self._rules = [copy.copy(r) for r in self.rules]
        for rule in self._rules:
            rule.callback = get_method(rule.callback)
            rule.process_links = get_method(rule.process_links)
            rule.process_request = get_method(rule.process_request)

    @classmethod
    def from_crawler(cls, crawler, *args, **kwargs):
        spider = super(CrawlSpider, cls).from_crawler(crawler, *args, **kwargs)
        spider._follow_links = crawler.settings.getbool(
            'CRAWLSPIDER_FOLLOW_LINKS', True)
        return spider

    def set_crawler(self, crawler):
        super(CrawlSpider, self).set_crawler(crawler)
        self._follow_links = crawler.settings.getbool('CRAWLSPIDER_FOLLOW_LINKS', True)

參考資料:scrapy官網(官方對這塊講的不多,但是其他我這邊沒有講的內容還有很多,真的需要好好看看官網),CSDN上的兩篇Scrapy原始碼分析的文章。



作者:小怪聊職場
連結:https://www.jianshu.com/p/d492adf17312
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。

相關推薦

SpiderCrawlSpider原始碼分析

一、Spider原始碼分析 在對CrawlSpider進行原始碼分析之前,先對Spider原始碼進行一個分析。 1.1、Spider介紹及主要函式講解 Spider類定義瞭如何爬取某個(或某些)網站。包括了爬取的動作(是否跟進連結)以及如何從網頁的內容中提取結構化資料(

原始碼】防抖節流原始碼分析

前言 防抖、節流主要用於頻繁事件觸發,例如滑鼠移動、改變視窗大小等。lodash等函式庫具備相對應的api, _.debounce 、_.throttle。 核心技術:閉包。 區別: 防抖, 連續觸發, 第一次和最後一次觸發有效 節流, 一段時間內僅觸發一次(第一次) 本文以防抖函式為

python3 整數型別PyLongObject PyObject原始碼分析

python3 整數型別PyLongObject 和PyObject原始碼分析一 測試環境介紹和準備測試環境:作業系統:windows10Python版本:3.7.0 下載地址VS版本:vs2015社群版(免費) 下載地址win10SDK(安裝vs2015是可以選擇,如果沒有安裝則需要獨立安裝)http://

資料結構(三)StackVector原始碼分析

一、基本概念: 1、棧是什麼? 是一個只能在某一端進行插入、刪除操作的線性表。 * 從棧頂插入一個元素稱之為入棧(push) * 從棧頂刪除一個元素稱之為出棧(pop) 2、圖解: 3、棧的實現: 鏈式儲存(連結串列) 順序儲存(陣列) 4

30.以太坊原始碼分析(30)eth-bloombitsfilter原始碼分析

以太坊的布隆過濾器 以太坊的區塊頭中包含了一個叫做logsBloom的區域。 這個區域儲存了當前區塊中所有的收據的日誌的布隆過濾器,一共是2048個bit。也就是256個位元組。 而我們的一個交易的收據包含了很多的日誌記錄。 每個日誌記錄包含了 合約的地址, 多個Topic。 而在我

ComparableComparator原始碼分析與對比

Comparable使用  Comparable只是一個簡單的介面, public interface Comparable<T> { public int compareTo(T o); } 使用如下: public class Person imp

javaIO(4):Reader,InputStreamReaderFileReader原始碼分析

前言 前面把OutputStream,InputStream和Writer體系講了,同時也講了“裝飾者模式”在IO體系中的應用。Reader體系跟前面的很相似。本文就將最後一個Reader體系給講了。 正文 一,Reader原始碼 package

javaIO(1):OutputStreamFileOutputStream原始碼分析及“裝飾者模式”在IO中的應用

前言 一,IO體系 從現在起,我們將基於JDK1.8詳細介紹java.io包中的關於輸入輸出有關的類。瞭解過這個包的都知道,裡面的類繼承關係錯綜複雜,光是弄清楚這些類的關係就夠喝一壺的了。說實話,我也沒有什麼好的方法來一下子就能弄清這些類,但是如果你瞭解“裝

Java集合框架之Map---HashMapLinkedHashMap原始碼分析

1、HashMap概述:    HashMap是基於雜湊表的Map介面的非同步實現。此實現提供所有可選的對映操作,並允許使用null值和null鍵。此類不保證對映的順序,特別是它不保證該順序恆久不變。 2、HashMap的資料結構 資料結構中有陣列和連結串列來實現對資料的

interface_castasBinder原始碼分析

研究Android底層程式碼時,尤其是Binder跨程序通訊時,經常會發現interface_cast和asBinder,很容易被這兩個函式繞暈,下面通過分析原始碼來講解一下: interface_cast 下面根據下述ICameraClient例子進行分析

Window windowManager原始碼分析

所有需要顯示在螢幕上的內容都要通過windowManager windowManager 是一個介面 三個方法 add remove update 他的具體實現類是windowManagerImpl類(橋接模式) 具體的實現是交給了windowManag

以太坊原始碼分析(30)eth-bloombitsfilter原始碼分析

## 以太坊的布隆過濾器以太坊的區塊頭中包含了一個叫做logsBloom的區域。 這個區域儲存了當前區塊中所有的收據的日誌的布隆過濾器,一共是2048個bit。也就是256個位元組。而我們的一個交易的收據包含了很多的日誌記錄。 每個日誌記錄包含了 合約的地址, 多個Topic。 而在我們的收據中也存在一個布隆

android中windowwindowManager原始碼分析(android-api-23)

一、前言 在android中window無處不在,如activity、dialog、toast等。它是view所依附的載體,每個window都對應於一個View和一個ViewRootImpl。ViewRootImpl就是Window和view的連線紐帶。windowMana

Scrapy框架-SpiderCrawlSpider的區別

sta don .com num 鏈接 links pan src () 目錄 1.目標 2.方法1:通過Spider爬取 3. 通過CrawlSpider爬取

Android SettingsSettingsProvider原始碼分析與修改

StringACCESSIBILITY_DISPLAY_INVERSION_ENABLEDSetting that specifies whether display color inversion is enabled. StringACCESSIBILITY_ENABLEDIf accessibili

Mybatis原始碼分析(4)—— Mapper的建立獲取

Mybatis我們一般都是和Spring一起使用的,它們是怎麼融合到一起的,又各自發揮了什麼作用? 就拿這個Mapper來說,我們定義了一個介面,聲明瞭一個方法,然後對應的xml寫了這個sql語句, 它怎麼就執行成功了?這傢伙是怎麼實現的,帶著這個好奇心,我一步步跟蹤,慢慢揭開了它的

NSQ原始碼分析(四)——inFlightPqueuePriorityQueue優先順序佇列

   在Channel結構體中用到了兩種優先順序佇列pqueue.PriorityQueue和inFlightPqueue。 deferredMessages map[MessageID]*pqueue.Item deferredPQ pqueue.Priorit

Android原始碼分析之為什麼在onCreate() onResume() 獲取不到 View 的寬高

轉載自:https://www.jianshu.com/p/d7ab114ac1f7 先來看一段很熟悉的程式碼,可能在最開始接觸安卓的時候,大部分人都寫過的一段程式碼;即嘗試在 onCreate() 和 onResume() 方法中去獲取某個 View 的寬高資訊: 但是列印輸出後,我們會發

Android IntentService用法原始碼分析

關於IntentService的介紹,我個人覺得還是先看官方描述比較好: IntentService is a base class for Services that handle asynchronous requests (expressed as Intents) o

zigbee 之ZStack-2.5.1a原始碼分析(三)無線資料傳送接收

前面說過SampleApp_Init和SampleApp_ProcessEvent是我們重點關注的函式,接下來分析無線傳送和接收相關的程式碼: 在SampleApp_ProcessEvent函式中: if ( events & SYS_EVENT_MSG ) {  &nbs