天天看點

爬蟲入門之Scrapy架構基礎LinkExtractors(十一)

1 parse()方法的工作機制:

1. 因為使用的yield,而不是return。parse函數将會被當做一個生成器使用。scrapy會逐一擷取parse方法中生成的結果,并判斷該結果是一個什麼樣的類型;
2. 如果是request則加入爬取隊列,如果是item類型則使用pipeline處理,其他類型則傳回錯誤資訊。
3. scrapy取到第一部分的request不會立馬就去發送這個request,隻是把這個request放到隊列裡,然後接着從生成器裡擷取;
4. 取盡第一部分的request,然後再擷取第二部分的item,取到item了,就會放到對應的pipeline裡處理;
5. parse()方法作為回調函數(callback)指派給了Request,指定parse()方法來處理這些請求                    scrapy.Request(url, callback=self.parse)
6. Request對象經過排程,執行生成 scrapy.http.response()的響應對象,并送回給parse()方法,直到排程器中沒有Request(遞歸的思路)
7. 取盡之後,parse()工作結束,引擎再根據隊列和pipelines中的内容去執行相應的操作;
8. 程式在取得各個頁面的items前,會先處理完之前所有的request隊列裡的請求,然後再提取items。
7. 這一切的一切,Scrapy引擎和排程器将負責到底。           

2 CrawlSpiders:定義了一些規則跟進link

通過下面的指令可以快速建立 CrawlSpider模闆 的代碼:

scrapy genspider -t crawl tencent tencent.com

上一個案例中,我們通過正規表達式,制作了新的url作為Request請求參數,現在我們可以換個花樣…

class scrapy.spiders.CrawlSpider

它是Spider的派生類,Spider類的設計原則是隻爬取start_url清單中的網頁,而CrawlSpider類定義了一些規則(rule)來提供跟進link的友善的機制,從爬取的網頁中擷取link并繼續爬取的工作更适合。

源碼參考

class CrawlSpider(Spider):
    rules = ()
    def __init__(self, *a, **kw):
        super(CrawlSpider, self).__init__(*a, **kw)
        self._compile_rules()

    #首先調用parse()來處理start_urls中傳回的response對象
    #parse()則将這些response對象傳遞給了_parse_response()函數處理,并設定回調函數為parse_start_url()
    #設定了跟進标志位follow = True
    #parse将傳回item和跟進了的Request對象    
    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

    #從response中抽取符合任一使用者定義'規則'的連結,并構造成Resquest對象傳回
    def _requests_to_follow(self, response):
        if not isinstance(response, HtmlResponse):
            return
        seen = set()
        #抽取之内的所有連結,隻要通過任意一個'規則',即表示合法
        for n, rule in enumerate(self._rules):
            links = [l for l in rule.link_extractor.extract_links(response) if l not in seen]
            #使用使用者指定的process_links處理每個連接配接
            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對象的回調函數
                r = Request(url=link.url, callback=self._response_downloaded)
                r.meta.update(rule=n, link_text=link.text)
               #對每個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):
        #首先判斷是否設定了回調函數 (該回調函數可能是rule中的解析函數,也可能是 parse_start_url函數)
        #如果設定了回調函數(parse_start_url()),那麼首先用parse_start_url()處理response對象,
        #然後再交給process_results處理。傳回cb_res的一個清單
        if callback:
            #如果是parse調用的,則會解析成Request對象
            #如果是rule callback,則會解析成Item
            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, basestring):
                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)


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

CrawlSpider繼承于Spider類,除了繼承過來的屬性外(name、allow_domains),還提供了新的屬性和方法:

3 LinkExtractors

class scrapy.linkextractors.LinkExtractor           

Link Extractors 的目的很簡單: 提取連結。

每個LinkExtractor有唯一的公共方法是 extract_links(),它接收一個 Response 對象,并傳回一個 scrapy.link.Link 對象。

Link Extractors要執行個體化一次,并且 extract_links 方法會根據不同的 response 調用多次提取連結。

class scrapy.linkextractors.LinkExtractor(
    allow = (),
    deny = (),
    allow_domains = (),
    deny_domains = (),
    deny_extensions = None,
    restrict_xpaths = (),
    tags = ('a','area'),
    attrs = ('href'),
    canonicalize = True,
    unique = True,
    process_value = None
)           

主要參數:

  • allow

    :滿足括号中“正規表達式”的值會被提取,如果為空,則全部比對。
  • deny

    :與這個正規表達式(或正規表達式清單)比對的URL一定不提取。
  • allow_domains

    :會被提取的連結的domains。
  • deny_domains

    :一定不會被提取連結的domains。
  • restrict_xpaths

    :使用xpath表達式,和allow共同作用過濾連結。

4 rules:适合全站爬取

在rules中包含一個或多個Rule對象,每個Rule對爬取網站的動作定義了特定操作。如果多個rule比對了相同的連結,則根據規則在本集合中被定義的順序,第一個會被使用。

class scrapy.spiders.Rule(
        link_extractor, 
        callback = None, 
        cb_kwargs = None, 
        follow = None, 
        process_links = None, 
        process_request = None
)           
  • link_extractor

    :是一個Link Extractor對象,用于定義需要提取的連結。
  • callback

    : 從link_extractor中每擷取到連結時,參數所指定的值作為回調函數,該回調函數接受一個response作為其第一個參數。
    注意:當編寫爬蟲規則時,避免使用parse作為回調函數。由于CrawlSpider使用parse方法來實作其邏輯,如果覆寫了 parse方法,crawl spider将會運作失敗。
  • follow

    :是一個布爾(boolean)值,指定了根據該規則從response提取的連結是否需要跟進。 如果callback為None,follow 預設設定為True ,否則預設為False。
  • process_links

    :指定該spider中哪個的函數将會被調用,從link_extractor中擷取到連結清單時将會調用該函數。該方法主要用來過濾。
  • process_request

    :指定該spider中哪個的函數将會被調用, 該規則提取到每個request時都會調用該函數。 (用來過濾request)

5 爬取規則(Crawling rules)

繼續用騰訊招聘為例,給出配合rule使用CrawlSpider的例子:

  1. 首先運作
    scrapy shell "http://hr.tencent.com/position.php?&start=0#a"           
  2. 導入LinkExtractor,建立LinkExtractor執行個體對象。:
    from scrapy.linkextractors import LinkExtractor           
    page_lx = LinkExtractor(allow=(‘position.php?&start=\d+’))
> allow : LinkExtractor對象最重要的參數之一,這是一個正規表達式或正規表達式清單,必須要比對這個正規表達式(或正規表達式清單)的URL才會被提取,如果沒有給出(或為空), 它會比對所有的連結。

   > deny : 用法同allow,隻不過與這個正規表達式比對的URL不會被提取)。它的優先級高于 allow 的參數,如果沒有給出(或None), 将不排除任何連結。

3. 調用LinkExtractor執行個體的extract_links()方法查詢比對結果:
    page_lx.extract_links(response)           
  1. 沒有查到:
    []           
  2. 注意轉義字元的問題,繼續重新比對:
    page_lx = LinkExtractor(allow=('position\.php\?&start=\d+'))
    
    # page_lx = LinkExtractor(allow = ('start=\d+'))
    
    page_lx.extract_links(response)           
## CrawlSpider 版本

那麼,scrapy shell測試完成之後,修改以下代碼

​```python
#提取比對 'http://hr.tencent.com/position.php?&start=\d+'的連結
page_lx = LinkExtractor(allow = ('start=\d+'))

rules = [
    #提取比對,并使用spider的parse方法進行分析;并跟進連結(沒有callback意味着follow預設為True)
    Rule(page_lx, callback = 'parse', follow = True)
]            

這麼寫對嗎?

不對!千萬記住 callback 千萬不能寫 parse,再次強調:由于CrawlSpider使用parse方法來實作其邏輯,是以回調函數必須保證不能與CrawlSpider中parse方法重名 , 如果覆寫了 parse方法,crawl spider将會運作失敗。

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

from  scrapy.spiders import CrawlSpider, Rule  # 提取超連結的規則
from  scrapy.linkextractors import LinkExtractor  # 提取超連結
from Tencent import items

class MytencentSpider(CrawlSpider):
    name = 'myTencent'
    allowed_domains = ['hr.tencent.com']
    start_urls = ['https://hr.tencent.com/position.php?lid=2218&start=0#a']
    page_lx = LinkExtractor(allow=("start=\d+"))
    rules = [
        Rule(page_lx, callback="parseContent", follow=True)
    ]

    # parse(self, response)
    def parseContent(self, response):
        for data in response.xpath("//tr[@class=\"even\"] | //tr[@class=\"odd\"]"):
            item = items.TencentItem()
            item["jobTitle"] = data.xpath("./td[1]/a/text()")[0].extract()
            item["jobLink"] = "https://hr.tencent.com/" + data.xpath("./td[1]/a/@href")[0].extract()
            item["jobCategories"] = data.xpath("./td[1]/a/text()")[0].extract()
            item["number"] = data.xpath("./td[2]/text()")[0].extract()
            item["location"] = data.xpath("./td[3]/text()")[0].extract()
            item["releasetime"] = data.xpath("./td[4]/text()")[0].extract()

            yield item

            # for i in range(1, 200):
            #     newurl = "https://hr.tencent.com/position.php?lid=2218&start=%d#a" % (i*10)
            #     yield scrapy.Request(newurl, callback=self.parse)           

運作:

scrapy crawl tencent

6 robots協定

Robots協定(也稱為爬蟲協定、機器人協定等)的全稱是“網絡爬蟲排除标準”(Robots Exclusion Protocol),網站通過Robots協定告訴搜尋引擎哪些頁面可以抓取,哪些頁面不能抓取。robots.txt檔案是一個文本檔案。當一個搜尋蜘蛛通路一個

站點

時,它會首先

檢查

該站點

根目錄

下是否存在robots.txt,如果存在,搜尋機器人就會按照該檔案中的内容來确定通路的範圍;如果該檔案不存在,所有的搜尋蜘蛛将能夠通路網站上所有沒有被密碼保護的頁面。

User-agent: * 這裡的*代表的所有的搜尋引擎種類,*是一個通配符
Disallow: /admin/ 這裡定義是禁止爬尋admin目錄下面的目錄
Disallow: /require/ 這裡定義是禁止爬尋require目錄下面的目錄
Disallow: /ABC/ 這裡定義是禁止爬尋ABC目錄下面的目錄
Disallow: /cgi-bin/*.htm 禁止通路/cgi-bin/目錄下的所有以".htm"為字尾的URL(包含子目錄)。
Disallow: /*?* 禁止通路網站中所有包含問号 (?) 的網址
Disallow: /.jpg$ 禁止抓取網頁所有的.jpg格式的圖檔
Disallow:/ab/adc.html 禁止爬取ab檔案夾下面的adc.html檔案。
Allow: /cgi-bin/ 這裡定義是允許爬尋cgi-bin目錄下面的目錄
Allow: /tmp 這裡定義是允許爬尋tmp的整個目錄
Allow: .htm$ 僅允許通路以".htm"為字尾的URL。
Allow: .gif$ 允許抓取網頁和gif格式圖檔
Sitemap: 網站地圖 告訴爬蟲這個頁面是網站地圖
           
執行個體分析:淘寶網的 robots.txt檔案           

禁止robots協定将 ROBOTSTXT_OBEY = True改為False

7 Logging

Scrapy提供了log功能,可以通過 logging 子產品使用。

可以修改配置檔案settings.py,任意位置添加下面兩行,效果會清爽很多。
LOG_ENABLED = True  # 開啟
LOG_FILE = "TencentSpider.log" #日志檔案名
LOG_LEVEL = "INFO" #日志級别           

Log levels

  • Scrapy提供5層logging級别:
  • CRITICAL - 嚴重錯誤(critical)
  • ERROR - 一般錯誤(regular errors)
  • WARNING - 警告資訊(warning messages)
  • INFO - 一般資訊(informational messages)
  • DEBUG - 調試資訊(debugging messages)

logging設定

通過在setting.py中進行以下設定可以被用來配置logging:

  1. LOG_ENABLED

    預設: True,啟用logging
  2. LOG_ENCODING

    預設: ‘utf-8’,logging使用的編碼
  3. LOG_FILE

    預設: None,在目前目錄裡建立logging輸出檔案的檔案名
  4. LOG_LEVEL

    預設: ‘DEBUG’,log的最低級别
  5. LOG_STDOUT

    預設: False 如果為 True,程序所有的标準輸出(及錯誤)将會被重定向到log中。例如,執行 print “hello” ,其将會在Scrapy log中顯示。
  6. 日志子產品已經被scrapy棄用,改用python自帶日志子產品
import logging

LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s"  # 設定輸出格式
DATE_FORMAT = "%Y/%m/%d %H:%M:%S"  # 設定時間格式
logging.basicConfig(filename='tianya.log', filemode='a+', format=LOG_FORMAT, datefmt=DATE_FORMAT)

logging.warning('錯誤')           

setting.py 設定抓取間隔

DOWNLOAD_DELAY = 0.25   #設定下載下傳間隔為250ms