需求:php
中国空气质量在线监测分析平台是一个收录全国各大城市天气数据的网站,包括温度、湿度、PM 2.五、AQI 等数据,连接为:https://www.aqistudy.cn/html/city_detail.html,网站显示为:html
该网站全部的空气质量数据都是基于图表进行显示的,而且都是触发鼠标滑动或者点动后才会显示某点的数据,因此若是基于selenium进行数据爬取很吃力,所以考虑采用requests模块进行数据爬取.node
首先要找到空气质量数据所在的数据包:python
使用抓包工具抓取,通过尝试发现,只有设置的图中设置项发生了变化,而后点击查询按钮,在抓包工具中才会捕获到两个ajax请求的数据包,判断须要爬取的数据存在于该数据包中.ajax
经过这两个 aqistudyapi.php 数据包发现,ajax请求的url为 https://www.aqistudy.cn/apinew/aqistudyapi.php ,请求方式为POST,两个ajax请求均发送了一个参数(d),可是其对应的值让我看不懂,而且两个的值不一样,考虑是两次提交的参数不相同,而且进行了加密.json
接着去看ajax请求的响应数据,发现也是一串加密后的字符串api
那么问题来了,ajax请求提交的数据和返回的响应数据都是通过加密的浏览器
解决:app
如今已经直到,两个aqistudyapi.php的ajax请求都是在修改设置而且点击查询按钮后触发的,也就是说查询按钮上必定绑定了某个点击事件,由这个点击事件发送了对应的ajax请求,首先经过浏览器查看该查询按钮绑定的事件栈函数
点击到事件对应的js代码,发现事件执行了getData()函数
接下来,搜索getData,找到该函数的js代码
接下来,搜索getAQIData函数,发现getWeatherData函数就在其下方,真是贴心
通过分析后发现,两个函数都调用了getServerData函数,而且传入了method,type,函数,0.5四个参数,此时思考一下,既然没有其他的代码,那么ajax请求是如何发送的呢,显而易见是由getServerData函数发送的,而且传入的函数看起来像是对obj.data进行了一些列的分析展现
那么,让咱们去找一下getServerData函数,首先局部搜索发现没有这个函数,不碍事,让咱们来全局搜索一下看看
JavaScript 混淆: 咱们会惊讶的发现getServerData后面跟的是什么鬼啊?不符合js函数定义的写法呀!看不懂呀!其实这里是通过 JavaScript 混淆加密了,混淆加密以后,代码将变为人不可读的形式,可是功能是彻底一致的,这是一种常见的 JavaScript 加密手段.咱们想要查看到该方法的原始实现则必须对其进行反混淆.
JavaScript 反混淆: JavaScript 混淆以后,实际上是有反混淆方法的,最简单的方法即是搜索在线反混淆网站,例如 http://www.bm8.com.cn/jsConfusion/ ,将getServerData存在的这行数据粘贴到反混淆网站中
来分析一下getServerData函数,发现它判断当前页面数据后,发起了一个ajax请求(终于找到了!),调用getParam函数,传入了两个参数(method和object),将getParam函数的返回值赋值给param,而且做为ajax请求的参数(d)对应的值发送到了../apinew/aqistudyapi.php
,咱们看到的乱码同样的参数就是这个函数产生的.
接下来,使用decodeData函数对响应数据进行了解密,而后使用json反序列化,将反序列化的结果赋值给obj,而后调用以前传入的callback进行分析
回想一下getServerData有四个参数分别是:
method: GETDETAIL或 GETCITYWEATHER
object: param对象,是个字典,有四个属性
callback: 回调函数,用于分析解密后的ajax请求响应
period: 0.5
接下来分别找一下getParam和decodeData函数
function decodeData(data) { data = AES.decrypt(data, aes_server_key, aes_server_iv); data = DES.decrypt(data, des_key, des_iv); data = BASE64.decrypt(data); return data } // 发现decodeData的内部分别使用了AES,DES,BASE64对响应数据进行解密
var getParam = (function () { function ObjectSort(obj) { var newObject = {}; Object.keys(obj).sort().map(function (key) { newObject[key] = obj[key] }); return newObject } return function (method, obj) { var appId = '1a45f75b824b2dc628d5955356b5ef18'; var clienttype = 'WEB'; var timestamp = new Date().getTime(); var param = { appId: appId, method: method, timestamp: timestamp, clienttype: clienttype, object: obj, secret: hex_md5(appId + method + timestamp + clienttype + JSON.stringify(ObjectSort(obj))) }; param = BASE64.encrypt(JSON.stringify(param)); return AES.encrypt(param, aes_client_key, aes_client_iv) } })(); // getParam的内部时使用了BASE64和AES对请求数据进行了加密
总结:
点击查询按钮后,触发的事件最终执行了 getServerData 函数,发起了ajax请求,请求的参数是经过getParam(method,object)进行加密的,响应的数据是经过decodeData(data)进行解密的.
接下来,须要借助于 PyExecJS模块来实现模拟JavaScript代码执行,获取动态加密的请求参数,而后再将加密的响应数据传入decodeData进行解密
PyExecJS介绍: PyExecJS 是一个能够使用 Python 来模拟运行 JavaScript 的库.
环境安装:
接下来,然咱们一步一步的来实现
将反混淆网站中的代码粘贴到code.js文件中
在该js文件中添加一个自定义函数getPostParamCode,该函数是为了获取且返回post请求的动态加密参数
function getPostParamCode(method, city, type, startTime, endTime){ // 封装getParam函数所需的参数 var param = {}; param.city = city; param.type = type; param.startTime = startTime; param.endTime = endTime; return getParam(method, param); }
import execjs # 实例化一个对象 node = execjs.get() # 建立参数 method = 'GETCITYWEATHER' city = '北京' c_type = 'HOUR' start_time = '2019-10-09 00:00:00' end_time = '2019-10-11 23:00:00' # 编译须要的js代码 file_path = './code.js' js_obj = node.compile(open(file_path, encoding='utf-8').read()) # 获取加密的参数 js_code = 'getPostParamCode("{0}", "{1}", "{2}", "{3}", "{4}")'.format(method, city, c_type, start_time, end_time) params = js_obj.eval(js_code) print(params)
import execjs import requests # 实例化一个对象 node = execjs.get() # 建立参数 method = 'GETCITYWEATHER' city = '北京' c_type = 'HOUR' start_time = '2019-10-09 00:00:00' end_time = '2019-10-11 23:00:00' # 编译须要的js代码 file_path = './code.js' js_obj = node.compile(open(file_path, encoding='utf-8').read()) # 获取加密的参数 js_code = 'getPostParamCode("{0}", "{1}", "{2}", "{3}", "{4}")'.format(method, city, c_type, start_time, end_time) params = js_obj.eval(js_code) # 发送请求 url = 'https://www.aqistudy.cn/apinew/aqistudyapi.php' response_text = requests.post(url, data={'d': params}).text print(response_text)
import execjs import requests # 实例化一个对象 node = execjs.get() # 建立参数 method = 'GETCITYWEATHER' city = '北京' c_type = 'HOUR' start_time = '2019-10-09 00:00:00' end_time = '2019-10-11 23:00:00' # 编译须要的js代码 file_path = './code.js' js_obj = node.compile(open(file_path, encoding='utf-8').read()) # 获取加密的参数 js_code = 'getPostParamCode("{0}", "{1}", "{2}", "{3}", "{4}")'.format(method, city, c_type, start_time, end_time) params = js_obj.eval(js_code) # 发送请求,获取响应数据 url = 'https://www.aqistudy.cn/apinew/aqistudyapi.php' response_text = requests.post(url, data={'d': params}).text # 对响应数据进行解密 js_decode_data = 'decodeData("{}")'.format(response_text) decode_data = js_obj.eval(js_decode_data) print(decode_data)
至此,完成了对空气质量数据的爬取.