python動(dòng)態(tài)網(wǎng)站爬蟲實(shí)戰(zhàn)(requests+xpath+demjson+redis)
之前簡單學(xué)習(xí)過python爬蟲基礎(chǔ)知識(shí),并且用過scrapy框架爬取數(shù)據(jù),都是直接能用xpath定位到目標(biāo)區(qū)域然后爬取??蛇@次碰到的需求是爬取一個(gè)用asp.net編寫的教育網(wǎng)站并且將教學(xué)ppt一次性爬取下來,由于該網(wǎng)站部分內(nèi)容渲染采用了js,所以比較難用xpath直接定位,同時(shí)發(fā)起下載ppt的請求比較難找。
經(jīng)過琢磨和嘗試后爬取成功,記錄整個(gè)爬取思路供自己和大家學(xué)習(xí)。文章比較詳細(xì),對于一些工具包和相關(guān)函數(shù)的使用會(huì)在源代碼或正文中添加注釋來介紹簡單相關(guān)知識(shí)點(diǎn),如果某些地方看不懂可以通過注釋及時(shí)去查閱簡單了解,然后繼續(xù)閱讀。(尾部有源代碼,全文僅對一些敏感的個(gè)人信息數(shù)據(jù)進(jìn)行了省略。)
一、主要思路
1、觀察網(wǎng)站
研究從進(jìn)入網(wǎng)站到成功下載資源需要幾次url跳轉(zhuǎn)。
先進(jìn)入目標(biāo)網(wǎng)站首頁,依次點(diǎn)擊教材->選擇初中->選擇教輔->選擇學(xué)科->xxx->資源列表->點(diǎn)擊下載ppt。
目標(biāo)網(wǎng)站首頁
資源列表
資源詳情頁
分析url每步跳轉(zhuǎn)以及資源下載是否需要cookie等header信息。
通過一步步跳轉(zhuǎn)進(jìn)入到最終的資源詳情頁,最終點(diǎn)擊下載資源按鈕時(shí)網(wǎng)站提示并且跳轉(zhuǎn)到了登陸頁面,說明發(fā)起下載的請求可能需要攜帶cookie等頭部信息。
2、編寫爬蟲代碼
- 登陸賬戶,獲取到識(shí)別用戶的cookies
- 請求資源列表頁面,定位獲得左側(cè)目錄每一章的跳轉(zhuǎn)url。
- 請求每個(gè)跳轉(zhuǎn)url,定位資源列表頁面右側(cè)下載資源按鈕的url請求(注意2、3步是圖資源列表)
- 發(fā)起url請求,進(jìn)入資源詳情頁,定位獲得下載資源按鈕的url請求(第4步是圖資源詳情頁)
- 發(fā)起請求,將下載的資源數(shù)據(jù)寫入文件。
這是本次爬蟲實(shí)戰(zhàn)編寫代碼的大致思路,具體每次步驟碰到的難點(diǎn)以及如何解決在接下來的實(shí)戰(zhàn)介紹中會(huì)進(jìn)行詳細(xì)分析。
二、爬蟲實(shí)戰(zhàn)
1、登陸獲取cookie
首先網(wǎng)站登陸,獲取到cookie和user-agent,作為之后請求的頭部。設(shè)置全局變量HEADER,方便調(diào)用
HEADER = { 'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36(KHTML, like Gecko)Chrome/93.0.4577.63 Safari/537.36", 'Cookie':"xxxxxxx", }
2、請求資源列表頁面,定位獲得左側(cè)目錄每一章的跳轉(zhuǎn)url(難點(diǎn))
首先使用requests發(fā)起資源列表頁面的請求
資源列表
BASE_URL = "http://www.guishiyun.com" #賦值網(wǎng)站根域名作為全局變量,方便調(diào)用 res = requests.get(BASE_URL + "/res_list.aspx?rid=9&tags=1-21,12-96,2-24,3-70", headers=HEADER).text #發(fā)起請求,獲得資源列表頁面的html
難點(diǎn):定位獲得左側(cè)目錄每一章的跳轉(zhuǎn)url
正常思路:打開瀏覽器控制臺(tái),查看網(wǎng)頁源代碼,尋找頁面左側(cè)課程目錄的章節(jié)在哪個(gè)元素內(nèi),用xpath定位。
使用xpath定位,發(fā)現(xiàn)無法定位到這個(gè)a標(biāo)簽,在確認(rèn)xpath語法無錯(cuò)誤后,嘗試打印上個(gè)代碼段中的res變量(也就是該html頁面),發(fā)現(xiàn)返回的頁面和控制臺(tái)頁面不同。
轉(zhuǎn)換思路:可能該頁面使用其他渲染方式渲染了html,導(dǎo)致瀏覽器控制臺(tái)看到的html和請求返回的不一樣(瀏覽器會(huì)將渲染后的頁面呈現(xiàn)),打開控制臺(tái),查看頁面源代碼,搜素九年級(jí)上冊(左側(cè)目錄標(biāo)題),發(fā)現(xiàn)在js的script腳本中,得出該頁面應(yīng)該是通過JS渲染DOM得來的,該js對象中含有跳轉(zhuǎn)的url。
xpath行不通后,我選擇采用正則表達(dá)式的方式直接篩選出該代碼。
import re #導(dǎo)入re 正則表達(dá)式包 pattern = r'var zNodes = (\[\s*[\s\S]*\])' #定義正則表達(dá)式,規(guī)則:找出以"var zNodes = [ \n"開頭,含有"[多個(gè)字符或空格]"的字符,并且以"]"結(jié)尾的文本 (相關(guān)知識(shí)不熟悉的可以簡單看看菜鳥的正則表達(dá)式) result = re.findall(pattern, res, re.M | re.I) #python正則表達(dá)式,查找res中符合pattern規(guī)則的文本。re.M多行匹配,re.I忽略大小寫。
將前兩個(gè)代碼塊封裝一下
def getRootText(): res = requests.get(BASE_URL + "/res_list.aspx?rid=9&tags=1-21,12-96,2-24,3-70", headers=HEADER).text #請求 pattern = r'var zNodes = (\[\s*[\s\S]*\])' result = re.findall(pattern, res, re.M | re.I) return result[0] #獲得篩選結(jié)果 [{id: 1322, pI': 1122, name: '九年級(jí)上冊', open: False, url: ?catId=1322&tags=1-21%2c12-96%2c2-24%2c3-70&rid=9#bottom_content', target: '_self'}, {...},{...}]
將結(jié)果轉(zhuǎn)換成dict類型,方便遍歷,獲得每個(gè)章節(jié)的url。瀏覽上面得出的result發(fā)現(xiàn),{id:1322,pId:xxx...}
并不是標(biāo)準(zhǔn)的json格式(key沒有引號(hào)),此時(shí)使用第三方包demjson,用于將不規(guī)則的json字符串變成python的dict對象。
import demjson def textToDict(text): data = demjson.decode(text) #獲得篩選結(jié)果[{'id': 1322, 'pId': 1122, 'name': '九年級(jí)上冊', 'open': False, 'url': '?catId=1322&tags=1-21%2c12-96%2c2-24%2c3-70&rid=9#bottom_content', 'target': '_self'}, {...},{...}] return data
遍歷轉(zhuǎn)換好的dict數(shù)據(jù),獲得左側(cè)目錄每一章的url。此處需要注意的是,本人目的是下載每一章的ppt課件,所以我只需要請求每一個(gè)總章節(jié)的url(即請求第 1 章,第 2 章,不需要請求 1.1反比例函數(shù)),右邊就會(huì)顯示該章節(jié)下的所有ppt課件。所以我在遍歷的時(shí)候,可以通過正則表達(dá)式,篩選出符合名稱要求的url,添加進(jìn)list并且返回。
def getUrls(dictData): list = [] pattern = r'第[\s\S]*?章' #正則規(guī)則:找出以"第"開頭,中間包含多個(gè)空格和文字,以"章"結(jié)尾的文本 for data in dictData: #遍歷上文轉(zhuǎn)換得到的dict數(shù)組對象 if len(re.findall(pattern, data['name'])) != 0: list.append(data['url']) #如果符合則將該url添加到列表中 return list
3、請求每個(gè)跳轉(zhuǎn)url,定位右側(cè)下載資源按鈕,獲得url請求
遍歷從上面獲得的url列表,通過拼接網(wǎng)站域名獲得網(wǎng)站url,然后發(fā)起請求
def download(urlList): # urlList是上面獲得的list for url in urlList: res = requests.get(BASE_URL + '/res_list.aspx/' + url, HEADER).text #完整url請求,獲得頁面html
查看源代碼,發(fā)現(xiàn)可以用xpath定位(目標(biāo)是獲取到onclick
里的url)
分析:該按鈕元素 (<input type=button>
)在<div class='res_list'><ul><li><div>
里。xpath定位代碼如下:
root = etree.HTML(res) # 構(gòu)造一個(gè)xpath對象 liList = root.xpath('//div[@class="res_list"]//ul//li') #xpath語法,返回多個(gè)<li>及子元素對象的列表
遍歷liList
,獲得資源名字(為之后下載寫入ppt的文件命名)以及跳轉(zhuǎn)到資源詳情下載頁的url
for li in liList: name = li.xpath('.//div[@class="info_area"]//div//h1//text()') name = name[0] # xpath返回的是包含name的列表,從中提取字符串 print(name): 1.1 反比例函數(shù) btnurl = li.xpath('.//div[@class="button_area"]//@onclick') # 獲得onlick內(nèi)的字符串 "window.open('res_view.aspx....')" pattern = r'\(\'([\s\S]*?)\'\)'# 只需要window.open內(nèi)的url,所以采用正則提取出來。 btnurl1 = re.findall(pattern, btnurl[0])
4、跳轉(zhuǎn)到資源詳情下載頁,獲得真正的下載請求(難點(diǎn))
上文代碼段中獲取到url之后依舊是拼接域名,然后通過完整url發(fā)起請求,獲得資源詳情下載頁面的html數(shù)據(jù)。
res1 = requests.get(BASE_URL + '/' + btnurl1[0], HEADER).text
跳轉(zhuǎn)后的詳情頁面
查看源代碼后按鈕本身只是觸發(fā)表單提交,而且是post
請求。點(diǎn)擊下載資源按鈕,使用瀏覽器控制臺(tái)抓包查看post請求需要的參數(shù)。
使用ctrl+f
在網(wǎng)頁源代碼中搜素這幾個(gè)參數(shù),發(fā)現(xiàn)存在于<input>
標(biāo)簽中,只是被css
隱藏了,所以接下來就是簡單的用xpath
和正則表達(dá)式將post
請求中的url
和這幾個(gè)參數(shù)值獲得,然后添加到header
中發(fā)起請求就行了。
VIEWSTATE = '__VIEWSTATE' # 全局變量,定義屬性名稱 VIEWSTATEGENERATOR = '__VIEWSTATEGENERATOR' EVENTVALIDATION = '__EVENTVALIDATION' BUTTON = 'BUTTON' BUTTON_value = '下 載 資 源'
root1 = etree.HTML(res1) # res1是之前代碼段請求的html文本 form = root1.xpath('//form[@id="form1"]') # xpath定位到form action = root1.xpath('//form[@id="form1"]/@action') action = re.findall(r'(/[\S]*?&[\S]*?)&', action[0], re.I) #正則表達(dá)式獲取form中action函數(shù)里的url VIEWSTATE_value = form[0].xpath( './/input[@name="__VIEWSTATE"]//@value') #獲取參數(shù)值 VIEWSTATEGENERATOR_value = form[0].xpath( './/input[@name="__VIEWSTATEGENERATOR"]//@value')#獲取參數(shù)值 EVENTVALIDATION_value = form[0].xpath( './/input[@name="__EVENTVALIDATION"]/@value')#獲取參數(shù)值 data = { # post提交所需要的data參數(shù) VIEWSTATE: VIEWSTATE_value, VIEWSTATEGENERATOR: VIEWSTATEGENERATOR_value, EVENTVALIDATION: EVENTVALIDATION_value, BUTTON: BUTTON_value } res2 = requests.post(BASE_URL + action[0],data=data,headers=HEADER).text #發(fā)起請求
此時(shí)發(fā)起請求之后發(fā)現(xiàn)返回的仍然是網(wǎng)頁html,如果打開控制臺(tái)工具,查看點(diǎn)擊按鈕發(fā)起請求后的頁面。
同時(shí)看到由于是更新頁面,還產(chǎn)生了許多其他各種各樣的請求,一時(shí)間很難找到真正下載文件的請求是哪一個(gè)。
此時(shí)筆者想到的是一個(gè)笨方法,通過抓包工具,對所有請求進(jìn)行攔截,然后一個(gè)個(gè)請求陸續(xù)通過,最終就可以找到下載請求。這里筆者用到的是BurpSuite
工具,陸續(xù)放行請求,觀察頁面是否有下載界面出現(xiàn),找到了url:/code/down_res.ashx?id=xxx
,同時(shí)在瀏覽器控制臺(tái)查找這一串字符串,最終在post
請求返回的頁面中找到了這個(gè)字符串的位置
不用多說,直接正則獲取
downUrl = re.search(r'\<script\>[\s]*?location\.href\s=\s\'([\S]*?)\'',res2,re.I) #正則篩選出url downUrl_text = downUrl.group(1)
發(fā)起請求,并且將數(shù)據(jù)讀寫進(jìn)指定的目錄中。
downPPT = requests.get(BASE_URL+downUrl_text,headers=HEADER) with open(f'./test/{name}.ppt','wb') as f: #將下載的數(shù)據(jù)以二進(jìn)制的形式寫入到當(dāng)前項(xiàng)目下test文件夾中,并且做好命名。name參數(shù)在上文中已經(jīng)獲得。 f.write(downPPT.content)
結(jié)果
5、添加額外功能,實(shí)現(xiàn)增量爬蟲
爬取到一半發(fā)現(xiàn)程序終止了,原來該網(wǎng)站對每個(gè)賬號(hào)每天下載數(shù)有限額,而我們的程序每次運(yùn)行都會(huì)從頭開始檢索,如何對已經(jīng)爬取過的url進(jìn)行存儲(chǔ),同時(shí)下次程序運(yùn)行時(shí)對已爬取過的url進(jìn)行識(shí)別?這里筆者使用的是通過redis
進(jìn)行存儲(chǔ),原理是對每次下載的url進(jìn)行存儲(chǔ),在每次發(fā)起下載請求時(shí)先判斷是否已經(jīng)存儲(chǔ),如果已經(jīng)存儲(chǔ)則跳過本次循環(huán)。
if(r.sadd(BASE_URL + action[0],'1')==0): # sadd是redis添加鍵值的方法,如果==0說明已經(jīng)存在,添加失敗。 continue
6、總源代碼
import re import requests from lxml import etree import demjson import redis pool = redis.ConnectionPool(host='localhost', port=6379, decode_responses=True) r = redis.Redis('localhost',6379,decode_responses=True) BASE_URL = "http://www.guishiyun.com" HEADER = { 'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36", 'Cookie': "xxx", } VIEWSTATE = '__VIEWSTATE' VIEWSTATEGENERATOR = '__VIEWSTATEGENERATOR' EVENTVALIDATION = '__EVENTVALIDATION' BUTTON = 'BUTTON' BUTTON_value = '下 載 資 源' def getRootText(): res = requests.get(BASE_URL + "/res_list.aspx?rid=9&tags=1-21,12-96,2-24,3-70", headers=HEADER).text pattern = r'var zNodes = (\[\s*[\s\S]*\])' result = re.findall(pattern, res, re.M | re.I) return result[0] def textToDict(text): data = demjson.decode(text) print(data) return data def getUrls(dictData): list = [] pattern = r'第[\s\S]*?章' for data in dictData: if len(re.findall(pattern, data['name'])) != 0: list.append(data['url']) return list def download(urlList): global r for url in urlList: res = requests.get(BASE_URL + '/res_list.aspx/' + url, HEADER).text root = etree.HTML(res) liList = root.xpath('//div[@class="res_list"]//ul//li') for li in liList: name = li.xpath('.//div[@class="info_area"]//div//h1//text()') name = name[0] btnurl = li.xpath('.//div[@class="button_area"]//@onclick') pattern = r'\(\'([\s\S]*?)\'\)' btnurl1 = re.findall(pattern, btnurl[0]) res1 = requests.get(BASE_URL + '/' + btnurl1[0], HEADER).text root1 = etree.HTML(res1) form = root1.xpath('//form[@id="form1"]') action = root1.xpath('//form[@id="form1"]/@action') action = re.findall(r'(/[\S]*?&[\S]*?)&', action[0], re.I) VIEWSTATE_value = form[0].xpath( './/input[@name="__VIEWSTATE"]//@value') VIEWSTATEGENERATOR_value = form[0].xpath( './/input[@name="__VIEWSTATEGENERATOR"]//@value') EVENTVALIDATION_value = form[0].xpath( './/input[@name="__EVENTVALIDATION"]/@value') data = { VIEWSTATE: VIEWSTATE_value, VIEWSTATEGENERATOR: VIEWSTATEGENERATOR_value, EVENTVALIDATION: EVENTVALIDATION_value, BUTTON: BUTTON_value } if(r.sadd(BASE_URL + action[0],'1')==0): continue res2 = requests.post(BASE_URL + action[0],data=data,headers=HEADER).text downUrl = re.search(r'\<script\>[\s]*?location\.href\s=\s\'([\S]*?)\'',res2,re.I) downUrl_text = downUrl.group(1) if(r.sadd(BASE_URL+downUrl_text,BASE_URL+downUrl_text,downUrl_text)==0): continue downPPT = requests.get(BASE_URL+downUrl_text,headers=HEADER) with open(f'./test/{name}.ppt','wb') as f: f.write(downPPT.content) def main(): text = getRootText() dictData = textToDict(text) list = getUrls(dictData) # download(list) if __name__ == '__main__': main()
三、總結(jié)
之前只是學(xué)習(xí)過最簡單最基礎(chǔ)的requests
請求+xpath
定位的爬蟲方式,這次碰巧遇到了較為麻煩的爬蟲實(shí)戰(zhàn),所以寫下爬蟲思路和實(shí)戰(zhàn)筆記,加深自己印象的同時(shí)也希望能對大家有所幫助。當(dāng)然這次爬蟲總的來說還是比較簡單,還沒有考慮代理+多線程等情況,同時(shí)還可以使用selenium
等瀏覽器渲染工具,就可以不用正則定位了,當(dāng)然筆者是為了順便學(xué)習(xí)一下正則。
到此這篇關(guān)于python動(dòng)態(tài)網(wǎng)站爬蟲實(shí)戰(zhàn)(requests+xpath+demjson+redis)的文章就介紹到這了,更多相關(guān)python動(dòng)態(tài)網(wǎng)站爬蟲 內(nèi)容請搜索本站以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持本站!
版權(quán)聲明:本站文章來源標(biāo)注為YINGSOO的內(nèi)容版權(quán)均為本站所有,歡迎引用、轉(zhuǎn)載,請保持原文完整并注明來源及原文鏈接。禁止復(fù)制或仿造本網(wǎng)站,禁止在非www.sddonglingsh.com所屬的服務(wù)器上建立鏡像,否則將依法追究法律責(zé)任。本站部分內(nèi)容來源于網(wǎng)友推薦、互聯(lián)網(wǎng)收集整理而來,僅供學(xué)習(xí)參考,不代表本站立場,如有內(nèi)容涉嫌侵權(quán),請聯(lián)系alex-e#qq.com處理。