我與Scrapy的初次相識,理論+實戰入門Scrapy

和Scrapy接觸不久,做一個項目學習並記錄一下,這個代碼倒是寫了有段時間了,一直沒來寫博客,這爬蟲集合的更新也耽誤好久了。隨着疫情的好轉,我這也恢復正常寫博文(糊臉,疫情不是自己不寫博文的理由),大家一起加油呀,加油加油,一起都已經好起來了。
實戰項目是爬取簡書網(https://www.jianshu.com/) 二級頁面信息的Scrapy項目,這也就個入門,大佬看見了一定請指點一下。

一、我對Scrapy的一些淺顯的理解

Scrapy就是個爬蟲框架,它像個房子的鋼筋混凝土框架一樣,使得我們可以在這個框架裏自由發揮,我們只需要在該安裝“門”的地方裝上“門”(做填空一樣),接下來就可以打開爬蟲的大門了,十分的便捷。

我這些淺顯的話還是要搭一些科普,不然就太沒營養了,我從百*百科cv大法了這個圖,下面就我的理解解釋一下這圖中五大部件:Scrapy Engine(引擎)、Scheduler(調度器)、Downloader(下載器)、Spider(爬蟲)、Item Pipeline(管道),及兩個中間件:Downloader Middlewares(下載中間件)、Spider Middlewares(爬蟲中間件)。
在這裏插入圖片描述

1.1、五大部件

Scrapy Engine(引擎):顧名思義,看到引擎,我就想到了汽車的發動機,這肯定是一個十分十分重要的東西。引擎關聯着其他的所有部件,從圖裏也看出,引擎起到一個信號塔的作用,傳遞着各部件之間的信息、數據等。

Scheduler(調度器):調度器調度器,調度兩個字尤其突出,那我們可以想到調度些什麼呢?調度爬蟲下載的請求,它會將從引擎傳來的請求加入隊列當中,當引擎需要的時候再給回引擎。

Downloader(下載器):下載器,啊哈,簡單,就是下載網頁嘛,不就是爬蟲了嘛,錯錯錯,這個下載器不是爬蟲,Scrapy分工十分明確的。下載器是按照引擎給的網頁請求,下載網頁的內容然後返回給引擎,由引擎交給爬蟲,它就專門下載網頁,也是後面Spider代碼裏面的response的由來。

Spider(爬蟲):這裏就比較熟悉了,自己編寫爬蟲邏輯,進行網頁的解析跳轉等等操作,我們使用Scrapy絕大功夫都在這裏了。

Item Pipeline(管道):這個啊,就是對Spider獲取到的數據進行一些處理,包括(清洗,過濾,儲存等等)。

1.2、兩個中間件

Downloader Middlewares(下載中間件):可以在這裏設置下載的請求頭,下載的時間間隔,代理等等操作,在一定程度上使Spider更加純粹了。

Spider Middlewares(爬蟲中間件):看圖它位於引擎和spider之間,而經由這條線路上的是request請求和response請求的返回還有items爬取結果(就那三根綠線),這個中間件意味可以自己定義擴展request、response和items。

1.3、項目簡說

前面講的五大部件,兩個中間件都太理論了,感覺Scrapy離我們還是有點遠,下面就簡單講一下項目的知識,後面還有實戰。

1、先要安裝呢,我這裏不介紹普通python安裝Scrapy,太複雜了,要安裝好多庫,我用的是Anaconda,特別方便,在終端cmd輸入conda install scrapy就行,還是有不明白的可以參考Anaconda按照Scrapy

2、安裝好後,輸入這條指令,會在你輸入指令的那個目錄下生成自定義項目名的Scrapy項目。

scrapy startproject 項目名

比如這是我在testScrapy目錄下執行了scrapy startproject study後指定生成的目錄,沒有任何改動下,我們就是這麼多文件。
在這裏插入圖片描述
3、而且它會提示你cd到項目目錄下,輸入指令生成一個spider文件。
在這裏插入圖片描述
這裏我解釋一下genspider後接的參數,第一個就是spider的名字,第二個是該爬蟲的限定網域,以保證不會爬到別的網頁去。

scrapy genspider test www.baidu.com

4、到上面我們這個框架算是完備了,看一下完整的目錄。
(這是我的理解,有錯誤望指點一下)我們要需要修改的也就是test.py(Spider)、item.py、pipelines.py、settings.py。
在這裏插入圖片描述
而我們完成Scrapy項目大體的步驟如下,在後面實戰項目中也有所體現:

  1. 新建爬蟲項目
  2. 分析網頁確定爬取的內容,修改items.py
  3. 編寫spider爬蟲,準備開始爬取網頁了
  4. 編寫pipelines.py,看看數據怎麼處理(Scrapy也自帶了數據處理)

好了,理論大概都講完了,下面來個項目感覺一下,在項目裏繼續學習基礎知識。

二、Scrapy簡書網項目

1、思路步驟

生成Scrapy項目

scrapy startproject jianshu

生成Spider

scrapy genspider jianshuwang jianshu.com

在這裏插入圖片描述

1.1、分析網頁

打開開發者工具看看,這是第一頁的url。
在這裏插入圖片描述
因爲這個網頁沒有具體的分頁,滑動側邊進度條刷新出了新的內容且網頁也沒變,所以它是個異步加載,去看一下XHR文件。
在這裏插入圖片描述
我這複製下來了,方便大家看,可以看出它是由多個seen_snote_ids%5B%5D和一個page參數拼接成的,也就是說後面的網頁都可以這樣來請求,可是seen_snote_ids%5B%5D這個哪來的呢?
https://www.jianshu.com/?seen_snote_ids%5B%5D=60076928&seen_snote_ids%5B%5D=63913898&seen_snote_ids%5B%5D=59873546&seen_snote_ids%5B%5D=63405402&seen_snote_ids%5B%5D=59679766&seen_snote_ids%5B%5D=60717313&seen_snote_ids%5B%5D=62921759&page=2
我們隨機取了其中一個全局查找了一下,發現這個id就是文章的data-note-id,ok了,這個問題解決了。
在這裏插入圖片描述

1.2、解析網頁

我本來是想爬這些信息的,但考慮到meta的參數還是比較常用的,想分享給大家,於是改爬取詳情頁的信息(後面那張截圖)。
在這裏插入圖片描述
在這裏插入圖片描述

1.3、編寫item.py

這個文件會給你一個默認的模板,很方便,跟着修改就行,下面是我們需要的信息。

from scrapy import Item, Field

class JianshuItem(Item):
    title_url = Field()  # 標題鏈接
    title = Field()  # 標題

    author = Field()  # 作者 
    author_url = Field()  # 作者主頁鏈接

    content = Field()  # 文章內容
    like = Field()  # 點贊數

    # time = Field()  # 發佈時間
    # word_number = Field()  # 文章字數
    # reading_volume = Field()  # 閱讀量

1.4、編寫Spider

我們先獲取每個文章的url,分析網頁可以發現每個文章都是在一個個的li標籤內,那先把li解析出來,在Scrapy中,是直接利用xpath解析response,這個response也就是下載器下載下來的網頁(之前有提到,大家沒忘記吧)。

這裏也可以直接用xpath解析文章的url然後用extract()返回一個url的列表,能夠理解的可以自己改改,這裏爲了演示的易懂些。

item = JianshuItem()

li_xpath = '//li'
for li in response.xpath(li_xpath):
     # 標題鏈接
     item['title_url'] = 'https://www.jianshu.com' + li.xpath('div/a/@href').extract_first()

這裏要講到這個xpath後面的extract_first(),xpath的語法是沒有改變的,可是最後面要跟上extract_first()或者extract(),extract_first() 它會返回xpath解析出的第一個內容,
extract() 會返回一個列表,看具體需求。

那我們有了url,就可以爬取詳情頁了,這裏涉及的知識感覺還是蠻多的,上個代碼看看。

item['title_url'] = 'https://www.jianshu.com' + li.xpath('div/a/@href').extract_first()
yield scrapy.Request(url=item['title_url'], headers=self.headers, meta={'item': copy.deepcopy(item)}, callback=self.donwnload_content)

哐哐,敲黑板,重點來了,記筆記,一個個來。
scrapy.Request,可以使得你在一個Reuqest裏調用另一個Request,這裏和我們以前寫的普通爬蟲爬取二級頁面的原理感覺差不多。url這個參數是必須的,而callback是回調一個函數,就是說這個新的Request將由這個回調函數執行(不要加括號,會報錯的)。

meta,這個參數可以以字典的形式將裏面的數據等傳遞給別的Request請求中,我這裏是在一級界面的Request請求中獲取了文章的url放在item裏,然後通過meta這個參數將item傳到爬取二級頁面的Request請求函數中,最後將獲取完所有數據的item一起返回。

這裏出現了一個問題:item傳過去後,值總是一個,後面知道了是因爲使用 Request 函數傳遞 item 時,使用的是淺複製(對象的字段值被複制時,字段引用的對象不會被複制,所以這裏導入copy模塊,使用深拷貝copy.deepcopy

yield,可以把它當作一個return,只不過它是一個迭代生成器,每次返回一次,return結束後是直接結束的,而yield結束後會接上之前的代碼,可以參考這篇python中yield的用法詳解——最簡單,最清晰的解釋,收益匪淺。


接着着思路來,這裏要講donwnload_content(),獲取二級頁面的函數。
這裏沒什麼,大體都是解析網頁,值得吐槽的是使用xpath無法獲取到作者名還有它的鏈接等信息,我只好用正則匹配返回回來的網頁了。

 # 獲取詳情頁信息
def donwnload_content(self, response):
    # 接送傳來的item
    item = response.meta['item']
    
    # 獲取返回的html
    html = response.body.decode('utf-8')
    # 標題
    item['title'] = response.xpath('//*[@id="__next"]/div[1]/div/div/section[1]/h1/text()').extract_first()

    # 作者主頁鏈接
    item['author_url'] = 'https://www.jianshu.com' + re.findall('<a class="qzhJKO" href="(.*?)"><span', html, re.S)[0]

    # 作者名
    item['author'] = re.findall('<span class="_22gUMi">(.*?)</span></a>', html, re.S)[0]

    # 文章內容
    content = re.findall('<article class="_2rhmJa">(.*?)</article>', html, re.S)[0]
    item['content'] = re.sub('<.*?>', '', re.sub('</p>', '\n', content))

    # 點贊數
    like = re.findall('aria-label="查看點贊列表">(.*?)</span', html, re.S)[0]
    item['like'] = re.sub('<!-- -->|,', '', like)

    # 返回item,這裏會結束這個函數,接回到之前調用這個函數的地方,回憶一下之前調用這個函數使用的是yield。
    yield item

等for循環結束,這裏爬取完一頁的了所有文章的信息了,接着要翻頁了。

解析出那些文章的id,獲取id作爲下一頁URL的參數。

data_note_id = response.xpath('//li/@data-note-id').extract()

拼接上url。

for data_id in data_note_id:
       url += 'seen_snote_ids%5B%5D={}&'.format(data_id)
       url += 'page={}'.format(self.page)

有了url後就是使用Request方法回調parse函數,這裏本來是可以自動根據start_urls這個url列表爬取的,而我們進行修改了,所以還是使用yield的方法進行操作。

# 測試就爬取五頁,自行修改
if self.page < 5:
      yield scrapy.Request(url=url, headers=self.headers, callback=self.parse)

至此,爬蟲項目應該就結束了,我們在終端輸入以下指令(依舊是要在這個Scrapy項目的目錄下)使用默認的數據存儲,將數據存到csv文件中。

scrapy crawl jianshuwang -o jianshu.csv

但是我們查看後發現爬取的文章有重複,按照想法,url中的id參數就可以避免重複了,看來是天真了。
查閱資料,要加上cookie值能解決這個問題,所以我這裏使用了start_request(),自定義請求頭,也可以去settings文件裏設置。

start_request()是Spider的默認函數,初始會自動調用,可以用於一些網站一進去就要登入的操作,使用這個這個函數配合FormRequest實現表單登入。

def start_requests(self):
     for url in self.start_urls:
         yield scrapy.Request(url, headers=self.headers, callback=self.parse)

好了,啥問題都解決了,可以正常爬取了。

1.5、編寫pipelines.py

接下來就是對數據的存儲了,這裏使用pipeline管道,我們對數據存儲到MongoDB中,一通百通,學會一種其他的就可以模仿着來了。

import pymongo


class JianshuPipeline(object):
    def __init__(self):
        cilent = pymongo.MongoClient('localhost', 27017)
        mydb = cilent['mydb']
        self.post = mydb['jianshuwang']
        
    def process_item(self, item, spider):
        info = dict(item)
        self.post.insert(info)
        return item

此時還應該在settings文件中指定pipeline管理。

ITEM_PIPELINES = {
   'jianshu.pipelines.JianshuPipeline': 300,
}

接下來就正常的執行,在終端裏輸入。

scrapy crawl jianshuwang

看看下面的截圖,是不是很興奮呢,我每每看到爬取成功的結果,都十分開心呢,你們也快動手做起來吧。
在這裏插入圖片描述

看完應該可以寫些簡單的Scrapy的爬蟲了,講的不好,還請多多指正。大家一起加油呀,有問題的話,大家在評論區裏留言或者私信我都行呀,沒問題也可以嘮嘮嗑哈。

2、項目代碼

Item

最後面註釋的三個沒有爬到,是個遺憾。scrapy的xpath解析不到,re正則表達式也匹配不到,看了請求回來的網頁裏沒有這三個,不知道靠什麼加載的,異步加載的XHR文件也沒有這個數據,下次可以嘗試seleinum來爬取。

from scrapy import Item, Field

class JianshuItem(Item):
    title_url = Field()  # 標題鏈接
    title = Field()  # 標題

    author = Field()  # 作者
    author_url = Field()  # 作者主頁鏈接

    content = Field()  # 文章內容
    like = Field()  # 點贊數

    # time = Field()  # 發佈時間
    # word_number = Field()  # 文章字數
    # reading_volume = Field()  # 閱讀量

Pipelines

import pymongo


class JianshuPipeline(object):
    def __init__(self):
        cilent = pymongo.MongoClient('localhost', 27017)
        mydb = cilent['mydb']
        self.post = mydb['jianshuwang']
        
    def process_item(self, item, spider):
        info = dict(item)
        self.post.insert(info)
        return item

settings

它默認了好多設置,我講幾個我這改過的。
在這裏插入圖片描述

# 控制檯的打印等級,默認的會輸出好多東西
# CRITICAL - 嚴重錯誤(critical)
# ERROR - 一般錯誤(regular errors)
# WARNING - 警告信息(warning messages)
# INFO - 一般信息(informational messages)
# DEBUG - 調試信息(debugging messages)
LOG_LEVEL = 'WARNING'

# 睡眠時間 也就是爬取的時間間隔,我就爬取了幾頁,不會對網站的服務器產生什麼壓力,如果是大量的爬取建議這裏寫大一點,開個玩笑,要是一不小心把別人網站搞崩了,小心會有免費衣服和食物還會給配一個亮閃閃的大鏈子。
DOWNLOAD_DELAY = 0.5

# 設置爬取時規避robots協議
ROBOTSTXT_OBEY = False

# 設置管道管理,如果存在多個管道,後面的數值可以區分先後進行
ITEM_PIPELINES = {
   'jianshu.pipelines.JianshuPipeline': 300,
}

# 下面這幾個 我在這個項目裏沒有在settings文件裏設置。
# 可以設置user-agent,也就是模擬瀏覽器或者其他什麼的
USER_AGENT = 'jianshu (+http://www.yourdomain.com)'

# 設置請求頭
DEFAULT_REQUEST_HEADERS = {
   'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
   'Accept-Language': 'en',
}

Spider

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

from jianshu.items import JianshuItem


class JianshuwangSpider(scrapy.Spider):
    name = 'jianshuwang'
    allowed_domains = ['jianshu.com']
    start_urls = ['https://www.jianshu.com/']
    page = 1
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36",
        "X-INFINITESCROLL": "true",
        "X-Requested-With": "XMLHttpRequest",
        "Cookie": "這裏放自己的Cookie,可以是不登入簡書賬號的cookie值"
    }

    def start_requests(self):
        for url in self.start_urls:
            yield scrapy.Request(url, headers=self.headers, callback=self.parse)

    def parse(self, response):
        # 獲取信息
        item = JianshuItem()

        li_xpath = '//li'
        for li in response.xpath(li_xpath):
            # 標題鏈接
            item['title_url'] = 'https://www.jianshu.com' + li.xpath('div/a/@href').extract_first()
            yield scrapy.Request(url=item['title_url'], headers=self.headers, meta={'item': copy.deepcopy(item)}, callback=self.donwnload_content)

	    # 獲取數據的id作爲下一頁URL的參數
        data_note_id = response.xpath('//li/@data-note-id').extract()
        
        self.page += 1

        url = 'https://www.jianshu.com/?'

        for data_id in data_note_id:
            url += 'seen_snote_ids%5B%5D={}&'.format(data_id)
        url += 'page={}'.format(self.page)

        if self.page < 5:
            yield scrapy.Request(url=url, headers=self.headers, callback=self.parse)

    # 獲取詳情頁信息
    def donwnload_content(self, response):
        # 接送傳來的item
        item = response.meta['item']
        
        html = response.body.decode('utf-8')
        # 標題
        item['title'] = response.xpath('//*[@id="__next"]/div[1]/div/div/section[1]/h1/text()').extract_first()

        # 作者主頁鏈接
        item['author_url'] = 'https://www.jianshu.com' + re.findall('<a class="qzhJKO" href="(.*?)"><span', html, re.S)[0]

        # 作者名
        item['author'] = re.findall('<span class="_22gUMi">(.*?)</span></a>', html, re.S)[0]

        # 文章內容
        content = re.findall('<article class="_2rhmJa">(.*?)</article>', html, re.S)[0]
        item['content'] = re.sub('<.*?>', '', re.sub('</p>', '\n', content))

        # 點贊數
        like = re.findall('aria-label="查看點贊列表">(.*?)</span', html, re.S)[0]
        item['like'] = re.sub('<!-- -->|,', '', like)

		# 返回item
        yield item
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章