前几天前室友yy询问笔者是否做过百度指数的爬虫,笔者没有尝试过,随即打开百度指数的网站做了一些分析,发现呈现数据的canvas画布上的数据都被加密了(Figure 1)👇
Figure 1 百度搜索指数抓包响应结果
考虑到之前在网易云音乐爬虫编写上有过一些JS逆向解密的经验,正好也有一段时间没有写点爬虫了,并不想用借助selenium驱动浏览器对canvas画布上的折线图进行图像识别来获取数据,想借这个机会再试试JS逆向,可是百度的JS实在是又臭又长,熬了一夜再加整了一天也没搞清楚究竟是在哪里发生解密的(PS:笔者很想知道这种解密JS代码的定位到底有什么比较高效率的方法,比如通过断点调试什么的)。
无奈在同性交友网上找找有没有人做过类似的事情,于是找到了spider-BaiduIndex,笔者一直觉得像这种需要编写解密逻辑的爬虫时效性是很差的,只要稍微修改一下密钥或者加密方法就完全不可行了,该repository截至2020-07-29依然可行。但是longxiaofei@github并没有在README里详细描述爬取思路,笔者在借鉴该脚本后,在本文中将详细解析百度指数爬取的思路,并对spider-BaiduIndex进行一定程度的完善。
声明:如有侵权,私信删除!
问题描述
事实上百度指数搜索结果页面上除了搜索指数,媒体指数与资讯指数三张canvas图表上的数据存在被加密的情况(Figure 2 ~ Figure 7)👇
Figure 2 搜索指数接口URL
Figure 3 搜索指数抓包响应
Figure 4 媒体指数接口URL
Figure 5 媒体指数抓包响应
Figure 6 资讯指数接口URL
Figure 7 资讯指数抓包响应
其他的数据如地域分布以及性别年龄兴趣分布,包括媒体指数具体的新闻来源信息都是没有被加密的(Figure 8 ~ Figure 10),在爬虫获取上是没有太大障碍的,因此不在本文的涉及范围内👇
Figure 8 地域分布抓包响应
Figure 9 性别年龄兴趣分布抓包响应
Figure 10 媒体指数具体相关来源抓包响应
以下列出上述提到的各个接口的URL,以及接口需要提交的参数信息示例,本文主要是对前三个接口(搜索指数:API_SEARCH_INDEX,媒体指数:API_NEWS_INDEX,资讯指数:API_FEEDSEARCH_INDEX)数据获取并解密过程的详细说明,文末附上完整源码👇
API_SEARCH_INDEX = "http://index.baidu.com/api/SearchApi/index?{}".format # 搜索指数查询接口
API_NEWS_INDEX = "http://index.baidu.com/api/NewsApi/getNewsIndex?{}".format # 媒体指数查询接口
API_FEEDSEARCH_INDEX = "http://index.baidu.com/api/FeedSearchApi/getFeedIndex?{}".format # 资讯指数查询接口
API_NEWS_SOURCE = "http://index.baidu.com/api/NewsApi/checkNewsIndex?{}".format # 新闻来源查询接口: 媒体指数数据来源
API_SEARCH_THUMBNAIL = "http://index.baidu.com/api/SearchApi/thumbnail?{}".format # 搜索指数缩略图: 目前我不确定这个数据是用来做什么的, 我猜是搜索指数在很长一段时间内的概况, 因为并不能与Searchapi/index?接口得到的结果匹配上, 而且其指数数据量有几百天, 不太清楚具体是什么
API_INDEX_BY_REGION = "http://index.baidu.com/api/SearchApi/region?{}".format # 搜索指数分地区情况统计
API_INDEX_BY_SOCIAL = "http://index.baidu.com/api/SocialApi/baseAttributes?{}".format # 搜索指数分年龄性别兴趣统计
### API_SEARCH_INDEX参数列表
KWARGS_SEARCH_INDEX = {
"word": json.dumps([[{"name":"围棋","wordType":1}]]), # word: json字符串为二维列表, 第一维是可以比较多组关键词(目前至多5组), 第二维是组合关键词(我理解是指数相加)
"startDate": "2020-01-01", # startDate: 起始日期(包含该日)
"endDate": "2020-06-30", # endDate: 中止日期(包含该日)
"area": 0, # area: 区域编码, 默认0指统计全国指数, 具体省份编号见本文档CODE2PROVINCE
} # * Tips: 可以不传入startDate与endDate而改为days参数, 即获取最近days天的指数
### API_NEWS_INDEX参数列表
KWARGS_NEWS_INDEX = {
"word": json.dumps([[{"name":"围棋","wordType":1}]]), # word: json字符串为二维列表, 第一维是可以比较多组关键词(目前至多5组), 第二维是组合关键词(我理解是指数相加)
"startDate": "2020-01-01", # startDate: 起始日期(包含该日)
"endDate": "2020-06-30", # endDate: 中止日期(包含该日)
"area": 0, # area: 区域编码, 默认0指统计全国指数, 具体省份编号见本文档CODE2PROVINCE
} # * Tips: 可以不传入startDate与endDate而改为days参数, 即获取最近days天的指数
### API_FEEDSEARCH_INDEX参数列表
KWARGS_FEEDSEARCH_INDEX = {
"word": json.dumps([[{"name":"围棋","wordType":1}]]), # word: json字符串为二维列表, 第一维是可以比较多组关键词(目前至多5组), 第二维是组合关键词(我理解是指数相加)
"startDate": "2020-01-01", # startDate: 起始日期(包含该日)
"endDate": "2020-06-30", # endDate: 中止日期(包含该日)
"area": 0, # area: 区域编码, 默认0指统计全国指数, 具体省份编号见本文档CODE2PROVINCE
} # * Tips: 可以不传入startDate与endDate而改为days参数, 即获取最近days天的指数
### API_NEWS_SOURCE参数列表
KWARGS_NEWS_SOURCE = {
"dates[]": "2020-07-02,2020-07-04", # dates[]: 逗号拼接的%Y-%m-%d格式的日期字符串
"type": "day", # type: 默认按日获取
"words": "围棋", # words: 关键词, 该接口应该只能支持单关键词的查询
}
### API_SEARCH_THUMBNAIL参数列表
KWARGS_SEARCH_THUMBNAIL = {
"word": json.dumps([[{"name":"围棋","wordType":1}]]), # word: json字符串为二维列表, 第一维是可以比较多组关键词(目前至多5组), 第二维是组合关键词(我理解是指数相加)
"area": 0, # area: 区域编码, 默认0指统计全国指数, 具体省份编号见本文档CODE2PROVINCE
}
### API_INDEX_BY_REGION参数列表
KWARGS_INDEX_BY_REGION = {
"region": 0, # region: 这个region很可能是可以既可以指省份, 也可以指华东华北这样的大区域的
"word": "围棋,象棋", # word: 多关键词请使用逗号分隔
"startDate": "2020-06-30", # startDate: 起始日期(包含该日)
"endDate": "2020-07-30", # endDate: 中止日期(包含该日)
"days": "", # days: 默认空字符串
}
### API_INDEX_BY_SOCIAL参数列表
KWARGS_INDEX_BY_SOCIAL = {
"wordlist[]": "围棋,象棋", # wordlist[]: 多关键词请使用逗号分隔
}
问题解决
解决获取三种指数问题,本文分以下几点展开:
- 登录百度账号
- 接口参数说明以及注意事项
- JS逆向获取解密逻辑
登录百度账号
百度指数的获取是需要登录百度账号的,为了避开爬虫登录百度账号的问题,我们直接从百度首页进行登录后,随便选取一个域名为www.baidu.com的数据包,取得其中请求头的Cookie信息即可(Figure 11)👇
Figure 11 百度账号登录Cookies获取
为了验证Cookie是否可用,可以使用以下代码进行测试,代码逻辑是通过携带Cookie信息访问百度首页以确定用户名图标,退出登录按钮以及登录按钮是否存在来进行判定,但是随着百度首页的更新可能需要有所修正,之所以选取了三个元素也是增强判定的可信度👇
import math
import json
import requests
from bs4 import BeautifulSoup
from datetime import datetime,timedelta
from baiduindex_config import *
HEADERS = {
"Host": "index.baidu.com",
"Connection": "keep-alive",
"X-Requested-With": "XMLHttpRequest",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:78.0) Gecko/20100101 Firefox/78.0",
}
URL_BAIDU = "https://www.baidu.com/" # 百度首页
def request_with_cookies(url,cookies,timeout=30): # 重写requests.get方法: 百度指数爬虫中大部分request请求都需要设置cookies后访问
headers = HEADERS.copy() # 使用config中默认的伪装头
headers["Cookie"] = cookies # 设置cookies
response = requests.get(url,headers=headers,timeout=timeout) # 发起请求
if response.status_code!=200: raise requests.Timeout # 请求异常: 一般为超时异常
return response # 返回响应内容
def is_cookies_valid(cookies): # 检查cookies是否可用: 通过访问百度首页并检查相关元素是否存在
response = request_with_cookies(URL_BAIDU,cookies) # 使用cookie请求百度首页, 获取响应内容
html = response.text # 获取响应的页面源代码
with open("1.html","w") as f:
f.write(html)
soup = BeautifulSoup(html,"lxml") # 解析页面源代码
flag1 = soup.find("a",class_="quit") is not None # "退出登录"按钮(a标签)是否存在
flag2 = soup.find("span",class_="user-name") is not None # 用户名(span标签)是否存在
flag3 = soup.find("a",attrs={"name":"tj_login"}) is None # "登录"按钮(a标签)是否存在
return flag3 and (flag1 or flag2) # cookies可用判定条件: 登录按钮不存在且用户名与退出登录按钮至少存在一个
接口参数说明以及注意事项
通过简单抓包观察可以发现,搜索指数:API_SEARCH_INDEX,媒体指数:API_NEWS_INDEX,资讯指数:API_FEEDSEARCH_INDEX的参数都是相同的👇
import json
KWARGS_SEARCH_INDEX = {
"word": json.dumps([[{"name":"围棋","wordType":1}]]), # word: json字符串为二维列表, 第一维是可以比较多组关键词(目前至多5组), 第二维是组合关键词(我理解是指数相加)
"startDate": "2020-01-01", # startDate: 起始日期(包含该日)
"endDate": "2020-06-30", # endDate: 中止日期(包含该日)
"area": 0, # area: 区域编码, 默认0指统计全国指数, 具体省份编号见本文档CODE2PROVINCE
}
以下分别对每个参数的说明以及注意点:
参数word
Figure 12 word参数来源
参数word是一个json格式的二元列表,因为百度指数支持多关键词指数查询以及不同关键词之间的对比查询(Figure 12),目前支持至多5组关键词的对比,每组关键词的数量不超过3,因此需要对传入的关键词组进行有效性检查👇
MAX_KEYWORD_GROUP_NUMBER = 5 # 最多可以将MAX_KEYWORD_GROUP_NUMBER组关键词进行比较
MAX_KEYWORD_GROUP_SIZE = 3 # 最多可以将MAX_KEYWORD_GROUP_SIZE个关键词进行组合
def is_keywords_valid(keywords): # 检查关键词集合是否合法: 满足最大的关键词数量限制, keywords为二维列表
flag1 = len(keywords)<=MAX_KEYWORD_GROUP_NUMBER # MAX_KEYWORD_GROUP_NUMBER
flag2 = max(map(len,keywords))<=MAX_KEYWORD_GROUP_SIZE # MAX_KEYWORD_GROUP_SIZE
return flag1 and flag2
is_keywords_valid([['拳皇','街霸','铁拳'],['英雄联盟'],['围棋'],['崩坏'],['明日方舟']])
此外并非每个关键词百度指数都有收录,通过抓包可以看到在指数查询前会有对该关键词的存在性进行检查的接口调用(Figure 13),可以查询的关键词(如计算机)与无法查询的关键词(如囚生CY)如下所示(Figure 14 & Figure 15)👇
Figure 13 关键词存在性接口URL
Figure 14 百度指数收录的关键词检查结果
Figure 15 百度指数未收录的关键词检查结果
由上图发现可以查询的关键词响应结果的result字段为空列表,而不可查询的关键词响应结果的result字段是非空的,通过这个规律我们可以对每个关键词进行预筛选(注意一定要在登录状态下访问该check接口,否则返回结果的message字段将会是not login)👇
API_BAIDU_INDEX_EXIST = "http://index.baidu.com/api/AddWordApi/checkWordsExists?word={}".format # 百度指数关键词是否存在检查 def is_keyword_existed(keyword,cookies): # 检查需要查询百度指数的关键词是否存在: keyword为单个关键词字符串 response = request_with_cookies(API_BAIDU_INDEX_EXIST(keyword),cookies) json_response = json.loads(response.text) if json_response["message"]=="not login": # message=="not login"表明cookies已经失效 raise Exception("Cookies has been expired !") flag1 = len(json_response["data"])==1 # 正常关键词响应结果的data字段里只有一个字段值: result flag2 = len(json_response["data"]["result"])==0 # 且result字段值应该是空列表 #print(flag1) #print(flag2) return flag1 or flag2 # 二者满足其一即认为关键词有效
参数startDate与endDate
在Figure 2 Figure 4 Figure 6中我们看到请求参数并没有startDate与endDate,而是days,这是因为默认显示的指数时间跨度为最近30天,需要在百度指数页面上自定义设置后再抓包就可以看到这两个请求参数(Figure 16)👇
Figure 16 百度指数自定义时间跨度
这里需要注意的是,时间跨度的天数超过366天(截至20200731测试结果)后,指数的折现图将不再按照每天展示,而更改为每周显示(Figure 17 & Figure 18)👇
Figure 17 跨度为365天的指数结果(20190101~20191231)
Figure 17 跨度为367天的指数结果(20190101~20200102)
指数数据是按日或按周可以通过响应结果中的type字段看出,如果是按周,则type字段值将为week👇(Figure 18)
Figure 18 API响应结果判定by day或by week
出于可适性考虑笔者建议不要使得时间跨度超过一年(366天),使用按日的数据总是要比按周的要更划算的。
参数area
参数area的默认值为0,表示统计全国的指数,也可以分地区考虑,通过查询文件名开头为main_vendor的JS文件可以找到全国各个区域,各个省份,各个城市的编号(Figure 19 ~ Figure 21)👇
Figure 19 main_vendor.js抓包
Figure 20 main_vendor.js中的省份编号
Figure 21 main_vendor.js中的区域编号与城市编号
简单点直接从JS里复制下来用就可以了👇
# 1. 百度指数js常用变量 ## 1.1 省份区域索引表: CODE2AREA ## - CODE2PROVINCE来源: main-vendor.xxxxxxxxxxxxxxxxxxx.js源码中直接复制可得 ## - js源码URL: e.g. http://index.baidu.com/v2/static/js/main-vendor.dbf1ed721cfef2e2755d.js CODE2AREA = { 901:"华东",902:"西南",903:"华中",904:"西南",905:"华北", 906:"华中",907:"东北",908:"华中",909:"华东",910:"华东", 911:"华北",912:"华南",913:"华南",914:"西南",915:"西南", 916:"华东",917:"华东",918:"西北",919:"西北",920:"华北", 921:"东北",922:"东北",923:"华北",924:"西北",925:"西北", 926:"西北",927:"华中",928:"华东",929:"华北",930:"华南", 931:"华南",932:"西南",933:"华南",934:"华南", } ## 1.2 省份编号索引表: CODE2PROVINCE, PROVINCE2CODE ## - CODE2PROVINCE来源: main-vendor.xxxxxxxxxxxxxxxxxxx.js源码中直接复制可得 ## - js源码URL: e.g. http://index.baidu.com/v2/static/js/main-vendor.dbf1ed721cfef2e2755d.js CODE2PROVINCE = { 901:"山东",902:"贵州",903:"江西",904:"重庆",905:"内蒙古", 906:"湖北",907:"辽宁",908:"湖南",909:"福建",910:"上海", 911:"北京",912:"广西",913:"广东",914:"四川",915:"云南", 916:"江苏",917:"浙江",918:"青海",919:"宁夏",920:"河北", 921:"黑龙江",922:"吉林",923:"天津",924:"陕西",925:"甘肃", 926:"新疆",927:"河南",928:"安徽",929:"山西",930:"海南", 931:"台湾",932:"西藏",933:"香港",934:"澳门", } PROVINCE2CODE = {province:str(code) for code,province in CODE2PROVINCE.items()} ## 1.3 城市编号索引表: CODE2CITY, CITY2CODE ## - CODE2PROVINCE来源: main-vendor.xxxxxxxxxxxxxxxxxxx.js源码中直接复制可得 ## - js源码URL: e.g. http://index.baidu.com/v2/static/js/main-vendor.dbf1ed721cfef2e2755d.js CODE2CITY = { 1:"济南",2:"贵阳",3:"黔南",4:"六盘水",5:"南昌", 6:"九江",7:"鹰潭",8:"抚州",9:"上饶",10:"赣州", 11:"重庆",13:"包头",14:"鄂尔多斯",15:"巴彦淖尔",16:"乌海", 17:"阿拉善盟",19:"锡林郭勒盟",20:"呼和浩特",21:"赤峰",22:"通辽", 25:"呼伦贝尔",28:"武汉",29:"大连",30:"黄石",31:"荆州", 32:"襄阳",33:"黄冈",34:"荆门",35:"宜昌",36:"十堰", 37:"随州",38:"恩施",39:"鄂州",40:"咸宁",41:"孝感", 42:"仙桃",43:"长沙",44:"岳阳",45:"衡阳",46:"株洲", 47:"湘潭",48:"益阳",49:"郴州",50:"福州",51:"莆田", 52:"三明",53:"龙岩",54:"厦门",55:"泉州",56:"漳州", 57:"上海",59:"遵义",61:"黔东南",65:"湘西",66:"娄底", 67:"怀化",68:"常德",73:"天门",74:"潜江",76:"滨州", 77:"青岛",78:"烟台",79:"临沂",80:"潍坊",81:"淄博", 82:"东营",83:"聊城",84:"菏泽",85:"枣庄",86:"德州", 87:"宁德",88:"威海",89:"柳州",90:"南宁",91:"桂林", 92:"贺州",93:"贵港",94:"深圳",95:"广州",96:"宜宾", 97:"成都",98:"绵阳",99:"广元",100:"遂宁",101:"巴中", 102:"内江",103:"泸州",104:"南充",106:"德阳",107:"乐山", 108:"广安",109:"资阳",111:"自贡",112:"攀枝花",113:"达州", 114:"雅安",115:"吉安",117:"昆明",118:"玉林",119:"河池", 123:"玉溪",124:"楚雄",125:"南京",126:"苏州",127:"无锡", 128:"北海",129:"钦州",130:"防城港",131:"百色",132:"梧州", 133:"东莞",134:"丽水",135:"金华",136:"萍乡",137:"景德镇", 138:"杭州",139:"西宁",140:"银川",141:"石家庄",143:"衡水", 144:"张家口",145:"承德",146:"秦皇岛",147:"廊坊",148:"沧州", 149:"温州",150:"沈阳",151:"盘锦",152:"哈尔滨",153:"大庆", 154:"长春",155:"四平",156:"连云港",157:"淮安",158:"扬州", 159:"泰州",160:"盐城",161:"徐州",162:"常州",163:"南通", 164:"天津",165:"西安",166:"兰州",168:"郑州",169:"镇江", 172:"宿迁",173:"铜陵",174:"黄山",175:"池州",176:"宣城", 177:"巢湖",178:"淮南",179:"宿州",181:"六安",182:"滁州", 183:"淮北",184:"阜阳",185:"马鞍山",186:"安庆",187:"蚌埠", 188:"芜湖",189:"合肥",191:"辽源",194:"松原",195:"云浮", 196:"佛山",197:"湛江",198:"江门",199:"惠州",200:"珠海", 201:"韶关",202:"阳江",203:"茂名",204:"潮州",205:"揭阳", 207:"中山",208:"清远",209:"肇庆",210:"河源",211:"梅州", 212:"汕头",213:"汕尾",215:"鞍山",216:"朝阳",217:"锦州", 218:"铁岭",219:"丹东",220:"本溪",221:"营口",222:"抚顺", 223:"阜新",224:"辽阳",225:"葫芦岛",226:"张家界",227:"大同", 228:"长治",229:"忻州",230:"晋中",231:"太原",232:"临汾", 233:"运城",234:"晋城",235:"朔州",236:"阳泉",237:"吕梁", 239:"海口",241:"万宁",242:"琼海",243:"三亚",244:"儋州", 246:"新余",253:"南平",256:"宜春",259:"保定",261:"唐山", 262:"南阳",263:"新乡",264:"开封",265:"焦作",266:"平顶山", 268:"许昌",269:"永州",270:"吉林",271:"铜川",272:"安康", 273:"宝鸡",274:"商洛",275:"渭南",276:"汉中",277:"咸阳", 278:"榆林",280:"石河子",281:"庆阳",282:"定西",283:"武威", 284:"酒泉",285:"张掖",286:"嘉峪关",287:"台州",288:"衢州", 289:"宁波",291:"眉山",292:"邯郸",293:"邢台",295:"伊春", 297:"大兴安岭",300:"黑河",301:"鹤岗",302:"七台河",303:"绍兴", 304:"嘉兴",305:"湖州",306:"舟山",307:"平凉",308:"天水", 309:"白银",310:"吐鲁番",311:"昌吉",312:"哈密",315:"阿克苏", 317:"克拉玛依",318:"博尔塔拉",319:"齐齐哈尔",320:"佳木斯",322:"牡丹江", 323:"鸡西",324:"绥化",331:"乌兰察布",333:"兴安盟",334:"大理", 335:"昭通",337:"红河",339:"曲靖",342:"丽江",343:"金昌", 344:"陇南",346:"临夏",350:"临沧",352:"济宁",353:"泰安", 356:"莱芜",359:"双鸭山",366:"日照",370:"安阳",371:"驻马店", 373:"信阳",374:"鹤壁",375:"周口",376:"商丘",378:"洛阳", 379:"漯河",380:"濮阳",381:"三门峡",383:"阿勒泰",384:"喀什", 386:"和田",391:"亳州",395:"吴忠",396:"固原",401:"延安", 405:"邵阳",407:"通化",408:"白山",410:"白城",417:"甘孜", 422:"铜仁",424:"安顺",426:"毕节",437:"文山",438:"保山", 456:"东方",457:"阿坝",466:"拉萨",467:"乌鲁木齐",472:"石嘴山", 479:"凉山",480:"中卫",499:"巴音郭楞",506:"来宾",514:"北京", 516:"日喀则",520:"伊犁",525:"延边",563:"塔城",582:"五指山", 588:"黔西南",608:"海西",652:"海东",653:"克孜勒苏柯尔克孜",654:"天门仙桃", 655:"那曲",656:"林芝",657:"None",658:"防城",659:"玉树", 660:"伊犁哈萨克",661:"五家渠",662:"思茅",663:"香港",664:"澳门", 665:"崇左",666:"普洱",667:"济源",668:"西双版纳",669:"德宏", 670:"文昌",671:"怒江",672:"迪庆",673:"甘南",674:"陵水黎族自治县", 675:"澄迈县",676:"海南",677:"山南",678:"昌都",679:"乐东黎族自治县", 680:"临高县",681:"定安县",682:"海北",683:"昌江黎族自治县",684:"屯昌县", 685:"黄南",686:"保亭黎族苗族自治县",687:"神农架",688:"果洛",689:"白沙黎族自治县", 690:"琼中黎族苗族自治县",691:"阿里",692:"阿拉尔",693:"图木舒克", } CITY2CODE = {city:str(code) for code,city in CODE2CITY.items()}
例如实际使用中将area参数替换成901就可以获得山东省的指数数据。
JS逆向获取解密逻辑
最后就是应当如何处理Figure 3 Figure 5 Figure 7中红框里的加密数据了,我们再重新仔细看一遍Figure 18👇
Figure 20 重新查看Figure 18
红框里的uniqid字段非常关键,现在我们还不能知道这个字段究竟是用于做什么的。
这里可以看到加密数据显然不可能是通用的标准加密算法得到的,因为显然加密结果并非随机,加密结果中频繁且规律性地出现相同字符。笔者起初试图在JS中找到解密算法的逻辑,但是百度指数的JS实在是又臭又长,没有指向性地去读JS实在是非人地折磨,后来又想通过找规律的方法试出这个加密算法,又无所得。最后终于还是在文件名以main.开头的JS文件中找到了加密逻辑(Figure 21 & Figure 22)👇
Figure 21 main.xxxxxxxxxxxxx.js抓包
Figure 22 main.xxxxxxxxxxxxx.js中的解密逻辑代码
其实笔者得出一个小经验,如果想在JS里找到加解密的逻辑,先试试搜索关键词encrypt和decrypt,如果写JS的码农还是个人(假设他不能是个狗),他应该不会给加解密函数随便起个没意义的名字(不过网易云爬虫里面涉及加解密的那部分还真是用abcdef来命名加解密函数的,不过好在网易云的JS很短,很容易就查到在哪里加解密了)。
言归正传,Figure 22划线部分即为解密逻辑,function(t,e)中变量t显然是Figure 3 Figure 5 Figure 7里那一长串的字符,变量e则是解密的密钥,可以将划线部分用python代码复现👇
def decrypt_index(key,data): # 指数数据解密 cipher2plain = {} # 密文明文映射字典 plain_chars = [] # 解密得到的明文字符 for i in range(len(key)//2): cipher2plain[key[i]] = key[len(key)//2+i] for i in range(len(data)): plain_chars.append(cipher2plain[data[i]]) index_list = "".join(plain_chars).split(",") # 所有明文字符拼接后按照逗号分隔 return index_list
可以看出密钥key其实可以转化为一个映射字典,可以将data里的每个字符映射成实际的数据,其实就是一个非常简单的映射加密(想不到百度竟然都不用标准的加密算法来加密,实在是懒得不行)。问题是如何取得密钥key?
这时候我们一个个检查XHR中的抓包结果,终于可以在一个文件名以ptbk?uniqid开头的数据包中找到线索(Figure 23 & Figure 24)👇
Figure 23 密钥请求接口URL
Figure 24 密钥请求接口响应
可以看到该数据包的请求用到了Figure 20中的uniqid,对比Figure 24中的响应data与Figure 20中那些加密后的字符串,发现加密字符串中的字符无一例外都可以在Figure 24中的响应的data中找到。有理由相信这个data就是可以用于解密的密钥👇
print(decrypt_index("RN7.xgl8EPKy%BD4.%209,8-5+3716","""Bxyx%lBx.g.lBxBRPlg.DglgRRRlg%x%lBxB%xlggRylgg.ylggPDlgPxRlgx.8lgy88lBxBgglBxx8DlBx.8ylggxPlgP%xlgBxDlgxDylBx.%xlBx.g%lBxygglB.yBxlByxRBlB.%8glBBgDRlByyD%lByx%.lB.gDx""")) # 输出 """ ['10307', '10292', '10145', '9269', '9444', '9707', '10170', '9943', '9923', '9956', '9504', '9028', '9388', '10199', '10086', '10283', '9905', '9570', '9106', '9063', '10270', '10297', '10399', '12310', '13041', '12789', '11964', '13367', '13072', '12960'] """
至此,百度指数爬虫大部分的问题要点已经解决。
源码获取加群:1136192749
原作者:囚生CY
如有侵权联系小编删除!