一、相關環境
開發工具:PyCharm 2022.3.1 (Community Edition)
開發環境:Python 3.9
操作系統:Windows10
二、目標數據
在百度指數首頁登錄,然後輸入一個關鍵詞進入搜索指數頁面,以下操作全是基於該頁面進行。
我們要抓取的是指定關鍵詞的搜索指數趨勢圖中某一段時間的數據,這裏以python的搜索指數為例:
需要獲取曲線上每一個這樣的點對應的數據。打開開發者工具,在元素欄中Ctrl+F搜索其中一個數據,搜索結果為0條,説明網頁源代碼中不存在需要的數據,而是動態獲取的。
三、獲取動態數據
打開開發者工具->切換到網絡欄->選中XHR項->刷新頁面
逐個點擊左側請求的名稱並查看右側的響應預覽,發現index開頭的請求中包含數據,觀察一下發現應該是我們需要的數據,但是data被加密了,那麼下一步需要js逆向找到加密方式。
四、JS逆向
我們分析上述請求返回的json數據,其形式大致如下:
{
"data": {
"generalRatio": [
{}
],
"userIndexes": [
{
"word": [],
"all": {
"data": "",
"endDate": "",
"startDate": ""
},
"pc": {},
"wise": {},
"type": "day"
}
],
"uniqid": "xxxxxxxxxx"
}
}
當前端拿到這樣的加密數據來解密時,必然要用到類似data.generalRatio、data.userIndexes這樣的方式來取出整個數組;更進一步考慮,前端會使用all.data來獲取最後一層數據。由於編譯過的js中整個數據一般會賦值給單字母,而all和data這種單詞又過於常見,因此我們的思路是先用generalRatio、userIndexes去搜索,沒有結果的話再用all.data去搜索。
Ctrl+Shift+F調出全局搜索框,搜索userIndexes(因為我們找的是userIndexes中的數據),定位到兩個js。
逐個查看後發現第一個js中userIndexes只有一處,是在處理請求的函數中。第二個js中userIndexes有三處,第一處是向後端發送axios請求,説明我們的目標就在這個js中;第二處和第三處在一起,可以很明顯得發現這裏在構造趨勢圖的數據,除了賦值操作之外,我們可以看到有一個decrypt函數,且分別用它操作了all.data、pc.data、wise.data,至此我們可以確定這個函數就是解密函數。
decrypt函數傳入了兩個參數,第二個參數可以看出是從後端獲取的加密數據,第一個參數暫時看出來是,我們在其中一個decrypt函數處打斷點進行調試,跳轉至decrypt函數內部時,可以看到第一個參數是"-GlAwgjW3yBdeQm7,%5+940831-6.2"這樣的加密字符串,在網絡欄中搜索一下看是不是從後端傳過來的,結果顯示這個參數是https://index.baidu.com/Interface/ptbk?uniqid=47ec7818050d810e4d02951afcbd38c7的返回。
再搜索一下參數uniqid,發現好幾個請求都有該參數返回且值相同,同時請求數據的接口也返回了該參數。
最後整理一下思路:
程序 api/SearchApi/index接口 Interface/ptbk接口 登錄 請求數據 加密數據、uniqid 參數:uniqid 解密參數ptbk 通過解密參數ptbk解密數據 程序 api/SearchApi/index接口 Interface/ptbk接口
五、代碼實現
導入所需的包
import requests
import pandas as pd
from fake_useragent import UserAgentnt
獲取Cookie
百度指數的請求會驗證用户是否登錄,所以發起請求前需要獲取Cookie信息,百度Cookie的核心是BDUSS,只能自己登錄賬號->開發者工具->應用程序->Cookie->BDUSS然後複製值。
在前面拼接BDUSS=即可構造Cookie
cookies = 'BDUSS=XXXXXXXXXX'
請求數據
1.請求頭及參數構造
網絡欄查看api/SearchApi/index請求,有三個參數:
1.area:省份代碼,默認0表示全國
2.word:由關鍵詞構造的二維數組,形如[[{“name”: “python”, “wordType”: 1}]]
3.days:返回數據是最近多少天,默認30
然後複製請求頭,將Cookie傳入
search_param = {'area': '0', 'word': '[[{"name": "python", "wordType": 1}]]', 'days': '60'}
search_url = 'https://index.baidu.com/api/SearchApi/index?'
headers = {
'Accept': 'application/json, text/plain, */*',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Cipher-Text': '1672733200884_1672792253933_1NLQa+cgc5N2JSNoinHAaMmrDrPtwqHL6D2NHONACx//1P+9YXcg/erBma8ucj43shvH2VsAi3Dzlo9cFfqA3k/PmqixjXJEslJCwNzCzNCVHs+/y7su33mGAxAtFWXrl55rYxzEJNGi4xM6jb4UUibTrVbOl46gKWq/7PVKAIzRyrJbxQP9pKmxECIpO12JbXFrA3leOj8xDZk69P1O/tNU6lD8eMPylUrgCp5k89c9EAD+Q4lgHhsZpTktcKTzKSbrJ5/l0GYNxNS96gEpS/0BnesBc6X52rqE7K4fNzrxm5cfgwbCJx/2+1ayhkI2gUMNDabQ1dnR0hr/NyWxeh7nYvxqarQHsZ+cu3XCt5uEHE4aAPgcXTfDgMsCQOrtMfDGKuxX5PiMDzDODjxSn8cDFRnJ+RMvfPjIIfq2P4k=',
'Connection': 'keep-alive',
'Cookie': cookie,
'Host': 'index.baidu.com',
'Referer': 'https://index.baidu.com/v2/main/index.html',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
'User-Agent': str(UserAgent().random)
}
2.發送請求,解析數據
使用requests發送get請求,獲取加密數據,同時保存uniqid
response = requests.get(search_url, params=search_param, headers=headers)
encrypted_data = response.json()['data']
uniqid = encrypted_data['uniqid']
解密數據
1.獲取解密參數
ptbk_url = f'http://index.baidu.com/Interface/ptbk?uniqid={uniqid}'
ptbk_response = requests.get(ptbk_url, headers=headers)
ptbk = ptbk_response.json()['data']
2.改寫解密函數
JS中的解密函數
Python版本的解密函數
def decrypt(ptbk, encrypted_data):
"""
:param ptbk: 解密參數
:param encrypted_data: 加密數據
:return: 解密後的數據
"""
if not ptbk:
return ""
n = len(ptbk) // 2
d = {ptbk[o]: ptbk[n + o] for o in range(n)}
decrypted_data = [d[data] for data in encrypted_data]
return ''.join(decrypted_data)
順帶把填充零的函數寫了
def fill_zero(data):
"""
:param data: 字符串格式的數據
:return:data為空則返回0,否則返回原數據
"""
if data == '':
return 0
else:
return data
3.解析數據並保存到csv
result = pd.DataFrame(columns=['關鍵詞', '日期', '全部', '電腦端', '移動端'])
for userIndexes_data in encrypted_data['userIndexes']:
word = userIndexes_data['word'][0]['name']
start_date = userIndexes_data['all']['startDate']
end_date = userIndexes_data['all']['endDate']
timestamp_list = pd.date_range(start_date, end_date).to_list()
date_list = [timestamp.strftime('%Y-%m-%d') for timestamp in timestamp_list]
encrypted_data_all = userIndexes_data['all']['data']
decrypted_data_all = [int(fill_zero(data)) for data in decrypt(ptbk, encrypted_data_all).split(',')]
encrypted_data_pc = userIndexes_data['pc']['data']
decrypted_data_pc = [int(fill_zero(data)) for data in decrypt(ptbk, encrypted_data_pc).split(',')]
encrypted_data_wise = userIndexes_data['wise']['data']
decrypted_data_wise = [int(fill_zero(data)) for data in decrypt(ptbk, encrypted_data_wise).split(',')]
df = pd.DataFrame({'關鍵詞': word, '日期': date_list, '全部': decrypted_data_all, '電腦端': decrypted_data_pc, '移動端': decrypted_data_wise})
result = pd.concat([result, df])
result.to_csv('./result.csv',index=False)
擴展
1.同時搜索多個關鍵詞
在word參數中添加多個關鍵詞,最多五個,超過五個只會返回前五個的結果
'word': '[[{"name": "python", "wordType": 1}],[{"name": "java", "wordType": 1}],[{"name": "c", "wordType": 1}],[{"name": "c++", "wordType": 1}],[{"name": "go", "wordType": 1}]'
2.指定時間段
將days參數修改為startDate、endDate這兩個參數
'startDate': '2022-12-01',
'endDate': '2022-12-31'
3.指定省份
可以只搜索某一個省份的數據,將area參數改為相應省份的代碼即可,如北京市的代碼為911,其他省份的代碼可以在步驟四的js文件中找到: