不知道大家有沒(méi)有遇到這種情況:當(dāng)我們r(jià)equests發(fā)出請(qǐng)求采集頁(yè)面信息的時(shí)候,得到的結(jié)果肯能會(huì)跟在瀏覽器中看到的不一樣疹启,在瀏覽器中看到的數(shù)據(jù),使用requests請(qǐng)求時(shí)可能會(huì)沒(méi)有溉浙。
1.前言
上面這種情況的原因就是requests獲取的都是靜態(tài)的HTML文檔內(nèi)容投放,而瀏覽器中看到的頁(yè)面,其中的部分?jǐn)?shù)據(jù)可能是JavaScript處理后生成的數(shù)據(jù)绒北,這種數(shù)據(jù)也有很多種生成方式:有Ajax加載生成的黎侈,也有經(jīng)過(guò)JavaScript和一定的計(jì)算方式生成的。
那對(duì)于Ajax闷游,這里簡(jiǎn)單介紹一下:Ajax是一種異步數(shù)據(jù)加載方式峻汉,就是原始的頁(yè)面生成之后,開(kāi)始不會(huì)包含這部分?jǐn)?shù)據(jù)脐往,之后會(huì)通過(guò)再次向服務(wù)器端請(qǐng)求某個(gè)接口獲取休吠,然后再經(jīng)過(guò)一定處理顯示再頁(yè)面上。
所以业簿,我們以后遇到這種頁(yè)面時(shí)瘤礁,我們直接發(fā)送requests請(qǐng)求是無(wú)法獲取到一些數(shù)據(jù)的,這時(shí)候我們就需要找到這部分?jǐn)?shù)據(jù)的源頭:也就是這個(gè)Ajax請(qǐng)求梅尤,在進(jìn)行模擬柜思,就可以成功獲取到數(shù)據(jù)了岩调,比如我們最開(kāi)始實(shí)現(xiàn)的例子:爬蟲(chóng)開(kāi)發(fā)實(shí)戰(zhàn)1.1 解決JS加密。沒(méi)有看的或者不記得的赡盘,可以返回去仔細(xì)的看一下号枕。
這篇主要是通過(guò)一個(gè)小例子來(lái)了解一下Ajax以及如何去解析采集這類的數(shù)據(jù)。
至于什么是Ajax陨享,如果需要了解其原理的話葱淳,可以去W3School上看下幾個(gè)示例
http://www.w3school.com.cn/ajax/ajax_xmlhttprequest_send.asp
或者去崔老師的博客
[Python3網(wǎng)絡(luò)爬蟲(chóng)開(kāi)發(fā)實(shí)戰(zhàn)] 6.1-什么是Ajax
2.Ajax分析
我們先去找個(gè)博主。作為吃貨大軍中的一員霉咨,果斷去了美食欄目蛙紫,就拿第一個(gè)博主為例吧,名字也很接地氣巴窘洹:365道菜:https://weibo.com/u/1558473534?refer_flag=1087030701_2975_2023_0&is_hot=1
先看下他的主頁(yè)坑傅,這不是在打廣告哈。喷斋。唁毒。
右鍵檢查,彈出開(kāi)發(fā)者工具界面星爪,我們打開(kāi)Network選項(xiàng)浆西,然后重新刷新頁(yè)面,就可以看到目前所有請(qǐng)求返回之后渲染HTML的信息了顽腾。
然后我們選擇Ajax相關(guān)的請(qǐng)求近零,對(duì)應(yīng)的請(qǐng)求類型是
XHR
,這里注意一下:剛開(kāi)始選擇XHR
選項(xiàng)時(shí)是沒(méi)有內(nèi)容的,然后鼠標(biāo)滾輪往下滾抄肖,直到出現(xiàn)第一條請(qǐng)求為止久信,見(jiàn)下圖:接下來(lái)我們→_→,看一下他的一些選項(xiàng)漓摩,首先Headers
這里包含了請(qǐng)求的地址裙士,請(qǐng)求的方式是get請(qǐng)求,請(qǐng)求的code是200表示成功管毙,下面是請(qǐng)求頭腿椎,返回頭,還有請(qǐng)求的參數(shù)夭咬,可以說(shuō)這里包含了一個(gè)請(qǐng)求所有的內(nèi)容了啃炸,先不看具體字段的意思。
這時(shí)候一個(gè)請(qǐng)求可能看不出什么卓舵,可以繼續(xù)往下滾動(dòng)滾輪南用,直到最下面,這里出現(xiàn)了分頁(yè), 就先不管了:
我們?cè)倏聪抡?qǐng)求训枢,這里有多出現(xiàn)了一條,下面就根據(jù)這兩條Ajax請(qǐng)求來(lái)分析一下:
剛才已經(jīng)了解了Headers了忘巧,下面看下Preview跟Response恒界,兩者都是響應(yīng)的信息,只是Preview是標(biāo)準(zhǔn)的Json格式的砚嘴,好看一點(diǎn):
這里就比較清楚了十酣,返回了三個(gè)參數(shù):
code
, data
, msg
,genuine意思我們就可以猜測(cè):data
中就是我們需要的數(shù)據(jù)了际长,把鼠標(biāo)移到
Show more(400KB)
上耸采,發(fā)現(xiàn)是一個(gè)HTML的代碼塊,看起來(lái)不是很清晰工育。可以通過(guò)右邊的copy虾宇,把這段復(fù)制出來(lái),然后在編輯器中新建一個(gè)html如绸,粘貼到這里面嘱朽,
ctrl+alt+L
整潔下代碼,呈現(xiàn)一下,看進(jìn)度條還是挺多內(nèi)容的:點(diǎn)擊右上角的google瀏覽器怔接,看一下頁(yè)面:
樣式?jīng)]有渲染出來(lái)搪泳,但是我們根據(jù)圖片可以在原頁(yè)面上找出對(duì)應(yīng)的內(nèi)容:
大致的數(shù)了一下,總共十五篇的信息扼脐。
下面的Ajax請(qǐng)求,響應(yīng)的數(shù)據(jù)跟這個(gè)是一樣的分析方式,這里就不再多說(shuō)了诫欠。
3.Ajax數(shù)據(jù)采集
Ajax的分析已經(jīng)完成了意述,下面就是開(kāi)始進(jìn)行采集了,首先先把基本架子寫(xiě)好:
import requests
class WeiboSpider(object):
def __init__(self):
self._headers = {
'Accept': '*/*',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Content-Type': 'application/x-www-form-urlencoded',
'Host': 'weibo.com',
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36',
'X-Requested-With': 'XMLHttpRequest',
}
def run(self):
pass
if __name__ == '__main__':
wb_spider = WeiboSpider()
wb_spider.run()
這里注意一點(diǎn):現(xiàn)在微博采集數(shù)據(jù)是要在請(qǐng)求頭中帶上cookie的脏榆,所以在
self._headers
中還要加上cookie
這個(gè)屬性猖毫。
加cookie屬性
現(xiàn)在來(lái)看一下請(qǐng)求參數(shù)是什么,因?yàn)槟壳耙豁?yè)中就只有兩次ajax數(shù)據(jù)加載须喂,所以我們可以看一下他們的共性:
通過(guò)對(duì)比兩次的請(qǐng)求參數(shù)吁断,不難發(fā)現(xiàn),其中的
pagebar
跟__rnd
兩個(gè)參數(shù)會(huì)有些變化坞生,pagebar
這個(gè)比較簡(jiǎn)單仔役,就分0, 1是己。 __rnd
參數(shù)發(fā)現(xiàn):這個(gè)就是個(gè)時(shí)間戳又兵,不過(guò)python中的時(shí)間戳是10位,而且是小數(shù),這里的是13位沛厨,這樣可以自己去測(cè)試一下:當(dāng)前時(shí)間的時(shí)間戳再拼接上3位的隨機(jī)數(shù):
def get_response(self, req_url, params_dict=None):
if params_dict:
response = requests.get(req_url, params=params_dict, headers=self._headers)
else:
response = requests.get(req_url, headers=self._headers)
if response.status_code == 200:
return response.content.decode('utf-8')
return None
def run(self, pagebar, rnd):
params_dict = {
"ajwvr": 6,
"domain": 100505,
"refer_flag": "1087030701_2975_2023_0",
"is_hot": 1,
"pagebar": pagebar,
"pl_name": "Pl_Official_MyProfileFeed__20",
"id": 1005051558473534,
"script_uri": "/u/1558473534",
"feed_type": 0,
"page": 1,
"pre_page": 1,
"domain_op": 100505,
"__rnd": rnd,
}
start_url = "https://weibo.com/p/aj/v6/mblog/mbloglist"
response = self.get_response(start_url, params_dict)
print(response)
if __name__ == '__main__':
wb_spider = WeiboSpider()
dtime = datetime.datetime.now()
un_time = time.mktime(dtime.timetuple())
rnd = int(f'{int(un_time)}{rd.randint(100, 999)}')
其實(shí)params_dict
中的部分參數(shù)可能也是不需要的宙地,這里想去實(shí)驗(yàn)的可以去嘗試一下
現(xiàn)在請(qǐng)求已經(jīng)完成了,看下打印的結(jié)果逆皮,由于內(nèi)容較多宅粥,就貼個(gè)圖:
接下來(lái)就是對(duì)獲取到的內(nèi)容進(jìn)行分析,拿到我們想要的數(shù)據(jù)了电谣,這里就隨便取幾個(gè)數(shù)據(jù)了:博主秽梅、博主頭像、時(shí)間剿牺、文字內(nèi)容企垦、圖片內(nèi)容、評(píng)論數(shù)晒来、點(diǎn)贊數(shù)钞诡。
之前的幾篇文中已經(jīng)實(shí)際應(yīng)用了一些解析的用法了,這里就不仔細(xì)寫(xiě)了湃崩,大概寫(xiě)一下思路吧:
首先我們要取的是一篇一篇的博客內(nèi)容臭增,上面的內(nèi)容可能是一篇文字內(nèi)容對(duì)應(yīng)多個(gè)圖片,所以在解析的時(shí)候需要對(duì)應(yīng)起來(lái)竹习,我們看下之前復(fù)制出來(lái)的Html塊:
加載的Html塊
通過(guò)左邊的 + -
符號(hào)可以很清晰的展現(xiàn)出每一篇博客的html塊誊抛,每一塊是由一個(gè)div
組成,這樣我們可以先取div塊整陌,然后再?gòu)拿總€(gè)div塊中再獲取我們所需要的數(shù)據(jù)拗窃,可以這樣處理:
首先獲取每篇博客的div塊,也就是博客列表泌辫,列表中是每篇博客div塊的Element對(duì)象
# 相應(yīng)信息中獲取加載的數(shù)據(jù)信息
data_dict = json.loads(response)
html_content = etree.HTML(data_dict['data']) # 轉(zhuǎn)為Element對(duì)象
# 獲取每篇博客的div塊
blog_list = html_content.xpath('//div[@action-type="feed_list_item"]')
看下結(jié)果:
[<Element div at 0x39801c8>, <Element div at 0x3980b48>, <Element div at 0x3980bc8>, <Element div at 0x3980b88>, <Element div at 0x3980c48>, <Element div at 0x3980d08>, <Element div at 0x3980cc8>, <Element div at 0x3980d48>, <Element div at 0x3980d88>, <Element div at 0x3980c08>, <Element div at 0x3980dc8>, <Element div at 0x3980e08>, <Element div at 0x3980e48>, <Element div at 0x395a988>, <Element div at 0x395aa08>]
然后再遍歷解析每篇博客随夸,獲取我們所需要的數(shù)據(jù):
for blog in blog_list:
blog_item = dict()
# 博主頭像
blog_item['blogger_photo'] = blog.xpath('descendant::img[@class="W_face_radius"]/@src')[0]
# 博主昵稱:這個(gè)信息有很多地方都出現(xiàn)了,可以選擇一個(gè)較好取值的,我選的是跟微博內(nèi)容在一個(gè)地方的震放,用nick-name屬性表示
blog_item['blogger_name'] = blog.xpath('descendant::div[contains(@class, "WB_text")]/@nick-name')[0]
# 博客時(shí)間
blog_item['blog_time'] = blog.xpath('descendant::a[@node-type="feed_list_item_date"]/@title')[0]
# 博客文字內(nèi)容, 這里注意的是有個(gè) \u200b 字符宾毒,,這是個(gè)0長(zhǎng)度的比較特殊的字符殿遂,編碼可能轉(zhuǎn)不過(guò)來(lái)诈铛,所以做個(gè)簡(jiǎn)單替換處理
blog_item['blog_content'] = blog.xpath('descendant::div[contains(@class, "WB_text")]/text()')[0].strip().replace('\u200b', '')
# 博客圖片內(nèi)容
blog_item['blog_picture_list'] = blog.xpath('descendant::ul[@node-type="fl_pic_list"]//li/img/@src')
# 評(píng)論數(shù)
blog_item['blog_comment'] = blog.xpath('descendant::span[@node-type="comment_btn_text"]//em[last()]/text()')[0]
# 點(diǎn)贊數(shù), 在這里有個(gè)處理:當(dāng)沒(méi)有點(diǎn)贊的時(shí)候會(huì)顯示出一個(gè) “贊” 字, 所以當(dāng)是 “贊” 的時(shí)候點(diǎn)贊數(shù)是 0
blog_likestar = blog.xpath('descendant::span[@node-type="like_status"]//em[last()]/text()')[0]
blog_item['blog_likestar'] = '0' if blog_likestar == '贊' else blog_likestar
yield blog_item
貼一下主要實(shí)現(xiàn)代碼:
def get_response(self, req_url, params_dict=None):
"""
請(qǐng)求
:param req_url:
:param params_dict:
:return:
"""
if params_dict:
response = requests.get(req_url, params=params_dict, headers=self._headers)
else:
response = requests.get(req_url, headers=self._headers)
if response.status_code == 200:
return response.content.decode('utf-8')
return None
def run(self, pagebar, rnd):
"""
主函數(shù)
:param pagebar:
:param rnd:
:return:
"""
params_dict = {
"ajwvr": 6,
"domain": 100505,
"refer_flag": "1087030701_2975_2023_0",
"is_hot": 1,
"pagebar": pagebar,
"pl_name": "Pl_Official_MyProfileFeed__20",
"id": 1005051558473534,
"script_uri": "/u/1558473534",
"feed_type": 0,
"page": 1,
"pre_page": 1,
"domain_op": 100505,
"__rnd": rnd,
}
start_url = "https://weibo.com/p/aj/v6/mblog/mbloglist"
# 1.發(fā)出請(qǐng)求墨礁,獲取響應(yīng)
response = self.get_response(start_url, params_dict)
# 2.數(shù)據(jù)解析
blog_content = self.get_blog_list(response)
# 3.輸出采集到的內(nèi)容幢竹, 想存儲(chǔ)的可自選存儲(chǔ)方式
for blog in blog_content:
print(blog)
def get_blog_list(self, response):
"""
獲取博客列表
:param response:
:return:
"""
# 相應(yīng)信息中獲取加載的數(shù)據(jù)信息
data_dict = json.loads(response)
html_content = etree.HTML(data_dict['data']) # 轉(zhuǎn)為Element對(duì)象
# 獲取每篇博客的div塊
blog_list = html_content.xpath('//div[@action-type="feed_list_item"]')
# 遍歷解析每篇博客內(nèi)容
blog_content = self.data_parse(blog_list)
return blog_content
def data_parse(self, blog_list):
"""
解析每篇博客內(nèi)容
:param response:
:return:
"""
for blog in blog_list:
blog_item = dict()
# 博主頭像
blog_item['blogger_photo'] = blog.xpath('descendant::img[@class="W_face_radius"]/@src')[0]
# 博主昵稱:這個(gè)信息有很多地方都出現(xiàn)了,可以選擇一個(gè)較好取值的,我選的是跟微博內(nèi)容在一個(gè)地方的恩静,用nick-name屬性表示
blog_item['blogger_name'] = blog.xpath('descendant::div[contains(@class, "WB_text")]/@nick-name')[0]
# 博客時(shí)間
blog_item['blog_time'] = blog.xpath('descendant::a[@node-type="feed_list_item_date"]/@title')[0]
# 博客文字內(nèi)容, 這里注意的是有個(gè) \u200b 字符焕毫,蹲坷,這是個(gè)0長(zhǎng)度的比較特殊的字符,編碼可能轉(zhuǎn)不過(guò)來(lái)邑飒,所以做個(gè)簡(jiǎn)單替換處理
blog_item['blog_content'] = blog.xpath('descendant::div[contains(@class, "WB_text")]/text()')[0].strip().replace('\u200b', '')
# 博客圖片內(nèi)容
blog_item['blog_picture_list'] = blog.xpath('descendant::ul[@node-type="fl_pic_list"]//li/img/@src')
# 評(píng)論數(shù)
blog_item['blog_comment'] = blog.xpath('descendant::span[@node-type="comment_btn_text"]//em[last()]/text()')[0]
# 點(diǎn)贊數(shù), 在這里有個(gè)處理:當(dāng)沒(méi)有點(diǎn)贊的時(shí)候會(huì)顯示出一個(gè) “贊” 字循签, 所以當(dāng)是 “贊” 的時(shí)候點(diǎn)贊數(shù)是 0
blog_likestar = blog.xpath('descendant::span[@node-type="like_status"]//em[last()]/text()')[0]
blog_item['blog_likestar'] = '0' if blog_likestar == '贊' else blog_likestar
yield blog_item
再貼一下main:
if __name__ == '__main__':
wb_spider = WeiboSpider()
dtime = datetime.datetime.now()
un_time = time.mktime(dtime.timetuple())
rnd = int(f'{int(un_time)}{rd.randint(100, 999)}')
for i in range(2):
print(f'{"=" * 30}第 {i + 1} 次數(shù)據(jù)加載')
wb_spider.run(i, rnd)
time.sleep(10)
看一下最終打印結(jié)果,由于數(shù)據(jù)較多疙咸,這里貼個(gè)圖:
4.結(jié)語(yǔ)
雖然看起來(lái)篇幅很長(zhǎng)懦底,其實(shí)也是挺簡(jiǎn)單基礎(chǔ)的一個(gè)采集小實(shí)例,就是簡(jiǎn)單說(shuō)了下Ajax異步加載數(shù)據(jù)獲取的方式跟分析的簡(jiǎn)單步驟罕扎,如果大家有更好的方法可以留言一起交流。下一篇再用一個(gè)完整的實(shí)例來(lái)加深對(duì)Ajax異步加載數(shù)據(jù)采集的印象丐重。