Flask作為一個在Python領(lǐng)域較為出名的web框架,其頁面構(gòu)建采用了一種Python語法糖——修飾器,剛開始看到的時候,覺得Django簡直是反Python之禪之大成!然后就火急火燎研究了一下修飾器的相關(guān)知識,瞬間覺得平時隨手寫的爬蟲可以更加DRY(don't repeat yourself),開坑之后發(fā)現(xiàn),這里面的坑還真深,所以容我寫一篇博客來裝逼。代碼比較長,所以放在了Github上。
鋪墊
首先我們需要想一個經(jīng)典的爬蟲應(yīng)用,然后再開始實現(xiàn),然后瞬間就想到了各種爬蟲入門都使用的妹子圖例子(我為什么會瞬間想到?我明明很單純的)。這個例子很簡單,先打開一個頁面,然后解析出所有圖片的鏈接,最后利用鏈接保存圖片。這個例子網(wǎng)上很多,隨便從簡書找了一個,可以看到,這個例子也進(jìn)行了函數(shù)的抽提以達(dá)到簡化代碼結(jié)構(gòu)和復(fù)用的功效。但是這些重復(fù)的過程可能你在寫下一個爬蟲的時候還是會再寫一次(如果你又一次引用了請別戳破,我只是覺得很少人會這樣做,包括我),所以如果能夠提取成一個庫,那么這些工作就可以一勞永逸了。
開始構(gòu)想
修飾器,其實際作用是將一個函數(shù)作為參數(shù)傳入某個函數(shù)進(jìn)行修飾,然后返回新的函數(shù),此時再調(diào)用該函數(shù),就是新的被修飾過的函數(shù)了。但是這樣的理解不太適合于我們進(jìn)行設(shè)計,打個不知道是否合適的比方,修飾器所需要傳入的函數(shù)其實是大白胸前的那張卡,如果沒有這張卡,大白就是不完整的,無法運(yùn)行,但是這張卡插入了,大白就完整了,而且這張卡還決定了大白的屬性。如此這般抽象到我們的想法當(dāng)中,妹子圖類似的爬蟲里面,請求頁面,保存圖片這些操作都是一樣的,就像大白充氣的身體。而唯一不同的就是解析頁面這個部分,可能這個網(wǎng)站的妹子圖的鏈接在一個class的img標(biāo)簽內(nèi),但是另一個網(wǎng)站的妹子圖在另一個class的img標(biāo)簽內(nèi),而這個解析的過程抽象出的函數(shù),就是修飾器需要修飾的方法,即大白需要插入的卡,可能是紅卡,可能是綠卡。來一張腦圖

綠色的框框表示每次都是一樣的操作,可以抽提為修飾器,而黃色的部分則是每次都不一樣而需要修改的部分。
如果還不能理解,類比Flask框架,接收用戶請求這部分可以看作我們這里請求頁面這部分,而給用戶返回結(jié)果的部分相當(dāng)與我們保存圖片這部分,中間唯一需要我們寫的生成頁面的部分就是我們這里的解析圖片鏈接的部分,如果還不能理解,咳咳,直接上代碼吧!
碼代碼
仔細(xì)想了想,我還是決定使用自頂向下的方法來講一下這個代碼,假使我們已經(jīng)創(chuàng)建了一個我們理想中的爬蟲框架,我們將其命名為spidry,其具有修飾器saveimages(類似Flask里面的app.route這樣的東西)。
那么爬蟲寫出來如下:
# -*- coding:utf-8 -*-
"""
@author: yangmqglobe
@file: test.py
@time: 2016/11/28
"""
from spidry import saveimages
from spidry import response as resp
import os
@saveimages(feature='json', sleep=3)# 使用修飾器修飾解析方法
def bilibili():
# 解析方法,生成包含需要保存圖片url和路徑的字典列表
iconlist = [{'url': icon['icon'],
'path': 'icon/'+icon['title']+'.gif'}
for icon in resp.json['fix']]
return iconlist
if __name__ == '__main__':
if not os.path.exists("icon"):
os.makedirs("icon")
# 調(diào)用被修飾的方法!
bilibili("http://www.bilibili.com/index/index-icon.json")
print("done!")
如果寫過Flask應(yīng)用的童鞋應(yīng)該對這樣的語法應(yīng)用不會很陌生,這里的response對象就是我們這個框架自動根據(jù)請求頁面生成的請求返回對象,已經(jīng)自動根據(jù)參數(shù)解析,類似Flask里面的session對象之類的。為了體現(xiàn)我等當(dāng)代青年的高尚追求,這里我們用了一個其他的例子,下載B站右上角的動圖,這段url會返回一個json,里面記錄了所有動圖的名稱和地址,網(wǎng)站顯示時使用一段js代碼隨機(jī)抽出一個顯示,這里我們?nèi)肯螺d。feature參數(shù)指定我們需要如何解析返回的數(shù)據(jù),這里設(shè)置為json,sleep參數(shù)為每下載一張圖片暫停的時間,更多的參數(shù)我們在代碼實現(xiàn)中自然會看到,這里暫且不提。
運(yùn)行之:
fetch:http://www.bilibili.com/index/index-icon.json
save:icon/羽生結(jié)弦.gif
save:icon/僵尸.gif
save:icon/困.gif
save:icon/南瓜燈.gif
...此處省略..
save:icon/233333.gif
done!
然后在icon文件夾下就出現(xiàn)了所有的鬼畜小動圖!
看到這里,是不是覺得這個框架會讓爬蟲變得非常簡單,寫起來就是那么自然、體貼、干爽、透氣,獨有的速效凹道和完美的吸收軌跡,讓你再也不用為每個月的那幾天感到焦慮和不安,再加上貼心的護(hù)翼設(shè)計,量多也不用當(dāng)心。對不起,我調(diào)皮了(雞汁地盜了一段話)。
修飾器類這樣實現(xiàn)
當(dāng)然啦,最重要還是如何實現(xiàn)修飾器,關(guān)于修飾器的基礎(chǔ)知識,這里不再造輪子,大家可以去這里看這篇文章,我認(rèn)為是講得比較清楚也比較全的一篇。直接上代碼:
# -*- coding:utf-8 -*-
"""
@author: yangmqglobe
@file: saveimages.py
@time: 2016/11/29
"""
from bs4 import BeautifulSoup
from functools import wraps
from .spidry import response
import requests
import time
class saveimages:
"""
修飾器類
"""
def __init__(self,
feature='html',
method='get',
sleep=0,
log=True,
**kwargs):
"""
構(gòu)造方法,初始化各種參數(shù)
:param feature: 解析請求數(shù)據(jù)的方法,暫時分為html的soup和json
:param method: 請求圖片的方法
:param sleep: 保存圖片時每張圖片的請求時間間隔
:param log: 是否打印log
:param kwargs: 其他的關(guān)鍵詞參數(shù),與requests庫的參數(shù)相關(guān)
"""
self.feature = feature
self.method = method
self.sleep = sleep
self.log = log
self.kwargs = kwargs
def __call__(self, fn):
"""
類被作為修飾器調(diào)用時調(diào)用方法
:param fn:傳入的圖片鏈接解析函數(shù)
:return:
"""
@wraps(fn)
def wrapper(url, method='get', **kwargs):
"""
修飾后的函數(shù)的實現(xiàn)
:param url: 需要請求的頁面地址
:param method: 請求的方法
:param kwargs: 其他請求參數(shù)
"""
self._fetchpage(url, method, **kwargs)
imglist = fn() # 調(diào)用原始方法,獲得圖片列表
for img in imglist: # 循環(huán)保存圖片
self.saveimage(img)
return wrapper
def _fetchpage(self, url, method, **kwargs):
"""
請求頁面并解析為相應(yīng)的解析對象
:param url:請求頁面的url
:param method:請求方法
:param kwargs:其他請求嘗試
"""
if self.log:
print('fetch:' + url)
response.r = requests.request(method, url, **kwargs)
response.text = response.r.text
if self.feature.lower() == 'html': # 將結(jié)果解析為soup
response.soup = BeautifulSoup(response.text, 'lxml')
response.json = None
elif self.feature.lower() == 'json': # 將結(jié)果解析為json
response.json = response.r.json()
response.soup = None
def saveimage(self, img):
"""
保存圖片函數(shù)
:param img: 包含圖片url和保存路徑的字典
"""
url = img['url']
path = img['path']
r = requests.request(self.method, url, **self.kwargs)
with open(path, 'wb') as img:
img.write(r.content)
if self.log:
print('save:' + path)
time.sleep(self.sleep)
這個沒什么好說的,幾乎就是修飾器的內(nèi)容,但是這里值得一提的是這里的respone對象,也是我們最終爬蟲代碼時調(diào)用的那個對象,這個對象實現(xiàn)起來其實也并不簡單。
不簡單的全局對象
前面說到了,這個response對象并不簡單,我們在使用Flask的時候,你可能會引入session或者request對象,大致使用如下:
from flask import session, request
name = session['name']
name = request.name
那么這個看似是一個全局變量的東西是如何定位到每次的的請求對象的?我們的這個response對象又該如何實現(xiàn)呢?
第一想法,使用全局對象,但是有一個問題,就是使用起來非常的麻煩,每次均需要聲明其為globe,且其是靜態(tài)的!然而Flask的對象并不是這樣,這又是為什么呢?所以還是找了一圈資料,如果想深究,建議直接看Flask的源碼,簡單點的,建議看這篇博客,講得不是很清楚,但是沒有啥講得更清楚的貌似!這里總結(jié)一下,flask的這個對象其實是使用了werkzeug庫的LocalStack類,該類是標(biāo)準(zhǔn)庫中threading包中的local類的一種封裝,至于local類的使用,可以看看這篇博客或者直接去看文檔,其實際是一個線程唯一類,在同一線程中能夠共享一些動態(tài)對象。這里我們也進(jìn)行一些封裝,實現(xiàn)如下:
# -*- coding:utf-8 -*-
"""
@author: yangmqglobe
@file: spidry.py
@time: 2016/11/29
"""
from threading import local
from requests.models import Response
from bs4 import BeautifulSoup
class Spidry(local):
def __init__(self):
self.soup = BeautifulSoup(features='lxml')
self.json = {}
self.r = Response()
self.text = ''
response = Spidry()
可以看到,我們是否進(jìn)行封裝是無所謂的,只需要實例化local類的對象,我們就可以往里面塞各種對象同時進(jìn)行共享,但是這里我還是進(jìn)行了封裝同時還假實例化了各個變量對應(yīng)的類,只是為了在后面引用時能夠獲得代碼提示而已,就這么簡單卻人性化,你來打我呀!
更多的擴(kuò)展
雖然至此,我們最初的想法是已經(jīng)成功了,但是如果只是能夠進(jìn)行圖片保存,那么是否有點無趣了呢?其次,對于異常的處理,更多的參數(shù)設(shè)置,都還有待完善!其實這只是一種思路,我們還可以再繼續(xù)添加將數(shù)據(jù)保存至數(shù)據(jù)庫或文件,自動翻頁等等修飾器,其次,還可以對現(xiàn)有的修飾器進(jìn)行細(xì)分使其通用性更強(qiáng),比如分開打開頁面與保存圖片的裝飾器,以修飾器嵌套的方式來實現(xiàn),這樣代碼的復(fù)用性將會更高!
整個坑從想法、搜集資料、編寫各種模塊的測試Demo到正式開坑進(jìn)行編寫最后寫下這篇博客,在一邊上課一邊各種作業(yè)轟炸的夾縫中折騰了大約2周,總之收獲很多,希望自己以后還能有各種各樣類似的奇思妙想來繼續(xù)折騰吧!至于開源的倉庫,萬一腦充血了,我可能就會更新,也歡迎pull request!