1. 程式人生 > >Scrapy爬蟲實戰 CrawlSpider和Item Loader的使用

Scrapy爬蟲實戰 CrawlSpider和Item Loader的使用

網站:

https://tech.china.com/articles/

建立專案:

scrapy startproject scrapyuniversal
 

之前建立專案,都用scrapy genspider +爬蟲名字+域名的方式,此次要建立CrawlSpider需要使用crawl,建立命令:

scrapy genspider -t crawl china tech.china.com
 

在專案開始之前,要先來了解一下LinkExtractor,

rules = (
    Rule(LinkExtractor(allow=r'Items/'), callback='parse_item', follow=True),
)
allow是一個正則表示式或者列表,定義了從當前頁面提取的連結哪些是符合要求的。
callback回撥函式,每次從link_extractor中獲取到連結時,被呼叫。接收一個response,返回一個包含Item或者Request物件的列表。

注意:callback中避免屬於parse()作為回撥函式

follow指定根據該規則提取的連結是否需要跟進,如果callback引數為None,follow預設 為True。否則為False。

定義Rule:

Spider會根據每一個Rule來提取這個頁面內的超連結,生成Request.

檢視原始碼:

 

可以發現所有的資訊都在這個節點內,用正則表示式將文章連結都匹配出來賦值給allow引數。

rules = (
    Rule(LinkExtractor(allow='article\/.*\.html',restrict_xpaths='//div[@id="left_side"]//div[@class="con_item"]'),
         callback='parse_item',
         follow=True),
)

然後找下一頁的連結:

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

解析頁面:

先定義欄位:

from scrapy import Field, Item

class NewsItem(Item):
    title = Field()
    text = Field()
    datetime = Field()
    source = Field()
    url = Field()
    #站點名稱,區分不同的站點
    website = Field()

獲取資料:

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

執行之後獲取的結果如下:

用Item Loader,通過

add_xpath()
add_value()
add_css()

實現配置化提取。

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()
from scrapy.loader import ItemLoader
from scrapy.loader.processors import TakeFirst, Join, Compose

定義類:
class NewsLoader(ItemLoader):
    #定義TakeFirst(),相當於extract_first()方法
    default_output_processor = TakeFirst()


class ChinaLoader(NewsLoader):
    #Compose兩個引數
    # Join()也是一個Processor,可以把列表拼合成一個字串
    # lambda可以將頭尾空白符去掉
    text_out = Compose(Join(), lambda s: s.strip())
    source_out = Compose(Join(), lambda s: s.strip())

通用配置的抽取

scrapy genspider -t crawl universal universal
新建一個spider,將上文寫的Spider內的屬性抽取出來配置成一個JSON,放到config.json中:

{
    "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": {
    "type": "dynamic",
    "method": "china",
    "args": [
      5,
      10
    ]
  },
  "allowed_domains": [
    "tech.china.com"
  ],
  "rules": "china"
}

這樣的話,要啟動爬蟲,僅僅需要從配置檔案中讀取後然後載入到Spider中即可,讀取方法程式碼如下:

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())

此時,我們只需要傳入JSON配置檔案 的名稱,就可以獲取配置資訊,入口檔案程式碼如下:

from scrapy.crawler import CrawlerProcess

def run():
    # Sys.argv[ ]其實就是一個列表,裡邊的項為使用者輸入的引數,關鍵就是要明白這引數是從程式外部輸入的,而非程式碼本身的什麼地方。
    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配置和專案全域性的settings配置做合併
    settings.update(custom_settings.get('settings'))
    process = CrawlerProcess(settings)
    # 啟動
    process.crawl(spider, **{'name': name})
    process.start()


if __name__ == '__main__':
    run()

解析資料的通用配置,程式碼如下:

# -*- coding: utf-8 -*-
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from scrapyuniversal.items import *
from scrapyuniversal.loaders import *
from scrapyuniversal.utils import get_config
from scrapyuniversal import urls
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'))
        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':
                # eval() 輸出是輸入的型別
                self.start_urls = list(eval('urls.' + start_urls.get('method'))(*start_urls.get('args', [])))
        self.allowed_domains = config.get('allowed_domains')
        super(UniversalSpider, self).__init__(*args, **kwargs)

    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()

python run.py china 執行。

值得一提的是,引入問題還有執行問題。

引入的時候,會出現一些引入錯誤,這個需要了解相對引入和絕對引入的一些知識。

在執行的時候,使用命令列執行,會出現一系列的細節問題,不過還好,查查資料都可以解決。