和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項目大體的步驟如下,在後面實戰項目中也有所體現:
- 新建爬蟲項目
- 分析網頁確定爬取的內容,修改items.py
- 編寫spider爬蟲,準備開始爬取網頁了
- 編寫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