Scrapy框架的使用之Scrapy通用爬蟲

崔慶才丨靜覓發表於2018-05-21

通過Scrapy,我們可以輕鬆地完成一個站點爬蟲的編寫。但如果抓取的站點量非常大,比如爬取各大媒體的新聞資訊,多個Spider則可能包含很多重複程式碼。

如果我們將各個站點的Spider的公共部分保留下來,不同的部分提取出來作為單獨的配置,如爬取規則、頁面解析方式等抽離出來做成一個配置檔案,那麼我們在新增一個爬蟲的時候,只需要實現這些網站的爬取規則和提取規則即可。

本節我們就來探究一下Scrapy通用爬蟲的實現方法。

一、CrawlSpider

在實現通用爬蟲之前,我們需要先了解一下CrawlSpider,其官方文件連結為:http://scrapy.readthedocs.io/en/latest/topics/spiders.html#crawlspider。

CrawlSpider是Scrapy提供的一個通用Spider。在Spider裡,我們可以指定一些爬取規則來實現頁面的提取,這些爬取規則由一個專門的資料結構Rule表示。Rule裡包含提取和跟進頁面的配置,Spider會根據Rule來確定當前頁面中的哪些連結需要繼續爬取、哪些頁面的爬取結果需要用哪個方法解析等。

CrawlSpider繼承自Spider類。除了Spider類的所有方法和屬性,它還提供了一個非常重要的屬性和方法。

  • rules,它是爬取規則屬性,是包含一個或多個Rule物件的列表。每個Rule對爬取網站的動作都做了定義,CrawlSpider會讀取rules的每一個Rule並進行解析。

  • parse_start_url(),它是一個可重寫的方法。當start_urls裡對應的Request得到Response時,該方法被呼叫,它會分析Response並必須返回Item物件或者Request物件。

這裡最重要的內容莫過於Rule的定義了,它的定義和引數如下所示:

class scrapy.contrib.spiders.Rule(link_extractor, callback=None, cb_kwargs=None, follow=None, process_links=None, process_request=None)複製程式碼

下面將依次說明Rule的引數。

  • link_extractor:是Link Extractor物件。通過它,Spider可以知道從爬取的頁面中提取哪些連結。提取出的連結會自動生成Request。它又是一個資料結構,一般常用LxmlLinkExtractor物件作為引數,其定義和引數如下所示:

    class scrapy.linkextractors.lxmlhtml.LxmlLinkExtractor(allow=(), deny=(), allow_domains=(), deny_domains=(), deny_extensions=None, restrict_xpaths=(), restrict_css=(), tags=('a', 'area'), attrs=('href', ), canonicalize=False, unique=True, process_value=None, strip=True)複製程式碼

    allow是一個正規表示式或正規表示式列表,它定義了從當前頁面提取出的連結哪些是符合要求的,只有符合要求的連結才會被跟進。deny則相反。allow_domains定義了符合要求的域名,只有此域名的連結才會被跟進生成新的Request,它相當於域名白名單。deny_domains則相反,相當於域名黑名單。restrict_xpaths定義了從當前頁面中XPath匹配的區域提取連結,其值是XPath表示式或XPath表示式列表。restrict_css定義了從當前頁面中CSS選擇器匹配的區域提取連結,其值是CSS選擇器或CSS選擇器列表。還有一些其他引數代表了提取連結的標籤、是否去重、連結的處理等內容,使用的頻率不高。可以參考文件的引數說明:http://scrapy.readthedocs.io/en/latest/topics/link-extractors.html#module-scrapy.linkextractors.lxmlhtml。

  • callback:即回撥函式,和之前定義Request的callback有相同的意義。每次從link_extractor中獲取到連結時,該函式將會呼叫。該回撥函式接收一個response作為其第一個引數,並返回一個包含Item或Request物件的列表。注意,避免使用parse()作為回撥函式。由於CrawlSpider使用parse()方法來實現其邏輯,如果parse()方法覆蓋了,CrawlSpider將會執行失敗。

  • cb_kwargs:字典,它包含傳遞給回撥函式的引數。

  • follow:布林值,即TrueFalse,它指定根據該規則從response提取的連結是否需要跟進。如果callback引數為Nonefollow預設設定為True,否則預設為False

  • process_links:指定處理函式,從link_extractor中獲取到連結列表時,該函式將會呼叫,它主要用於過濾。

  • process_request:同樣是指定處理函式,根據該Rule提取到每個Request時,該函式都會呼叫,對Request進行處理。該函式必須返回Request或者None

以上內容便是CrawlSpider中的核心Rule的基本用法。但這些內容可能還不足以完成一個CrawlSpider爬蟲。下面我們利用CrawlSpider實現新聞網站的爬取例項,來更好地理解Rule的用法。

二、Item Loader

我們瞭解了利用CrawlSpider的Rule來定義頁面的爬取邏輯,這是可配置化的一部分內容。但是,Rule並沒有對Item的提取方式做規則定義。對於Item的提取,我們需要藉助另一個模組Item Loader來實現。

Item Loader提供一種便捷的機制來幫助我們方便地提取Item。它提供的一系列API可以分析原始資料對Item進行賦值。Item提供的是儲存抓取資料的容器,而Item Loader提供的是填充容器的機制。有了它,資料的提取會變得更加規則化。

Item Loader的API如下所示:

class scrapy.loader.ItemLoader([item, selector, response, ] **kwargs)複製程式碼

Item Loader的API返回一個新的Item Loader來填充給定的Item。如果沒有給出Item,則使用中的類自動例項化default_item_class。另外,它傳入selectorresponse引數來使用選擇器或響應引數例項化。

下面將依次說明Item Loader的API引數。

  • item:它是Item物件,可以呼叫add_xpath()add_css()add_value()等方法來填充Item物件。

  • selector:它是Selector物件,用來提取填充資料的選擇器。

  • response:它是Response物件,用於使用構造選擇器的Response。

一個比較典型的Item Loader例項如下所示:

from scrapy.loader import ItemLoader
from project.items import Product

def parse(self, response):
    loader = ItemLoader(item=Product(), response=response)
    loader.add_xpath('name', '//div[@class="product_name"]')
    loader.add_xpath('name', '//div[@class="product_title"]')
    loader.add_xpath('price', '//p[@id="price"]')
    loader.add_css('stock', 'p#stock]')
    loader.add_value('last_updated', 'today')
    return loader.load_item()複製程式碼

這裡首先宣告一個Product Item,用該ItemResponse物件例項化ItemLoader,呼叫add_xpath()方法把來自兩個不同位置的資料提取出來,分配給name屬性,再用add_xpath()add_css()add_value()等方法對不同屬性依次賦值,最後呼叫load_item()方法實現Item的解析。這種方式比較規則化,我們可以把一些引數和規則單獨提取出來做成配置檔案或存到資料庫,即可實現可配置化。

另外,Item Loader每個欄位中都包含了一個Input Processor(輸入處理器)和一個Output Processor(輸出處理器)。Input Processor收到資料時立刻提取資料,Input Processor的結果被收集起來並且儲存在ItemLoader內,但是不分配給Item。收集到所有的資料後,load_item()方法被呼叫來填充再生成Item物件。在呼叫時會先呼叫Output Processor來處理之前收集到的資料,然後再存入Item中,這樣就生成了Item。

下面將介紹一些內建的的Processor。

1. Identity

Identity是最簡單的Processor,不進行任何處理,直接返回原來的資料。

2. TakeFirst

TakeFirst返回列表的第一個非空值,類似extract_first()的功能,常用作Output Processor,如下所示:

from scrapy.loader.processors import TakeFirst
processor = TakeFirst()
print(processor(['', 1, 2, 3]))複製程式碼

輸出結果如下所示:

1複製程式碼

經過此Processor處理後的結果返回了第一個不為空的值。

3. Join

Join方法相當於字串的join()方法,可以把列表拼合成字串,字串預設使用空格分隔,如下所示:

from scrapy.loader.processors import Join
processor = Join()
print(processor(['one', 'two', 'three']))複製程式碼

輸出結果如下所示:

one two three複製程式碼

它也可以通過引數更改預設的分隔符,例如改成逗號:

from scrapy.loader.processors import Join
processor = Join(',')
print(processor(['one', 'two', 'three']))複製程式碼

執行結果如下所示:

one,two,three複製程式碼

4. Compose

Compose是用給定的多個函式的組合而構造的Processor,每個輸入值被傳遞到第一個函式,其輸出再傳遞到第二個函式,依次類推,直到最後一個函式返回整個處理器的輸出,如下所示:

from scrapy.loader.processors import Compose
processor = Compose(str.upper, lambda s: s.strip())
print(processor(' hello world'))複製程式碼

執行結果如下所示:

HELLO WORLD複製程式碼

在這裡我們構造了一個Compose Processor,傳入一個開頭帶有空格的字串。Compose Processor的引數有兩個:第一個是str.upper,它可以將字母全部轉為大寫;第二個是一個匿名函式,它呼叫strip()方法去除頭尾空白字元。Compose會順次呼叫兩個引數,最後返回結果的字串全部轉化為大寫並且去除了開頭的空格。

5. MapCompose

Compose類似,MapCompose可以迭代處理一個列表輸入值,如下所示:

from scrapy.loader.processors import MapCompose
processor = MapCompose(str.upper, lambda s: s.strip())
print(processor(['Hello', 'World', 'Python']))複製程式碼

執行結果如下所示:

['HELLO', 'WORLD', 'PYTHON']複製程式碼

被處理的內容是一個可迭代物件,MapCompose會將該物件遍歷然後依次處理。

6. SelectJmes

SelectJmes可以查詢JSON,傳入Key,返回查詢所得的Value。不過需要先安裝Jmespath庫才可以使用它,命令如下所示:

pip3 install jmespath複製程式碼

安裝好Jmespath之後,便可以使用這個Processor了,如下所示:

from scrapy.loader.processors import SelectJmes
proc = SelectJmes('foo')
processor = SelectJmes('foo')
print(processor({'foo': 'bar'}))複製程式碼

執行結果如下所示:

bar複製程式碼

以上內容便是一些常用的Processor,在本節的例項中我們會使用Processor來進行資料的處理。

接下來,我們用一個例項來了解Item Loader的用法。

三、本節目標

我們以中華網科技類新聞為例,來了解CrawlSpider和Item Loader的用法,再提取其可配置資訊實現可配置化。官網連結為:http://tech.china.com/。我們需要爬取它的科技類新聞內容,連結為:http://tech.china.com/articles/,頁面如下圖所示。

Scrapy框架的使用之Scrapy通用爬蟲

我們要抓取新聞列表中的所有分頁的新聞詳情,包括標題、正文、時間、來源等資訊。

四、新建專案

首先新建一個Scrapy專案,名為scrapyuniversal,如下所示:

scrapy startproject scrapyuniversal複製程式碼

建立一個CrawlSpider,需要先制定一個模板。我們可以先看看有哪些可用模板,命令如下所示:

scrapy genspider -l複製程式碼

執行結果如下所示:

Available templates:
  basic
  crawl
  csvfeed
  xmlfeed複製程式碼

之前建立Spider的時候,我們預設使用了第一個模板basic。這次要建立CrawlSpider,就需要使用第二個模板crawl,建立命令如下所示:

scrapy genspider -t crawl china tech.china.com複製程式碼

執行之後便會生成一個CrawlSpider,其內容如下所示:

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

class ChinaSpider(CrawlSpider):
    name = 'china'
    allowed_domains = ['tech.china.com']
    start_urls = ['http://tech.china.com/']

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

    def parse_item(self, response):
        i = {}
        #i['domain_id'] = response.xpath('//input[@id="sid"]/@value').extract()
        #i['name'] = response.xpath('//div[@id="name"]').extract()
        #i['description'] = response.xpath('//div[@id="description"]').extract()
        return i複製程式碼

這次生成的Spider內容多了一個rules屬性的定義。Rule的第一個引數是LinkExtractor,就是上文所說的LxmlLinkExtractor,只是名稱不同。同時,預設的回撥函式也不再是parse,而是parse_item

五、定義Rule

要實現新聞的爬取,我們需要做的就是定義好Rule,然後實現解析函式。下面我們就來一步步實現這個過程。

首先將start_urls修改為起始連結,程式碼如下所示:

start_urls = ['http://tech.china.com/articles/']複製程式碼

之後,Spider爬取start_urls裡面的每一個連結。所以這裡第一個爬取的頁面就是我們剛才所定義的連結。得到Response之後,Spider就會根據每一個Rule來提取這個頁面內的超連結,去生成進一步的Request。接下來,我們就需要定義Rule來指定提取哪些連結。

當前頁面如下圖所示。

Scrapy框架的使用之Scrapy通用爬蟲

這是新聞的列表頁,下一步自然就是將列表中的每條新聞詳情的連結提取出來。這裡直接指定這些連結所在區域即可。檢視原始碼,所有連結都在ID為left_side的節點內,具體來說是它內部的classcon_item的節點,如下圖所示。

Scrapy框架的使用之Scrapy通用爬蟲

此處我們可以用LinkExtractorrestrict_xpaths屬性來指定,之後Spider就會從這個區域提取所有的超連結並生成Request。但是,每篇文章的導航中可能還有一些其他的超連結標籤,我們只想把需要的新聞連結提取出來。真正的新聞連結路徑都是以article開頭的,我們用一個正規表示式將其匹配出來再賦值給allow引數即可。另外,這些連結對應的頁面其實就是對應的新聞詳情頁,而我們需要解析的就是新聞的詳情資訊,所以此處還需要指定一個回撥函式callback

到現在我們就可以構造出一個Rule了,程式碼如下所示:

Rule(LinkExtractor(allow='article\/.*\.html', restrict_xpaths='//div[@id="left_side"]//div[@class="con_item"]'), callback='parse_item')複製程式碼

接下來,我們還要讓當前頁面實現分頁功能,所以還需要提取下一頁的連結。分析網頁原始碼之後可以發現下一頁連結是在ID為pageStyle的節點內,如下圖所示。

Scrapy框架的使用之Scrapy通用爬蟲

但是,下一頁節點和其他分頁連結區分度不高,要取出此連結我們可以直接用XPath的文字匹配方式,所以這裡我們直接用LinkExtractorrestrict_xpaths屬性來指定提取的連結即可。另外,我們不需要像新聞詳情頁一樣去提取此分頁連結對應的頁面詳情資訊,也就是不需要生成Item,所以不需要加callback引數。另外這下一頁的頁面如果請求成功了就需要繼續像上述情況一樣分析,所以它還需要加一個follow引數為True,代表繼續跟進匹配分析。其實,follow引數也可以不加,因為當callback為空的時候,follow預設為True。此處Rule定義為如下所示:

Rule(LinkExtractor(restrict_xpaths='//div[@id="pageStyle"]//a[contains(., "下一頁")]'))複製程式碼

所以現在rules就變成了:

rules = (
    Rule(LinkExtractor(allow='article\/.*\.html', restrict_xpaths='//div[@id="left_side"]//div[@class="con_item"]'), callback='parse_item'),
    Rule(LinkExtractor(restrict_xpaths='//div[@id="pageStyle"]//a[contains(., "下一頁")]'))
)複製程式碼

接著我們執行程式碼,命令如下所示:

scrapy crawl china複製程式碼

現在已經實現頁面的翻頁和詳情頁的抓取了,我們僅僅通過定義了兩個Rule即實現了這樣的功能,執行效果如下圖所示。

Scrapy框架的使用之Scrapy通用爬蟲

六、解析頁面

接下來我們需要做的就是解析頁面內容了,將標題、釋出時間、正文、來源提取出來即可。首先定義一個Item,如下所示:

from scrapy import Field, Item

class NewsItem(Item):
    title = Field()
    url = Field()
    text = Field()
    datetime = Field()
    source = Field()
    website = Field()複製程式碼

這裡的欄位分別指新聞標題、連結、正文、釋出時間、來源、站點名稱,其中站點名稱直接賦值為中華網。因為既然是通用爬蟲,肯定還有很多爬蟲也來爬取同樣結構的其他站點的新聞內容,所以需要一個欄位來區分一下站點名稱。

詳情頁的預覽圖如下圖所示。

Scrapy框架的使用之Scrapy通用爬蟲

如果像之前一樣提取內容,就直接呼叫response變數的xpath()css()等方法即可。這裡parse_item()方法的實現如下所示:

def parse_item(self, response):
    item = NewsItem()
    item['title'] = response.xpath('//h1[@id="chan_newsTitle"]/text()').extract_first()
    item['url'] = response.url
    item['text'] = ''.join(response.xpath('//div[@id="chan_newsDetail"]//text()').extract()).strip()
    item['datetime'] = response.xpath('//div[@id="chan_newsInfo"]/text()').re_first('(\d+-\d+-\d+\s\d+:\d+:\d+)')
    item['source'] = response.xpath('//div[@id="chan_newsInfo"]/text()').re_first('來源:(.*)').strip()
    item['website'] = '中華網'
    yield item複製程式碼

這樣我們就把每條新聞的資訊提取形成了一個NewsItem物件。

這時實際上我們就已經完成了Item的提取。再執行一下Spider,如下所示:

scrapy crawl china複製程式碼

輸出內容如下圖所示。

Scrapy框架的使用之Scrapy通用爬蟲

現在我們就可以成功將每條新聞的資訊提取出來。

不過我們發現這種提取方式非常不規整。下面我們再用Item Loader,通過add_xpath()add_css()add_value()等方式實現配置化提取。我們可以改寫parse_item(),如下所示:

def parse_item(self, response):
    loader = ChinaLoader(item=NewsItem(), response=response)
    loader.add_xpath('title', '//h1[@id="chan_newsTitle"]/text()')
    loader.add_value('url', response.url)
    loader.add_xpath('text', '//div[@id="chan_newsDetail"]//text()')
    loader.add_xpath('datetime', '//div[@id="chan_newsInfo"]/text()', re='(\d+-\d+-\d+\s\d+:\d+:\d+)')
    loader.add_xpath('source', '//div[@id="chan_newsInfo"]/text()', re='來源:(.*)')
    loader.add_value('website', '中華網')
    yield loader.load_item()複製程式碼

這裡我們定義了一個ItemLoader的子類,名為ChinaLoader,其實現如下所示:

from scrapy.loader import ItemLoader
from scrapy.loader.processors import TakeFirst, Join, Compose

class NewsLoader(ItemLoader):
    default_output_processor = TakeFirst()

class ChinaLoader(NewsLoader):
    text_out = Compose(Join(), lambda s: s.strip())
    source_out = Compose(Join(), lambda s: s.strip())複製程式碼

ChinaLoader繼承了NewsLoader類,其內定義了一個通用的Out ProcessorTakeFirst,這相當於之前所定義的extract_first()方法的功能。我們在ChinaLoader中定義了text_outsource_out欄位。這裡使用了一個Compose Processor,它有兩個引數:第一個引數Join也是一個Processor,它可以把列表拼合成一個字串;第二個引數是一個匿名函式,可以將字串的頭尾空白字元去掉。經過這一系列處理之後,我們就將列表形式的提取結果轉化為去重頭尾空白字元的字串。

程式碼重新執行,提取效果是完全一樣的。

至此,我們已經實現了爬蟲的半通用化配置。

七、通用配置抽取

為什麼現在只做到了半通用化?如果我們需要擴充套件其他站點,仍然需要建立一個新的CrawlSpider,定義這個站點的Rule,單獨實現parse_item()方法。還有很多程式碼是重複的,如CrawlSpider的變數、方法名幾乎都是一樣的。那麼我們可不可以把多個類似的幾個爬蟲的程式碼共用,把完全不相同的地方抽離出來,做成可配置檔案呢?

當然可以。那我們可以抽離出哪些部分?所有的變數都可以抽取,如nameallowed_domainsstart_urlsrules等。這些變數在CrawlSpider初始化的時候賦值即可。我們就可以新建一個通用的Spider來實現這個功能,命令如下所示:

scrapy genspider -t crawl universal universal複製程式碼

這個全新的Spider名為universal。接下來,我們將剛才所寫的Spider內的屬性抽離出來配置成一個JSON,命名為china.json,放到configs資料夾內,和spiders資料夾並列,程式碼如下所示:

{
  "spider": "universal",
  "website": "中華網科技",
  "type": "新聞",
  "index": "http://tech.china.com/",
  "settings": {
    "USER_AGENT": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36"
  },
  "start_urls": [
    "http://tech.china.com/articles/"
  ],
  "allowed_domains": [
    "tech.china.com"
  ],
  "rules": "china"
}複製程式碼

第一個欄位spider即Spider的名稱,在這裡是universal。後面是站點的描述,比如站點名稱、型別、首頁等。隨後的settings是該Spider特有的settings配置,如果要覆蓋全域性專案,settings.py內的配置可以單獨為其配置。隨後是Spider的一些屬性,如start_urlsallowed_domainsrules等。rules也可以單獨定義成一個rules.py檔案,做成配置檔案,實現Rule的分離,如下所示:

from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import Rule

rules = {
    'china': (
        Rule(LinkExtractor(allow='article\/.*\.html', restrict_xpaths='//div[@id="left_side"]//div[@class="con_item"]'),
             callback='parse_item'),
        Rule(LinkExtractor(restrict_xpaths='//div[@id="pageStyle"]//a[contains(., "下一頁")]'))
    )
}複製程式碼

這樣我們將基本的配置抽取出來。如果要啟動爬蟲,只需要從該配置檔案中讀取然後動態載入到Spider中即可。所以我們需要定義一個讀取該JSON檔案的方法,如下所示:

from os.path import realpath, dirname
import json
def get_config(name):
    path = dirname(realpath(__file__)) + '/configs/' + name + '.json'
    with open(path, 'r', encoding='utf-8') as f:
        return json.loads(f.read())複製程式碼

定義了get_config()方法之後,我們只需要向其傳入JSON配置檔案的名稱即可獲取此JSON配置資訊。隨後我們定義入口檔案run.py,把它放在專案根目錄下,它的作用是啟動Spider,如下所示:

import sys
from scrapy.utils.project import get_project_settings
from scrapyuniversal.spiders.universal import UniversalSpider
from scrapyuniversal.utils import get_config
from scrapy.crawler import CrawlerProcess

def run():
    name = sys.argv[1]
    custom_settings = get_config(name)
    # 爬取使用的Spider名稱
    spider = custom_settings.get('spider', 'universal')
    project_settings = get_project_settings()
    settings = dict(project_settings.copy())
    # 合併配置
    settings.update(custom_settings.get('settings'))
    process = CrawlerProcess(settings)
    # 啟動爬蟲
    process.crawl(spider, **{'name': name})
    process.start()

if __name__ == '__main__':
    run()複製程式碼

執行入口為run()。首先獲取命令列的引數並賦值為namename就是JSON檔案的名稱,其實就是要爬取的目標網站的名稱。我們首先利用get_config()方法,傳入該名稱讀取剛才定義的配置檔案。獲取爬取使用的spider的名稱、配置檔案中的settings配置,然後將獲取到的settings配置和專案全域性的settings配置做了合併。新建一個CrawlerProcess,傳入爬取使用的配置。呼叫crawl()start()方法即可啟動爬取。

universal中,我們新建一個__init__()方法,進行初始化配置,實現如下所示:

from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from scrapyuniversal.utils import get_config
from scrapyuniversal.rules import rules

class UniversalSpider(CrawlSpider):
    name = 'universal'
    def __init__(self, name, *args, **kwargs):
        config = get_config(name)
        self.config = config
        self.rules = rules.get(config.get('rules'))
        self.start_urls = config.get('start_urls')
        self.allowed_domains = config.get('allowed_domains')
        super(UniversalSpider, self).__init__(*args, **kwargs)

    def parse_item(self, response):
        i = {}
        return i複製程式碼

__init__()方法中,start_urlsallowed_domainsrules等屬性被賦值。其中,rules屬性另外讀取了rules.py的配置,這樣就成功實現爬蟲的基礎配置。

接下來,執行如下命令執行爬蟲:

python3 run.py china複製程式碼

程式會首先讀取JSON配置檔案,將配置中的一些屬性賦值給Spider,然後啟動爬取。執行效果完全相同,執行結果如下圖所示。

Scrapy框架的使用之Scrapy通用爬蟲

現在我們已經對Spider的基礎屬性實現了可配置化。剩下的解析部分同樣需要實現可配置化,原來的解析函式如下所示:

def parse_item(self, response):
    loader = ChinaLoader(item=NewsItem(), response=response)
    loader.add_xpath('title', '//h1[@id="chan_newsTitle"]/text()')
    loader.add_value('url', response.url)
    loader.add_xpath('text', '//div[@id="chan_newsDetail"]//text()')
    loader.add_xpath('datetime', '//div[@id="chan_newsInfo"]/text()', re='(\d+-\d+-\d+\s\d+:\d+:\d+)')
    loader.add_xpath('source', '//div[@id="chan_newsInfo"]/text()', re='來源:(.*)')
    loader.add_value('website', '中華網')
    yield loader.load_item()複製程式碼

我們需要將這些配置也抽離出來。這裡的變數主要有Item Loader類的選用、Item類的選用、Item Loader方法引數的定義,我們可以在JSON檔案中新增如下item的配置:

"item": {
  "class": "NewsItem",
  "loader": "ChinaLoader",
  "attrs": {
    "title": [
      {
        "method": "xpath",
        "args": [
          "//h1[@id='chan_newsTitle']/text()"
        ]
      }
    ],
    "url": [
      {
        "method": "attr",
        "args": [
          "url"
        ]
      }
    ],
    "text": [
      {
        "method": "xpath",
        "args": [
          "//div[@id='chan_newsDetail']//text()"
        ]
      }
    ],
    "datetime": [
      {
        "method": "xpath",
        "args": [
          "//div[@id='chan_newsInfo']/text()"
        ],
        "re": "(\\d+-\\d+-\\d+\\s\\d+:\\d+:\\d+)"
      }
    ],
    "source": [
      {
        "method": "xpath",
        "args": [
          "//div[@id='chan_newsInfo']/text()"
        ],
        "re": "來源:(.*)"
      }
    ],
    "website": [
      {
        "method": "value",
        "args": [
          "中華網"
        ]
      }
    ]
  }
}複製程式碼

這裡定義了classloader屬性,它們分別代表Item和Item Loader所使用的類。定義了attrs屬性來定義每個欄位的提取規則,例如,title定義的每一項都包含一個method屬性,它代表使用的提取方法,如xpath即代表呼叫Item Loader的add_xpath()方法。args即引數,就是add_xpath()的第二個引數,即XPath表示式。針對datetime欄位,我們還用了一次正則提取,所以這裡還可以定義一個re引數來傳遞提取時所使用的正規表示式。

我們還要將這些配置之後動態載入到parse_item()方法裡。最後,最重要的就是實現parse_item()方法,如下所示:

 def parse_item(self, response):
    item = self.config.get('item')
    if item:
        cls = eval(item.get('class'))()
        loader = eval(item.get('loader'))(cls, response=response)
        # 動態獲取屬性配置
        for key, value in item.get('attrs').items():
            for extractor in value:
                if extractor.get('method') == 'xpath':
                    loader.add_xpath(key, *extractor.get('args'), **{'re': extractor.get('re')})
                if extractor.get('method') == 'css':
                    loader.add_css(key, *extractor.get('args'), **{'re': extractor.get('re')})
                if extractor.get('method') == 'value':
                    loader.add_value(key, *extractor.get('args'), **{'re': extractor.get('re')})
                if extractor.get('method') == 'attr':
                    loader.add_value(key, getattr(response, *extractor.get('args')))
        yield loader.load_item()複製程式碼

這裡首先獲取Item的配置資訊,然後獲取class的配置,將其初始化,初始化Item Loader,遍歷Item的各個屬性依次進行提取。判斷method欄位,呼叫對應的處理方法進行處理。如methodcss,就呼叫Item Loader的add_css()方法進行提取。所有配置動態載入完畢之後,呼叫load_item()方法將Item提取出來。

重新執行程式,結果如下圖所示。

Scrapy框架的使用之Scrapy通用爬蟲

執行結果是完全相同的。

我們再回過頭看一下start_urls的配置。這裡start_urls只可以配置具體的連結。如果這些連結有100個、1000個,我們總不能將所有的連結全部列出來吧?在某些情況下,start_urls也需要動態配置。我們將start_urls分成兩種,一種是直接配置URL列表,一種是呼叫方法生成,它們分別定義為staticdynamic型別。

本例中的start_urls很明顯是static型別的,所以start_urls配置改寫如下所示:

"start_urls": {
  "type": "static",
  "value": [
    "http://tech.china.com/articles/"
  ]
}複製程式碼

如果start_urls是動態生成的,我們可以呼叫方法傳引數,如下所示:

"start_urls": {
  "type": "dynamic",
  "method": "china",
  "args": [
    5, 10
  ]
}複製程式碼

這裡start_urls定義為dynamic型別,指定方法為urls_china(),然後傳入引數5和10,來生成第5到10頁的連結。這樣我們只需要實現該方法即可,統一新建一個urls.py檔案,如下所示:

def china(start, end):
    for page in range(start, end + 1):
        yield 'http://tech.china.com/articles/index_' + str(page) + '.html'複製程式碼

其他站點可以自行配置。如某些連結需要用到時間戳,加密引數等,均可通過自定義方法實現。

接下來在Spider的__init__()方法中,start_urls的配置改寫如下所示:

from scrapyuniversal import urls

start_urls = config.get('start_urls')
if start_urls:
    if start_urls.get('type') == 'static':
        self.start_urls = start_urls.get('value')
    elif start_urls.get('type') == 'dynamic':
        self.start_urls = list(eval('urls.' + start_urls.get('method'))(*start_urls.get('args', [])))複製程式碼

這裡通過判定start_urls的型別分別進行不同的處理,這樣我們就可以實現start_urls的配置了。

至此,Spider的設定、起始連結、屬性、提取方法都已經實現了全部的可配置化。

綜上所述,整個專案的配置包括如下內容。

  • spider:指定所使用的Spider的名稱。

  • settings:可以專門為Spider定製配置資訊,會覆蓋專案級別的配置。

  • start_urls:指定爬蟲爬取的起始連結。

  • allowed_domains:允許爬取的站點。

  • rules:站點的爬取規則。

  • item:資料的提取規則。

我們實現了Scrapy的通用爬蟲,每個站點只需要修改JSON檔案即可實現自由配置。

八、本節程式碼

本節程式碼地址為:https://github.com/Python3WebSpider/ScrapyUniversal。

九、結語

本節介紹了Scrapy通用爬蟲的實現。我們將所有配置抽離出來,每增加一個爬蟲,就只需要增加一個JSON檔案配置。之後我們只需要維護這些配置檔案即可。如果要更加方便的管理,可以將規則存入資料庫,再對接視覺化管理頁面即可。


本資源首發於崔慶才的個人部落格靜覓: Python3網路爬蟲開發實戰教程 | 靜覓

如想了解更多爬蟲資訊,請關注我的個人微信公眾號:進擊的Coder

weixin.qq.com/r/5zsjOyvEZ… (二維碼自動識別)


相關文章