使用execjs破解某监测分析平台

一、爬虫需求

  • 网址:https://www.aqistudy.cn/html/city_detail.html
  • 准备:火狐浏览器,chrom或qq浏览器,安装pyexecjs
  • 要求:我们要实现指定一个城市和指定的时间范围,然后获取图表中的数据

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3g9VQ36V-1600745096517)(C:\Users\qinfan\AppData\Roaming\Typora\typora-user-images\1600735971550.png)]

二、爬虫分析

1.确定数据来源

  • 该网站所有的空气质量数据都是基于图表进行显示的,并且都是出发鼠标滑动或者点动后才会显示某点的数据,所以如果基于selenium进行数据爬取也是挺吃力的,因此我们采用requests模块进行数据爬取。
  • 当我们指定城市和时间范围,点击搜索后,发现页面网址没变,并没有全局刷新,因此得出结论:此页面为ajax传输数据。

2.确定加密参数

  • 点击捕获后的ajax数据包,发现请求方式为post请求,并且每次请求携带一个密文参数d

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QqgcJDET-1600745175966)(C:\Users\qinfan\AppData\Roaming\Typora\typora-user-images\1600736161209.png)]

  • 分析一下,我们ajax请求的作用一般就是从后台拿数据,然后通过回调函数,将数据展示。此处大概流程就是ajax从后端拿到加密的数据,然后回调函数进行解密,然后将数据渲染至页面。 因此我们需要做的就是寻找对应的ajax事件,寻找对应的回调函数即可。

  • 我们可以通过火狐浏览器去检测该查询按钮上到底绑定了哪些事件且是否发起了ajax请求呢?火狐浏览器可以分析页面某个元素的绑定事件以及定位到具体的代码在哪一行。

3.寻找解密参数的ajax回调方法

  • 通过火狐查看页面元素。我们设置完搜索条件,点击搜索按钮,因此大概率是通过点击事件来触发的ajax
    在这里插入图片描述
  • 定位到回调函数为getData后,我们跳转到该函数,然后ctrl+f寻找具体的getData函数的定义。
<script>
function getData()
       {
       	  state = 0;
    		  city=$('#city').val();
    		  $.cookie('dcity', city, {expires : 30});
    		  //type = $('#type').combobox('getValue');
         	  type = $('input:radio[name=type]:checked').val();
    		  getTimeSel();

    		  if(type=="HOUR")
    		  {
    		  	 var timediff = converTimeFormat($('#dtbEndTime').datetimebox('getValue')).getTime()-converTimeFormat($('#dtbStartTime').datetimebox('getValue')).getTime();
    		  	 if(timediff >30*24*3600*1000)
    		  	 {
    		  	 	showMessage(false,"按小时查询仅支持查询一个月数据,查看长时间变化趋势请选择按日查询!");
    		  	 	return ;
    		  	 }
    		  }
    		  getAQIData();
    		  getWeatherData();
       }
    </script>
  • 该函数没有写到数据如何解密,但是调用了getAQIData()getWeatherData()两个函数,因此解密应该在这两个函数中,我们往下ctrl+f搜索这两个函数
<script>
function getAQIData(){
         var method = 'GETDETAIL';
         var param = {};
         param.city = city;
         param.type = type;
         param.startTime = startTime;
         param.endTime = endTime;
         getServerData(method, param, function(obj) {
            ...
         }, 0.5);
       }
----------------------------------------------------
 function getWeatherData(){
         var method = 'GETCITYWEATHER';
         var param = {};
         param.city = city;
         param.type = type;
         param.startTime = startTime;
         param.endTime = endTime;
         getServerData(method, param, function(obj) {
            ...
         }, 0.5);
       }
   </script>

我们看到这两个函数写法大概相同,也没有解密的方法。里面大概分为两个部分,第一部分为定义了参数,分别对应了我们的搜索条件:城市,类型,时间范围

param.city = city;
param.type = type; # 而我们此处的type由上一个getData得知type=="HOUR"
param.startTime = startTime;
param.endTime = endTime;

然后第二部分是一个getServerData函数的调用,里面有四个参数,分别是:method,param的四个参数,一个函数,0.5,由于这两个方法没有解密方法,但是同时调用了getServerData这个方法,因此我们去寻找这个方法。ctrl+f去寻找该方法,发现这个js文件中没有,因此我们需要ctrl+shift+f进行全局搜索。
在这里插入图片描述

4. js混淆,解密参数

  • js混淆:定位到了这个js,J我们会惊讶的发现getServerData后面的代码不符合js函数定义的写法!其实这里是经过 JavaScript 混淆加密了,混淆加密之后,代码将变为不可读的形式,但是功能是完全一致的,这是一种常见的 JavaScript 加密手段。我们想要查看到该方法的原始实现则必须对其进行反混淆。
  • 反混淆:JavaScript 混淆之后,其实是有反混淆方法的,最简单的方法便是搜索在线反混淆网站,这里提供一个:http://www.bm8.com.cn/jsConfusion/。我们可以将getServerData存在的这一行代码粘贴到反混淆的网站中。
    在这里插入图片描述
  • 将代码复制出到js编辑器,寻找到对应的getServerData
    在这里插入图片描述
  • 在反混淆后,我们很清晰的看到了ajax请求发送的实现。然后还看到了ajax对应post请求的动态加密请求参数的加密方法getParam(),并且将method和object作为了函数的参数。method和object是从getServerData函数的参数中获取的,那么getServerData函数中的method和object表示的是什么呢?我们需要回过头去查看getServerData函数的调用
    在这里插入图片描述
  • 发现method是固定形式字符串,object就是param是一个字典,里面存储了三组键值对city表示查询城市名称,startTime和endTime为查询起止时间,type表示为HOUR:
    在这里插入图片描述
  • 至此getParam()函数中的两个参数的表示含义我们已经清楚了。getParam函数的返回值就是ajax对应post请求的动态加密请求参数了,我们需要定位到其函数内部的实现,看看是如何对请求参数进行加密的。在getServerData中我们发现了ajax请求对应的操作代码,其中还有一个非常重要的一步,就是ajax请求成功后的回调函数实现内部,接受到了响应数据data,data我们知道是一组密文数据,然后调用了decodeData对data进行了解密操作:
    在这里插入图片描述
  • 在反混淆网站的代码中我们可以搜索到getPrame和decodeData这两个函数的实现:
    在这里插入图片描述
  • 现getParam函数中使用了 Base64 和 AES 对param进行加密。加密之后的字符串便作为ajax对应post的请求参数传送给服务器了。
    在这里插入图片描述
    服务器相应回来的密文数据是被decodeData进行解密的。观察解密函数发现是通过base64+AES+DES进行的解密!

5.分析总结

点击查询按钮后,最终是触发了getServerData函数发起了ajax请求,请求参数是通过getParam进行的加密,响应回来的密文数据是通过decodeData函数进行解密处理的。

三、代码编写

1.安装 PyExecJS库

我们需要借助于 PyExecJS 库来实现模拟JS代码执行获取动态加密的请求参数,然后再将加密的响应数据带入decodeData进行解密即可!

PyExecJS介绍:PyExecJS 是一个可以使用 Python 来模拟运行 JavaScript 的库。我们需要pip install PyExecJS对其进行环境安装。

2.代码编写

  • 将反混淆网站中的代码粘贴到jsCode.js文件
  • 在该js文件中添加一个自定义函数getPostParamCode,该函数是为了获取且返回post请求的动态加密参数:
function getPostParamCode(method, city, type, startTime, endTime){
    var param = {};
    param.city = city;
    param.type = type;
    param.startTime = startTime;
    param.endTime = endTime;
    return getParam(method, param);
}
  • py文件

    import execjs
    import requests
    
    node = execjs.get()
     
    # 定义参数
    method = 'GETCITYWEATHER'
    city = '北京'
    type = 'HOUR'
    start_time = '2018-01-25 00:00:00'
    end_time = '2018-01-25 23:00:00'
     
    # 编译js代码
    file = 'jsCode.js'
    ctx = node.compile(open(file).read())
     
    # 像js代码传入参数
    js = 'getPostParamCode("{0}", "{1}", "{2}", "{3}", "{4}")'.format(method, city, type, start_time, end_time)
    params = ctx.eval(js)
    
    # 发起post请求
    url = 'https://www.aqistudy.cn/apinew/aqistudyapi.php'
    response_text = requests.post(url, data={'d': params}).text
    
    #对加密的响应数据进行解密
    js = 'decodeData("{0}")'.format(response_text)
    decrypted_data = ctx.eval(js)
    print(decrypted_data)
    

结果:

{"success":true,"errcode":0,"errmsg":"success","result":{"success":true,"data":{"total":24,"rows":[{"time":"2018-01-25 00:00:00","temp":"-7","humi":"35","wse":"1","wd":"\u4e1c\u5317\u98ce","tq":"\u6674"},{"time":"2018-01-25 01:00:00","temp":"-9","humi":"38","wse":"1","wd":"\u897f\u98ce","tq":"\u6674"},{"time":"2018-01-25 02:00:00","temp":"-10","humi":"40","wse":"1","wd":"\u4e1c\u5317\u98ce","tq":"\u6674"},{"time":"2018-01-25 03:00:00","temp":"-8","humi":"27","wse":"2","wd":"\u4e1c\u5317\u98ce","tq":"\u6674"},{"time":"2018-01-25 04:00:00","temp":"-8","humi":"26","wse":"2","wd":"\u4e1c\u98ce","tq":"\u6674"},{"time":"2018-01-25 05:00:00","temp":"-8","humi":"23","wse":"2","wd":"\u4e1c\u5317\u98ce","tq":"\u6674"},{"time":"2018-01-25 06:00:00","temp":"-9","humi":"27","wse":"2","wd":"\u4e1c\u5317\u98ce","tq":"\u591a\u4e91"},{"time":"2018-01-25 07:00:00","temp":"-9","humi":"24","wse":"2","wd":"\u4e1c\u5317\u98ce","tq":"\u591a\u4e91"},{"time":"2018-01-25 08:00:00","temp":"-9","humi":"25","wse":"2","wd":"\u4e1c\u98ce","tq":"\u6674\u8f6c\u591a\u4e91\u8f6c\u591a\u4e91\u95f4\u6674"},{"time":"2018-01-25 09:00:00","temp":"-8","humi":"21","wse":"3","wd":"\u4e1c\u5317\u98ce","tq":"\u6674\u8f6c\u591a\u4e91\u8f6c\u591a\u4e91\u95f4\u6674"},{"time":"2018-01-25 10:00:00","temp":"-7","humi":"19","wse":"3","wd":"\u4e1c\u5317\u98ce","tq":"\u6674\u8f6c\u591a\u4e91\u8f6c\u591a\u4e91\u95f4\u6674"},{"time":"2018-01-25 11:00:00","temp":"-6","humi":"18","wse":"3","wd":"\u4e1c\u5317\u98ce","tq":"\u591a\u4e91"},{"time":"2018-01-25 12:00:00","temp":"-6","humi":"17","wse":"3","wd":"\u4e1c\u5317\u98ce","tq":"\u591a\u4e91"},{"time":"2018-01-25 13:00:00","temp":"-5","humi":"17","wse":"2","wd":"\u4e1c\u5317\u98ce","tq":"\u591a\u4e91"},{"time":"2018-01-25 14:00:00","temp":"-5","humi":"16","wse":"2","wd":"\u4e1c\u98ce","tq":"\u591a\u4e91"},{"time":"2018-01-25 15:00:00","temp":"-5","humi":"15","wse":"2","wd":"\u5317\u98ce","tq":"\u591a\u4e91"},{"time":"2018-01-25 16:00:00","temp":"-5","humi":"16","wse":"2","wd":"\u4e1c\u5317\u98ce","tq":"\u591a\u4e91"},{"time":"2018-01-25 17:00:00","temp":"-5","humi":"16","wse":"2","wd":"\u4e1c\u98ce","tq":"\u591a\u4e91"},{"time":"2018-01-25 18:00:00","temp":"-6","humi":"18","wse":"2","wd":"\u4e1c\u98ce","tq":"\u6674\u95f4\u591a\u4e91"},{"time":"2018-01-25 19:00:00","temp":"-7","humi":"19","wse":"2","wd":"\u4e1c\u98ce","tq":"\u6674\u95f4\u591a\u4e91"},{"time":"2018-01-25 20:00:00","temp":"-7","humi":"19","wse":"1","wd":"\u4e1c\u98ce","tq":"\u6674\u95f4\u591a\u4e91"},{"time":"2018-01-25 21:00:00","temp":"-7","humi":"19","wse":"0","wd":"\u5357\u98ce","tq":"\u6674\u95f4\u591a\u4e91"},{"time":"2018-01-25 22:00:00","temp":"-7","humi":"22","wse":"1","wd":"\u4e1c\u5317\u98ce","tq":"\u6674\u95f4\u591a\u4e91"},{"time":"2018-01-25 23:00:00","temp":"-9","humi":"27","wse":"1","wd":"\u897f\u5357\u98ce","tq":"\u6674\u95f4\u591a\u4e91"}]}}}
### 一、概述 先贴上网站首页地址:https://www.aqistudy.cn/historydata/ 需要抓取对应city下的逐月数据,最终需要爬取的网站示例如下:https://www.aqistudy.cn/historydata/monthdata.php?city=%E5%8C%97%E4%BA%AC 我所使用到的代码可以参考这一篇:https://www.jianshu.com/p/87ab84828a5d 爬取这个网站的初衷只是想练习一下爬虫,并且网上有大量关于爬取该网站的示例,我选用的是python+scrapy+selenium的方法。但是 **参考目前网上的爬取方法并不可行**,主要的问题如下: ### 二、主要问题 #### 2.1.开发者模式下调试最终页面会被网站识别 ![图片说明](https://img-ask.csdn.net/upload/202003/18/1584524701_300942.png) 解决方法我已经找到,参考 https://www.liuyixiang.com/post/109399.html 主要问题在于页面上的endebug函数,对它进行对应处理后就可以进行调试。 #### 2.2.使用chromedriver模拟登陆同样会被识别 在有头模式下对最终网站模拟登陆仍会弹出上述页面(无头也尝试过),说明网站依然监测到了爬虫。我对之前的endebug函数进行了JS反混淆,但是仍看不出反混淆后的代码是如何识别到chromedriver的,或者它和2.1有什么共性因素而被识别出来(不确定是否是这样做,初学者多多包涵)。 如果对网站的JS加载进行停用,如下对middlewares设置,那么不会弹出非法调试的页面,但是需要爬取的AQI等数据无法完全加载出来。 ``` class AreaSpiderMiddleware(object): def process_request(self, request, spider): prefs = { 'profile.default_content_setting_values': { 'images': 2, 'javascript': 2 # 2即为禁用的意思 } } chrome_options = Options() chrome_options.add_experimental_option('prefs', prefs) # 禁止加载图片,JS chrome_options.add_argument("--disable-extensions"); chrome_options.add_experimental_option('excludeSwitches', ['enable-automation']) self.driver = webdriver.Chrome(chrome_options=chrome_options) if request.url != 'https://www.aqistudy.cn/historydata/': self.driver.get(request.url) js = "window.endebug = () => true;return endebug()" # 执行js self.driver.execute_script(js) time.sleep(1) html = self.driver.page_source self.driver.quit() return scrapy.http.HtmlResponse(url=request.url, body=html.encode('utf-8'), encoding='utf-8', request=request) ``` 所以我目前能想到的是可否参照2.1的方法对网页加载运行到endebug时进行暂停,然后再重写JS方法,但是具体方法不知道怎么写。 或者针这种情况是否有更好的策略,希望有大神指点下,感谢!!!
©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页