爬蟲之Js混淆&加密案例

需求: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請求提交的數據和返回的響應數據都是通過加密的瀏覽器

  1. 如何生成加密的請求數據?
  2. 在獲取響應數據後如何解密?

解決: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對象,是個字典,有四個屬性

    • city: 明顯是城市
    • type: 經過標籤訂位發現是主頁右上角的radio框的值,分別爲HOUR,DAY,MOUTH
    • startTime: 起始時間
    • endTime: 終止時間
  • 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 模塊

接下來,須要藉助於 PyExecJS模塊來實現模擬JavaScript代碼執行,獲取動態加密的請求參數,而後再將加密的響應數據傳入decodeData進行解密

PyExecJS介紹: PyExecJS 是一個能夠使用 Python 來模擬運行 JavaScript 的庫.

環境安裝:

  • pip install PyExecJS
  • 安裝nodejs的開發環境

接下來,然咱們一步一步的來實現

獲取ajax請求的動態變化且加密的請求參數

  • 將反混淆網站中的代碼粘貼到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)

至此,完成了對空氣質量數據的爬取.

相關文章
相關標籤/搜索