JavaScript加密逻辑分析与Python模拟执行实现数据爬取

本节来说明一下 JavaScript 加密逻辑分析并利用 Python 模拟执行 JavaScript 实现数据爬取的过程。在这里以中国空气质量在线监测分析平台为例来进行分析,主要分析其加密逻辑及破解方法,并利用 PyExecJS 来实现 JavaScript 模拟执行来实现该网站的数据爬取。

疑难杂症

中国空气质量在线监测分析平台是一个收录全国各大城市天气数据的网站,包括温度、湿度、PM 2.5、AQI 等数据,链接为:https://www.aqistudy.cn/html/city_detail.html,预览图如下:

image

通过这个网站我们可以获取到各大城市任何一天的天气数据,对数据分析还是非常有用的。

然而不幸的是,该网站的数据接口通信都被加密了。经过分析之后发现其页面数据是通过 Ajax 加载的,数据接口地址是:https://www.aqistudy.cn/apinew/aqistudyapi.php,是一个 POST 形式访问的接口,这个接口的请求数据和返回数据都被加密了,即 POST 请求的 Data、返回的数据都被加密了,下图是数据接口的 Form Data 部分,可见传输数据是一个加密后的字符串:

image

下图是该接口返回的内容,同样是经过加密的字符串:

image

遇到这种接口加密的情况,一般来说我们会选择避开请求接口的方式进行数据爬取,如使用 Selenium 模拟浏览器来执行。但这个网站的数据是图表展示的,所以其数据会变得难以提取。

那怎么办呢?刚啊!

一刚到底

之前的老法子都行不通了,那就只能上了!接下来我们就不得不去分析这个网站接口的加密逻辑,并通过一些技巧来破解这个接口了。

首先找到突破口,当我们点击了这个搜索按钮之后,后台便会发出 Ajax 请求,说明这个点击动作是被监听的,所以我们可以找一下这个点击事件对应的处理代码在哪里,这里可以借助于 Firefox 来实现,它可以分析页面某个元素的绑定事件以及定位到具体的代码在哪一行,如图所示:

image

这里我们发现这个搜索按钮绑定了三个事件,blur、click、focus,同时 Firefox 还帮助我们列出来了对应事件的处理函数在哪个代码的哪一行,这里可以看到 click 事件是在 city_detail.html 的第 139 行处理的,而且是调用了 getData() 函数。

接下来我们就可以顺藤摸瓜,找到 city_detail.html 文件的 getData() 函数,然后再找到这个函数的定义即可,很容易地,我们在 city_detail.html 的第 463 行就找到了这个函数的定义:

image

经过分析发现它又调用了 getAQIData() 和 getWeatherData() 两个方法,而这两个方法的声明就在下面,再进一步分析发现这两个方法都调用了 getServerData() 这个方法,并传递了 method、param 等参数,然后还有一个回调函数很明显是对返回数据进行处理的,这说明 Ajax 请求就是由这个 getServerData() 方法发起的,如图所示:

image

所以这里我们只需要再找到 getServerData() 方法的定义即可分析它的加密逻辑了。继续搜索,然而在原始 html 文件中没有搜索到该方法,那就继续去搜寻其他的 JavaScript 文件有没有这个定义,终于经过一番寻找,居然在 jquery-1.8.0.min.js 这个文件中找到了:

image

有的小伙伴可能会说,jquery.min.js 不是一个库文件吗,怎么会有这种方法声明?嗯,我只想说,最危险的地方就是最安全的地方。

好了,现在终于找到这个方法了,可为什么看不懂呢?这个方法名后面怎么直接跟了一些奇怪的字符串,而且不符合一般的 JavaScript 写法。其实这里是经过 JavaScript 混淆加密了,混淆加密之后,代码将变为不可读的形式,但是功能是完全一致的,这是一种常见的 JavaScript 加密手段。

那到这里了该怎么解呢?当然是接着刚啊!

反混淆

JavaScript 混淆之后,其实是有反混淆方法的,最简单的方法便是搜索在线反混淆网站,这里提供一个:http://www.bm8.com.cn/jsConfusion/,我们将 jquery-1.8.0.min.js 中第二行 eval 开头的混淆后的 JavaScript 代码复制一下,然后粘贴到这个网站中进行反混淆,就可以看到正常的 JavaScript 代码了,搜索一下就可以找到 getServerData() 方法了,可以看到这个方法确实发出了一个 Ajax 请求,请求了刚才我们分析到的接口:

image

那么到这里我们又可以发现一个很关键的方法,那就是 getParam(),它接受了 method 和 object 参数,然后返回得到的 param 结果就作为 POST Data 参数请求接口了,所以 param 就是加密后的 POST Data,一些加密逻辑都在 getParam() 方法里面,其方法实现如下:

JavaScript

<header class="article-header">

JavaScript加密逻辑分析与Python模拟执行实现数据爬取

Python 崔庆才 <time class="muted">10个月前 (01-25)</time> 8140浏览 12评论

</header>

<article class="article-content">

本节来说明一下 JavaScript 加密逻辑分析并利用 Python 模拟执行 JavaScript 实现数据爬取的过程。在这里以中国空气质量在线监测分析平台为例来进行分析,主要分析其加密逻辑及破解方法,并利用 PyExecJS 来实现 JavaScript 模拟执行来实现该网站的数据爬取。

疑难杂症

中国空气质量在线监测分析平台是一个收录全国各大城市天气数据的网站,包括温度、湿度、PM 2.5、AQI 等数据,链接为:https://www.aqistudy.cn/html/city_detail.html,预览图如下:

image

通过这个网站我们可以获取到各大城市任何一天的天气数据,对数据分析还是非常有用的。

然而不幸的是,该网站的数据接口通信都被加密了。经过分析之后发现其页面数据是通过 Ajax 加载的,数据接口地址是:https://www.aqistudy.cn/apinew/aqistudyapi.php,是一个 POST 形式访问的接口,这个接口的请求数据和返回数据都被加密了,即 POST 请求的 Data、返回的数据都被加密了,下图是数据接口的 Form Data 部分,可见传输数据是一个加密后的字符串:

image

下图是该接口返回的内容,同样是经过加密的字符串:

image

遇到这种接口加密的情况,一般来说我们会选择避开请求接口的方式进行数据爬取,如使用 Selenium 模拟浏览器来执行。但这个网站的数据是图表展示的,所以其数据会变得难以提取。

那怎么办呢?刚啊!

一刚到底

之前的老法子都行不通了,那就只能上了!接下来我们就不得不去分析这个网站接口的加密逻辑,并通过一些技巧来破解这个接口了。

首先找到突破口,当我们点击了这个搜索按钮之后,后台便会发出 Ajax 请求,说明这个点击动作是被监听的,所以我们可以找一下这个点击事件对应的处理代码在哪里,这里可以借助于 Firefox 来实现,它可以分析页面某个元素的绑定事件以及定位到具体的代码在哪一行,如图所示:

image

这里我们发现这个搜索按钮绑定了三个事件,blur、click、focus,同时 Firefox 还帮助我们列出来了对应事件的处理函数在哪个代码的哪一行,这里可以看到 click 事件是在 city_detail.html 的第 139 行处理的,而且是调用了 getData() 函数。

接下来我们就可以顺藤摸瓜,找到 city_detail.html 文件的 getData() 函数,然后再找到这个函数的定义即可,很容易地,我们在 city_detail.html 的第 463 行就找到了这个函数的定义:

image

经过分析发现它又调用了 getAQIData() 和 getWeatherData() 两个方法,而这两个方法的声明就在下面,再进一步分析发现这两个方法都调用了 getServerData() 这个方法,并传递了 method、param 等参数,然后还有一个回调函数很明显是对返回数据进行处理的,这说明 Ajax 请求就是由这个 getServerData() 方法发起的,如图所示:

image

所以这里我们只需要再找到 getServerData() 方法的定义即可分析它的加密逻辑了。继续搜索,然而在原始 html 文件中没有搜索到该方法,那就继续去搜寻其他的 JavaScript 文件有没有这个定义,终于经过一番寻找,居然在 jquery-1.8.0.min.js 这个文件中找到了:

image

有的小伙伴可能会说,jquery.min.js 不是一个库文件吗,怎么会有这种方法声明?嗯,我只想说,最危险的地方就是最安全的地方。

好了,现在终于找到这个方法了,可为什么看不懂呢?这个方法名后面怎么直接跟了一些奇怪的字符串,而且不符合一般的 JavaScript 写法。其实这里是经过 JavaScript 混淆加密了,混淆加密之后,代码将变为不可读的形式,但是功能是完全一致的,这是一种常见的 JavaScript 加密手段。

那到这里了该怎么解呢?当然是接着刚啊!

反混淆

JavaScript 混淆之后,其实是有反混淆方法的,最简单的方法便是搜索在线反混淆网站,这里提供一个:http://www.bm8.com.cn/jsConfusion/,我们将 jquery-1.8.0.min.js 中第二行 eval 开头的混淆后的 JavaScript 代码复制一下,然后粘贴到这个网站中进行反混淆,就可以看到正常的 JavaScript 代码了,搜索一下就可以找到 getServerData() 方法了,可以看到这个方法确实发出了一个 Ajax 请求,请求了刚才我们分析到的接口:

image

那么到这里我们又可以发现一个很关键的方法,那就是 getParam(),它接受了 method 和 object 参数,然后返回得到的 param 结果就作为 POST Data 参数请求接口了,所以 param 就是加密后的 POST Data,一些加密逻辑都在 getParam() 方法里面,其方法实现如下:

JavaScript

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)

    }

})();

|

可以看到这里使用了 Base64 和 AES 加密。加密之后的字符串便作为 POST Data 传送给服务器了,然后服务器再进行解密处理,然后进行逻辑处理,然后再对处理后的数据进行加密,返回了加密后的数据,那么 JavaScript 再接收到之后再进行一次解密,再渲染才能得到正常的结果。

所以这里还需要分析服务器传回的数据是怎样解密的。顺腾摸瓜,很容易就找到一个 decodeData() 方法,其定义如下:

JavaScript

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

    }

 |

嗯,这里又经过了三层解密,才把正常的明文数据解析出来。

所以一切都清晰了,我们需要实现两个过程才能正常使用这个接口,即实现 POST Data 的加密过程和 Response Data 的解密过程。其中 POST Data 的加密过程是 Base64 + AES 加密,Response Data 的解密是 AES + DES + Base64 解密。加密解密的 Key 也都在 JavaScript 文件里能找到,我们用 Python 实现这些加密解密过程就可以了。

所以接下来怎么办?接着刚啊!

接着刚才怪!

何必去费那些事去用 Python 重写一遍 JavaScript,万一二者里面有数据格式不统一或者二者由于语言不兼容问题导致计算结果偏差,上哪里去 Debug?

那怎么办?这里我们借助于 PyExecJS 库来实现 JavaScript 模拟就好了。

PyExecJS

PyExecJS 是一个可以使用 Python 来模拟运行 JavaScript 的库。大家可能听说过 PyV8,它也是用来模拟执行 JavaScript 的库,可是由于这个项目已经不维护了,而且对 Python3 的支持不好,而且安装出现各种问题,所以这里选用了 PyExecJS 库来代替它。

首先我们来安装一下这个库:

pip install PyExecJS

使用 pip 安装即可。

在使用这个库之前请确保你的机器上安装了以下其中一个JS运行环境:

  • JScript
  • JavaScriptCore
  • Nashorn
  • Node
  • PhantomJS
  • PyV8
  • SlimerJS
  • SpiderMonkey

PyExecJS 库会按照优先级调用这些引擎来实现 JavaScript 执行,这里推荐安装 Node.js 或 PhantomJS。

接着我们运行代码检查一下运行环境:

Python


import  execjs

print(execjs.get().name)

运行之后,由于我安装了 Node.js,所以这里会使用 Node.js 作为渲染引擎,结果如下:


Node.js  (V8)

接下来我们将刚才反混淆的 JavaScript 保存成一个文件,叫做 encryption.js,然后用 PyExecJS 模拟运行相关的方法即可。

首先我们来实现加密过程,这里 getServerData() 方法其实已经帮我们实现好了,并实现了 Ajax 请求,但这个方法里面有获取 Storage 的方法,Node.js 不适用,所以这里我们直接改写下,实现一个 getEncryptedData() 方法实现加密,在 encryption.js 里面实现如下方法:


function  getEncryptedData(method,  city,  type,  startTime,  endTime)  {

    var  param  =  {};

    param.city  =  city;

    param.type  =  type;

    param.startTime  =  startTime;

    param.endTime  =  endTime;

    return  getParam(method,  param);

}

接着我们模拟执行这些方法即可:

import execjs

# Init environment

node  =  execjs.get()

# Params

method  =  'GETCITYWEATHER'

city  =  '北京'

type  =  'HOUR'

start_time  =  '2018-01-25 00:00:00'

end_time  =  '2018-01-25 23:00:00'

# Compile javascript

file  =  'encryption.js'

ctx  =  node.compile(open(file).read())

# Get params

js  =  'getEncryptedData("{0}", "{1}", "{2}", "{3}", "{4}")'.format(method,  city,  type,  start_time,  end_time)

params  =  ctx.eval(js)

这里我们首先定义一些参数,如 method、city、start_time 等,这些都可以通过分析 JavaScript 很容易得出其规则。

然后这里首先通过 execjs(即 PyExecJS)的 get() 方法声明一个运行环境,然后调用 compile() 方法来执行刚才保存下来的加密库 encryption.js,因为这里面包含了一些加密方法和自定义方法,所以只有执行一遍才能调用。

接着我们再构造一个 js 字符串,传递这些参数,然后通过 eval() 方法来模拟执行,得到的结果赋值为 params,这个就是 POST Data 的加密数据。

接着我们直接用 requests 库来模拟 POST 请求就好了,也没必要用 jQuery 自带的 Ajax 了,当然后者也是可行的,只不过需要加载一下 jQuery 库。

接着我们用 requests 库来模拟 POST 请求:


# Get encrypted response text

api  =  'https://www.aqistudy.cn/apinew/aqistudyapi.php'

response  =  requests.post(api,  data={'d':  params})

这样 response 的内容就是服务器返回的加密的内容了。

接下来我们再调用一下 JavaScript 中的 decodeData() 方法即可实现解密:

# Decode data

js  =  'decodeData("{0}")'.format(response.text)

decrypted_data  =  ctx.eval(js)

这样 decrypted_data 就是解密后的字符串了,解密之后,实际上是一个 JSON 字符串:

{'success':  True,  'errcode':  0,  'errmsg':  'success',  'result':  {'success':  True,  'data':  {'total':  22,  'rows':  [{'time':  '2018-01-25 00:00:00',  'temp':  '-7',  'humi':  '35',  'wse':  '1',  'wd':  '东北风',  'tq':  '晴'},  {'time':  '2018-01-25 01:00:00',  'temp':  '-9',  'humi':  '38',  'wse':  '1',  'wd':  '西风',  'tq':  '晴'},  {'time':  '2018-01-25 02:00:00',  'temp':  '-10',  'humi':  '40',  'wse':  '1',  'wd':  '东北风',  'tq':  '晴'},  {'time':  '2018-01-25 03:00:00',  'temp':  '-8',  'humi':  '27',  'wse':  '2',  'wd':  '东北风',  'tq':  '晴'},  {'time':  '2018-01-25 04:00:00',  'temp':  '-8',  'humi':  '26',  'wse':  '2',  'wd':  '东风',  'tq':  '晴'},  {'time':  '2018-01-25 05:00:00',  'temp':  '-8',  'humi':  '23',  'wse':  '2',  'wd':  '东北风',  'tq':  '晴'},  {'time':  '2018-01-25 06:00:00',  'temp':  '-9',  'humi':  '27',  'wse':  '2',  'wd':  '东北风',  'tq':  '多云'},  {'time':  '2018-01-25 07:00:00',  'temp':  '-9',  'humi':  '24',  'wse':  '2',  'wd':  '东北风',  'tq':  '多云'},  {'time':  '2018-01-25 08:00:00',  'temp':  '-9',  'humi':  '25',  'wse':  '2',  'wd':  '东风',  'tq':  '晴转多云转多云间晴'},  {'time':  '2018-01-25 09:00:00',  'temp':  '-8',  'humi':  '21',  'wse':  '3',  'wd':  '东北风',  'tq':  '晴转多云转多云间晴'},  {'time':  '2018-01-25 10:00:00',  'temp':  '-7',  'humi':  '19',  'wse':  '3',  'wd':  '东北风',  'tq':  '晴转多云转多云间晴'},  {'time':  '2018-01-25 11:00:00',  'temp':  '-6',  'humi':  '18',  'wse':  '3',  'wd':  '东北风',  'tq':  '多云'},  {'time':  '2018-01-25 12:00:00',  'temp':  '-6',  'humi':  '17',  'wse':  '3',  'wd':  '东北风',  'tq':  '多云'},  {'time':  '2018-01-25 13:00:00',  'temp':  '-5',  'humi':  '17',  'wse':  '2',  'wd':  '东北风',  'tq':  '多云'},  {'time':  '2018-01-25 14:00:00',  'temp':  '-5',  'humi':  '16',  'wse':  '2',  'wd':  '东风',  'tq':  '多云'},  {'time':  '2018-01-25 15:00:00',  'temp':  '-5',  'humi':  '15',  'wse':  '2',  'wd':  '北风',  'tq':  '多云'},  {'time':  '2018-01-25 16:00:00',  'temp':  '-5',  'humi':  '16',  'wse':  '2',  'wd':  '东北风',  'tq':  '多云'},  {'time':  '2018-01-25 17:00:00',  'temp':  '-5',  'humi':  '16',  'wse':  '2',  'wd':  '东风',  'tq':  '多云'},  {'time':  '2018-01-25 18:00:00',  'temp':  '-6',  'humi':  '18',  'wse':  '2',  'wd':  '东风',  'tq':  '晴间多云'},  {'time':  '2018-01-25 19:00:00',  'temp':  '-7',  'humi':  '19',  'wse':  '2',  'wd':  '东风',  'tq':  '晴间多云'},  {'time':  '2018-01-25 20:00:00',  'temp':  '-7',  'humi':  '19',  'wse':  '1',  'wd':  '东风',  'tq':  '晴间多云'},  {'time':  '2018-01-25 21:00:00',  'temp':  '-7',  'humi':  '19',  'wse':  '0',  'wd':  '南风',  'tq':  '晴间多云'}]}}}

大功告成!

这样我们就可以成功获取温度、湿度、风力、天气等信息了。

另外这部分数据其实不全,还有 PM 2.5、AQI 等数据需要用另外一个 method 参数 GETDETAIL,修改一下即可获取这部分数据了。

再往后的数据就是解析和存储了,这里不再赘述。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容