不知道大家有沒有遇到這種情況:當(dāng)我們r(jià)equests發(fā)出請求采集頁面信息的時(shí)候,得到的結(jié)果肯能會(huì)跟在瀏覽器中看到的不一樣,在瀏覽器中看到的數(shù)據(jù),使用requests請求時(shí)可能會(huì)沒有。
1.前言
上面這種情況的原因就是requests獲取的都是靜態(tài)的HTML文檔內(nèi)容,而瀏覽器中看到的頁面,其中的部分?jǐn)?shù)據(jù)可能是JavaScript處理后生成的數(shù)據(jù),這種數(shù)據(jù)也有很多種生成方式:有Ajax加載生成的,也有經(jīng)過JavaScript和一定的計(jì)算方式生成的。
那對于Ajax,這里簡單介紹一下:Ajax是一種異步數(shù)據(jù)加載方式,就是原始的頁面生成之后,開始不會(huì)包含這部分?jǐn)?shù)據(jù),之后會(huì)通過再次向服務(wù)器端請求某個(gè)接口獲取,然后再經(jīng)過一定處理顯示再頁面上。
所以,我們以后遇到這種頁面時(shí),我們直接發(fā)送requests請求是無法獲取到一些數(shù)據(jù)的,這時(shí)候我們就需要找到這部分?jǐn)?shù)據(jù)的源頭:也就是這個(gè)Ajax請求,在進(jìn)行模擬,就可以成功獲取到數(shù)據(jù)了,比如我們最開始實(shí)現(xiàn)的例子:爬蟲開發(fā)實(shí)戰(zhàn)1.1 解決JS加密。沒有看的或者不記得的,可以返回去仔細(xì)的看一下。
這篇主要是通過一個(gè)小例子來了解一下Ajax以及如何去解析采集這類的數(shù)據(jù)。
至于什么是Ajax,如果需要了解其原理的話,可以去W3School上看下幾個(gè)示例
http://www.w3school.com.cn/ajax/ajax_xmlhttprequest_send.asp
或者去崔老師的博客
[Python3網(wǎng)絡(luò)爬蟲開發(fā)實(shí)戰(zhàn)] 6.1-什么是Ajax
2.Ajax分析
我們先去找個(gè)博主。作為吃貨大軍中的一員,果斷去了美食欄目,就拿第一個(gè)博主為例吧,名字也很接地氣?。?a target="_blank">365道菜:https://weibo.com/u/1558473534?refer_flag=1087030701_2975_2023_0&is_hot=1
先看下他的主頁,這不是在打廣告哈。。。

右鍵檢查,彈出開發(fā)者工具界面,我們打開Network選項(xiàng),然后重新刷新頁面,就可以看到目前所有請求返回之后渲染HTML的信息了。
然后我們選擇Ajax相關(guān)的請求,對應(yīng)的請求類型是
XHR,這里注意一下:剛開始選擇XHR選項(xiàng)時(shí)是沒有內(nèi)容的,然后鼠標(biāo)滾輪往下滾,直到出現(xiàn)第一條請求為止,見下圖:
接下來我們→_→,看一下他的一些選項(xiàng),首先Headers

這里包含了請求的地址,請求的方式是get請求,請求的code是200表示成功,下面是請求頭,返回頭,還有請求的參數(shù),可以說這里包含了一個(gè)請求所有的內(nèi)容了,先不看具體字段的意思。
這時(shí)候一個(gè)請求可能看不出什么,可以繼續(xù)往下滾動(dòng)滾輪,直到最下面,這里出現(xiàn)了分頁, 就先不管了:

我們再看下請求,這里有多出現(xiàn)了一條,下面就根據(jù)這兩條Ajax請求來分析一下:

剛才已經(jīng)了解了Headers了,下面看下Preview跟Response,兩者都是響應(yīng)的信息,只是Preview是標(biāo)準(zhǔn)的Json格式的,好看一點(diǎn):

這里就比較清楚了,返回了三個(gè)參數(shù):
code, data, msg,genuine意思我們就可以猜測:data中就是我們需要的數(shù)據(jù)了,把鼠標(biāo)移到
Show more(400KB)上,發(fā)現(xiàn)是一個(gè)HTML的代碼塊,看起來不是很清晰。可以通過右邊的copy,把這段復(fù)制出來,然后在編輯器中新建一個(gè)html,粘貼到這里面,
ctrl+alt+L整潔下代碼,呈現(xiàn)一下,看進(jìn)度條還是挺多內(nèi)容的:
點(diǎn)擊右上角的google瀏覽器,看一下頁面:

樣式?jīng)]有渲染出來,但是我們根據(jù)圖片可以在原頁面上找出對應(yīng)的內(nèi)容:


大致的數(shù)了一下,總共十五篇的信息。
下面的Ajax請求,響應(yīng)的數(shù)據(jù)跟這個(gè)是一樣的分析方式,這里就不再多說了。
3.Ajax數(shù)據(jù)采集
Ajax的分析已經(jīng)完成了,下面就是開始進(jìn)行采集了,首先先把基本架子寫好:
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ù)是要在請求頭中帶上cookie的,所以在
self._headers中還要加上cookie這個(gè)屬性。
加cookie屬性
現(xiàn)在來看一下請求參數(shù)是什么,因?yàn)槟壳耙豁撝芯椭挥袃纱蝍jax數(shù)據(jù)加載,所以我們可以看一下他們的共性:


通過對比兩次的請求參數(shù),不難發(fā)現(xiàn),其中的
pagebar跟__rnd兩個(gè)參數(shù)會(huì)有些變化,pagebar這個(gè)比較簡單,就分0, 1。 __rnd參數(shù)發(fā)現(xiàn):這個(gè)就是個(gè)時(shí)間戳,不過python中的時(shí)間戳是10位,而且是小數(shù),這里的是13位,這樣可以自己去測試一下:當(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)在請求已經(jīng)完成了,看下打印的結(jié)果,由于內(nèi)容較多,就貼個(gè)圖:

接下來就是對獲取到的內(nèi)容進(jìn)行分析,拿到我們想要的數(shù)據(jù)了,這里就隨便取幾個(gè)數(shù)據(jù)了:博主、博主頭像、時(shí)間、文字內(nèi)容、圖片內(nèi)容、評論數(shù)、點(diǎn)贊數(shù)。
之前的幾篇文中已經(jīng)實(shí)際應(yīng)用了一些解析的用法了,這里就不仔細(xì)寫了,大概寫一下思路吧:
首先我們要取的是一篇一篇的博客內(nèi)容,上面的內(nèi)容可能是一篇文字內(nèi)容對應(yīng)多個(gè)圖片,所以在解析的時(shí)候需要對應(yīng)起來,我們看下之前復(fù)制出來的Html塊:
加載的Html塊
通過左邊的 + - 符號可以很清晰的展現(xiàn)出每一篇博客的html塊,每一塊是由一個(gè)div組成,這樣我們可以先取div塊,然后再從每個(gè)div塊中再獲取我們所需要的數(shù)據(jù),可以這樣處理:
首先獲取每篇博客的div塊,也就是博客列表,列表中是每篇博客div塊的Element對象
# 相應(yīng)信息中獲取加載的數(shù)據(jù)信息
data_dict = json.loads(response)
html_content = etree.HTML(data_dict['data']) # 轉(zhuǎn)為Element對象
# 獲取每篇博客的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長度的比較特殊的字符,編碼可能轉(zhuǎn)不過來,所以做個(gè)簡單替換處理
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')
# 評論數(shù)
blog_item['blog_comment'] = blog.xpath('descendant::span[@node-type="comment_btn_text"]//em[last()]/text()')[0]
# 點(diǎn)贊數(shù), 在這里有個(gè)處理:當(dāng)沒有點(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):
"""
請求
: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ā)出請求,獲取響應(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對象
# 獲取每篇博客的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長度的比較特殊的字符,編碼可能轉(zhuǎn)不過來,所以做個(gè)簡單替換處理
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')
# 評論數(shù)
blog_item['blog_comment'] = blog.xpath('descendant::span[@node-type="comment_btn_text"]//em[last()]/text()')[0]
# 點(diǎn)贊數(shù), 在這里有個(gè)處理:當(dāng)沒有點(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é)語
雖然看起來篇幅很長,其實(shí)也是挺簡單基礎(chǔ)的一個(gè)采集小實(shí)例,就是簡單說了下Ajax異步加載數(shù)據(jù)獲取的方式跟分析的簡單步驟,如果大家有更好的方法可以留言一起交流。下一篇再用一個(gè)完整的實(shí)例來加深對Ajax異步加載數(shù)據(jù)采集的印象。

