一、相关环境
开发工具: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,发现好几个请求都有该参数返回且值相同,同时请求数据的接口也返回了该参数。
最后整理一下思路:
五、代码实现
导入所需的包
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文件中找到: