知識點
Promise物件
Promise物件是ES6版本中提供的,主要是為了解決死亡回撥的問題。
先看一段程式碼:
點選檢視程式碼
function fn() {
let username = "alex";
let password = "123456";
// 傳送請求給伺服器要求登入
console.log("傳送請求出去,嘗試登入");
setTimeout(function () {
console.log("伺服器返回了一個結果");
let result_1 = true;
if(result_1===true){ //登入成功
// 載入選單資訊
console.log("準備載入選單資訊");
setTimeout(function () {
console.log("顯示選單的資訊");
// 載入使用者資訊
console.log("準備載入使用者資訊");
setTimeout(function () {
console.log("顯示使用者資訊");
},1000)
}, 1000);
}
}, 1000);
}
該程式碼是登入網站後網站一步步顯示資訊的一個demo,可以看到裡面存在很多的巢狀,如果想要解決多層巢狀的問題,就可以採用Promise物件,看如下demo:
點選檢視程式碼
function send(url) {
// promise:確保,保證
// reslove:解決了
// reject:拒絕
return new Promise(function (resolve, reject) {
console.log("幫你傳送一個請求到", url); // 答應你的一件事
let result = 123;
if (result) {
// 這件事我辦成了
// 接下來你要做的事應該是呼叫這個函式的那個人去寫
resolve(i); //這裡代表當前任務被解決
} else {
// 這件事沒辦成
reject(i); // 這裡代表當前任務沒解決
}
});
}
function fn() {
let username = "";
let password = "";
//傳送請求到登入
send("xxxxx").then(function (data) {
console.log("登入的結果");
console.log("登入返回的結果是",data);
return send("載入選單");
}).then(function (data) {
// 載入選單
console.log("載入選單得到的資訊",data);
return send("載入個人資訊")
}).then(function (data) {
// 載入個人資訊
console.log("載入個人資訊得到的資訊", data);
})
}
send
函式中會返回一個Promise物件,如果成功了就會執行resolve
對應的函式,失敗了就執行reject
對應的函式。在fn
函式中省略了reject
對應的函式,因為一般會在Promise物件的最後加一個catch,只要失敗了,就直接走catch中的函式,把整個程式碼抽象一下如下:
點選檢視程式碼
//鏈式邏輯
new Promise(function (a, b) {}).then(function () {
return new Promise();
},function () {
}).then(function () {
return new Promise();
}).then(function () {
}).catch(function () {
console.log("程式出錯,請聯絡管理員....")
})
axios攔截器
axios是一個基於Promise的網路請求庫,網站如果採用的是axios方法,那麼加密和解密的邏輯大機率存在於axios攔截器中。
axios攔截器分為請求攔截器和響應攔截器,加密邏輯大機率在請求攔截器中,解密邏輯大機率在響應攔截器中,下面看axios攔截器使用的程式碼:
點選檢視程式碼
// 請求的攔截器
axios.interceptors.request.use(function (config) {
console.log(config, "你好啊");
// 嘗試修改請求引數
config.data['hehe'] = "i love you";
return config;
}, function (err) {
console.log(err);
});
// 響應的攔截器
axios.interceptors.response.use(function (response) {
console.log(response);
// 這裡一般會有什麼??? 解密操作
return response.data; // 攔截器返回的東西直接給到then中的函式
}, function (err) {
console.log(err);
});
上面兩個知識點講完,就該進入正篇了。
七麥資料實戰
url:https://www.qimai.cn/rank
滑動頁面,抓包,老樣子還是看Fetch/XHR
型別的。
有三個資料包,樣式都一樣,看下它們的請求引數和響應資料。
這樣子就知道0
對應的是付費榜,1
對應的是免費榜,2
對應的是暢銷榜。既然三個請求引數都一樣,那就以其中一個為例即可。
請求頭中就一個analysis
引數的值是加密的,那目標就是知道該引數的值如何加密的。
按照慣例,搜尋url。
總共三處地方,但這三處全是賦值操作,沒有其他的程式碼,那麼搜尋url就失效了,接下來搜尋analysis
關鍵詞。
三處地方,但analysis
都位於url地址中,根本不可能是給analysis
引數賦值的,所以這也失效了,最後只能透過Initiator來找了。
明顯的看到了Promise物件,就可以聯想到axios攔截器了,搜尋interceptors
。
也是三處,第一處是個賦值,不可能是加密邏輯,看下第二處和第三處整個的邏輯。
點選檢視程式碼
l.prototype.request = function(e) {
"string" == typeof e ? (e = arguments[1] || {}).url = arguments[0] : e = e || {},
(e = s(this.defaults, e)).method ? e.method = e.method.toLowerCase() : this.defaults.method ? e.method = this.defaults.method.toLowerCase() : e.method = "get";
var t = [o, void 0]
, n = Promise.resolve(e);
for (this.interceptors.request.forEach((function(e) {
t.unshift(e.fulfilled, e.rejected)
}
)),
this.interceptors.response.forEach((function(e) {
t.push(e.fulfilled, e.rejected)
}
)); t.length; )
n = n.then(t.shift(), t.shift());
return n
}
先對e
進行型別判斷和值的重新賦值,然後宣告t
為陣列和n
為Promise物件,接著兩個for迴圈,請求攔截器中遍歷往t
陣列的頭部插入元素,響應攔截器遍歷往t
陣列的尾部插入元素,可以看到遍歷完成後,t
陣列中總共有6個物件,最後從t
陣列的頭部彈出兩個元素交給Promise物件的then函式執行。
根據Promise物件的then函式可以知道,會給其傳兩個引數,成功了執行第一個引數,失敗了執行第二個引數。所以如果這裡存在加密邏輯的話,那麼一定在t
陣列的第一個引數處,定位。
從以下三個變數的值也可以看出沒找錯地方。
這段程式碼中存在非常多的花指令,得先將其還原,打斷點進行除錯,還原出來的程式碼如下。(catch中函式就不用管了)
點選檢視程式碼
function fn(t) {
var n;
n = i["ej"]("synct"),
s = c["default"]["prototype"]["difftime"] = -i["ej"]("syncd") || +new z["Date"] - 1000 * n;
var e, r = +new z["Date"] - (s || 0) - 1661224081041, a = [];
return void 0 === t["params"] && (t["params"] = {}),
z["Object"]["keys"](t["params"])["forEach"](function (n) {
if (n == "analysis")
return !1;
t["params"]["hasOwnProperty"](n) && a["push"](t["params"][n])
}),
a = a["sort"]()["join"](""),
a = i["cv"](a),
a = (a += "@#" + t["url"]["replace"](t["baseURL"], "")) + ("@#" + r) + ("@#" + 3),
e = i["cv"](i["oZ"](a, "xyz517cda96efgh")),
-B == t["url"]["indexOf"]("analysis") && (t["url"] += (-B != t["url"]["indexOf"]("?") ? "&" : "?") + "analysis" + "=" + z["encodeURIComponent"](e)),
t
}
接下來分析這段程式碼。
n = i["ej"]("synct")
用於獲取cookie中synct
的值。
s = c["default"]["prototype"]["difftime"] = -i["ej"]("syncd") || +new z["Date"] - 1000 * n;
用於獲取cookie中syncd
中的值,如果cookie中沒有syncd
,則s=new Date()-1000*n
。
r = +new z["Date"] - (s || 0) - 1661224081041
就是計算一個時間差,這個值不是固定的,所以我們可以直接把s
的值固定,上面兩行程式碼就沒用了。
void 0 === t[Zt] && (t[Zt] = {})
就是false。
z["Object"]["keys"](t["params"])["forEach"](function (n) { if (n == "analysis") return !1; t["params"]["hasOwnProperty"](n) && a["push"](t["params"][n]) })
遍歷t["params"]
中的所有鍵,將對應的值全部存放到a
陣列中,z
是window物件,故z["Object"]["keys"]
等同於Object["keys"]
。
a = a["sort"]()["join"]("")
對a陣列進行排序,並用空字串連線。
a = i["cv"](a)
需要知道i["cv"]
是什麼,等下直接把原始碼複製進來即可。
a = (a += "@#" + t["url"]["replace"](t["baseURL"], "")) + ("@#" + r) + ("@#" + 3)
對a
的值進行拼接。
e = i["cv"](i["oZ"](a, "xyz517cda96efgh"))
同理,直接複製原始碼。
-B == t["url"]["indexOf"]("analysis") && (t["url"] += (-B != t["url"]["indexOf"]("?") ? "&" : "?") + "analysis" + "=" + z["encodeURIComponent"](e))
判斷url背後的引數是用&
還是?
連線。這裡最主要的是要得到e
的值,直接返回e即可。
到目前為止,化簡後的程式碼為:
點選檢視程式碼
function fn(t) {
var e, r = new Date() + 226 - 1661224081041, a = [];
return false,
Object["keys"](t["params"])["forEach"](function (n) {
if (n == "analysis")
return !1;
t["params"]["hasOwnProperty"](n) && a["push"](t["params"][n])
}),
a = a["sort"]()["join"](""),
a = i["cv"](a),
a = (a += "@#" + t["url"]["replace"](t["baseURL"], "")) + ("@#" + r) + ("@#" + 3),
e = i["cv"](i["oZ"](a, "xyz517cda96efgh")),
e;
}
下面就是要去補全i["cv"]
和i["oZ"]
和這兩個函式中用到的其他變數,花指令該還原就還原。
補全和還原後的程式碼如下:
點選檢視程式碼
function o(n) {
t = "",
['66', '72', '6f', '6d', '43', '68', '61', '72', '43', '6f', '64', '65']["forEach"](function(n) {
t += unescape("%u00" + n)
});
var t, e = t;
return String[e](n)
}
function u() {
return unescape("861831832863830866861836861862839831831839862863839830865834861863837837830830837839836861835833"["replace"](/8/g, "%u00"))
}
var i = {
cv:function v(t) {
t = encodeURIComponent(t)["replace"](/%([0-9A-F]{2})/g, function(n, t) {
return o("0x" + t)
});
try {
return btoa(t)
} catch (n) {
return Buffer["from"](t)["toString"]("base64")
}
},
oZ:function h(n, t) {
t = t || u();
for (var e = (n = n["split"](""))["length"], r = t["length"], a = "charCodeAt", i = 0; i < e; i++)
n[i] = o(n[i][a](0) ^ t[(i + 10) % r][a](0));
return n["join"]("")
}
};
function fn(t) {
var e, r = new Date() + 226 - 1661224081041, a = [];
return false,
Object["keys"](t["params"])["forEach"](function (n) {
if (n == "analysis")
return !1;
t["params"]["hasOwnProperty"](n) && a["push"](t["params"][n])
}),
a = a["sort"]()["join"](""),
a = i["cv"](a),
a = (a += "@#" + t["url"]["replace"](t["baseURL"], "")) + ("@#" + r) + ("@#" + 3),
e = i["cv"](i["oZ"](a, "xyz517cda96efgh")),
e;
}
測試一下。
點選檢視程式碼
var t = {
"url": "/rank/indexPlus/brand_id/1",
"method": "get",
"headers": {
"common": {
"Accept": "application/json, text/plain, */*"
},
"delete": {},
"get": {},
"head": {},
"post": {
"Content-Type": "application/x-www-form-urlencoded"
},
"put": {
"Content-Type": "application/x-www-form-urlencoded"
},
"patch": {
"Content-Type": "application/x-www-form-urlencoded"
}
},
"params": {},
"baseURL": "https://api.qimai.cn",
"transformRequest": [
null
],
"transformResponse": [
null
],
"timeout": 15000,
"withCredentials": true,
"xsrfCookieName": "XSRF-TOKEN",
"xsrfHeaderName": "X-XSRF-TOKEN",
"maxContentLength": -1,
"maxBodyLength": -1
}
console.log(fn(t));
執行結果如下:
得到了跟analysis
引數值相似的字串,說明我們找到了加密的邏輯,接下來就可以寫python程式碼爬取資料了,完整的python程式碼和JavaScript程式碼如下:
JavaScript程式碼:
點選檢視程式碼
function o(n) {
t = "",
['66', '72', '6f', '6d', '43', '68', '61', '72', '43', '6f', '64', '65']["forEach"](function(n) {
t += unescape("%u00" + n)
});
var t, e = t;
return String[e](n)
}
function u() {
return unescape("861831832863830866861836861862839831831839862863839830865834861863837837830830837839836861835833"["replace"](/8/g, "%u00"))
}
var i = {
cv:function v(t) {
t = encodeURIComponent(t)["replace"](/%([0-9A-F]{2})/g, function(n, t) {
return o("0x" + t)
});
try {
return btoa(t)
} catch (n) {
return Buffer["from"](t)["toString"]("base64")
}
},
oZ:function h(n, t) {
t = t || u();
for (var e = (n = n["split"](""))["length"], r = t["length"], a = "charCodeAt", i = 0; i < e; i++)
n[i] = o(n[i][a](0) ^ t[(i + 10) % r][a](0));
return n["join"]("")
}
};
function fn(t) {
var e, r = new Date() + 226 - 1661224081041, a = [];
return false,
Object["keys"](t["params"])["forEach"](function (n) {
if (n == "analysis")
return !1;
t["params"]["hasOwnProperty"](n) && a["push"](t["params"][n])
}),
a = a["sort"]()["join"](""),
a = i["cv"](a),
a = (a += "@#" + t["url"]["replace"](t["baseURL"], "")) + ("@#" + r) + ("@#" + 3),
e = i["cv"](i["oZ"](a, "xyz517cda96efgh")),
e;
}
function final(url, pm) {
var params = {
"url": url,
"baseURL": "https://api.qimai.cn",
"params":pm,
};
return fn(params);
}
python程式碼:
點選檢視程式碼
import subprocess
from functools import partial
subprocess.Popen = partial(subprocess.Popen, encoding="utf-8")
import execjs
import json
import requests
f = open("攔截器邏輯二.js", mode="r", encoding="utf-8")
js = execjs.compile(f.read())
f.close()
data = {
"brand": "all",
"country": "cn",
"date": "2024-03-18",
"device": "iphone",
"genre": "36",
"page": 2,
}
host = "https://api.qimai.cn"
url = "/rank/indexPlus/brand_id/1"
analysis = js.call("final", url, data)
final_url = host+url+"?analysis=" + analysis
# print(final_url)
session = requests.session()
session.headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 "
"Safari/537.36",
}
# 載入最開始的cookie
session.get("https://www.qimai.cn/rank")
# 經過測試,這玩意沒什麼用
session.cookies["qm_check"] = "A1sdRUIQChtxen8pI0dAMRcOUFseEHBeQF0JTjVBWCwycRd1QlhAXFEGFUdeS0laHQdKAAkABAsgXyVBWD0TR1JRRAp0BQlFEBQ3TSZKFUdBbwxvBBRFIlQsSUhTFxsQU1FVV1NHXEVYVElWBRsCHAkSSQ%3D%3D"
# 開幹
resp = session.get(final_url)
decoded_text = bytes(resp.text, 'utf-8').decode('unicode_escape')
print(decoded_text)
執行python程式碼結果如下:
成功拿到資料。