本项目记录一些学习爬虫逆向的案例或者资料,仅供学习参考,请勿用于非法用途。
目前已经爬取:企查查、**五矿、qq音乐、产业政策大数据平台、企知道、天眼查、雪球网、1688、七麦数据、whggzy、企名科技、mohurd、艺恩数据、欧科云链(oklink)、企知道、度衍(uyan)、凤凰云智影院管理平台
环境安装:
npm install
pip install -r requirements.txt
- 静态页面字体加密:HTML实体编码
- 数据加密:动态数据 -> ajax JSON.parse
接口为 “http://www.whggzy.com/front/search/category”
将接口的headers和data传入,并进行测试
headers = {
# 这两个参数是必须的
"User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36",
"Referer": "http://www.whggzy.com/PurchaseAdvisory/index.html",
}
# 通过查看参数发现data传的是字符串,所以这个通过字符串的方式发送data,而不是字典
data = {
"categoryCode": "MostImportant",
"pageNo": 1,
"pageSize": 15
}
访问后报错
在源代码->xhr/提取断点中加入断点,观察到headers的参数,且data参数为字符串
import requests
url = "http://www.whggzy.com/front/search/category"
headers = {
# 这两个参数是必须的
"User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36",
"Referer": "http://www.whggzy.com/PurchaseAdvisory/index.html",
# 通过xhr断点找到接口的headers包含一下几个
'Accept': "*/*",
'Content-Type': "application/json",
'X-Requested-With': "XMLHttpRequest",
}
# 通过查看参数发现data传的是字符串,所以这个通过字符串的方式发送data,而不是字典
data = '''{
"categoryCode": "MostImportant",
"pageNo": 1,
"pageSize": 15
}'''
print(requests.post(url, headers=headers, data=data).text)
url: https://www.qimingpian.com/finosda/project/pinvestment
搜索不到数据,判断为通过ajax数据加密
找到接口:
找到js文件
找到加密数据的方法:
// 还需要找到o方法和decode方法,整合成js文件
function s(e) {
return JSON.parse(o("5e5062e82f15fe4ca9d24bc5", a.a.decode(e), 0, 0, "012345677890123", 1))
}
import json
import execjs
import requests
url = 'https://vipapi.qimingpian.cn/DataList/productListVip'
headers = {}
data = {}
# 打开js文件
with open("./demo2_qiming.js", "r") as f:
jscode = f.read()
# 调用api接口拿到加密数据
resp = requests.get(url, headers=headers, data=data).json()
# 调用js文件
ctx = execjs.compile(jscode).call('s', resp["encrypt_data"])
print(ctx)
print(f"type:{type(ctx)}")
url: https://jzsc.mohurd.gov.cn/data/company
接口:https://jzsc.mohurd.gov.cn/APi/webApi/dataservice/query/comp/list?pg=0&pgsz=15&total=0
数据经过加密,data: 95780ba09437300...
通过接口名称找到js文件,在js文件里搜索JSON.parse,打断点进行调试
常见js算法:crypto-js、jsdom、hash、md5、逆向算法
判断使用的加密是CryptoJS,使用npm安装库后导入,替换原始js里使用的方法
const CryptoJS = require('crypto-js')
// 常见js算法:jsdom hash md5 逆向算法
function m(t) {
// d.a是CryptoJS
var f = CryptoJS.enc.Utf8.parse("jo8j9wGw%6HbxfFn")
var h = CryptoJS.enc.Utf8.parse("0123456789ABCDEF")
var e = CryptoJS.enc.Hex.parse(t),
n = CryptoJS.enc.Base64.stringify(e),
a = CryptoJS.AES.decrypt(n, f, {
iv: h,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}),
r = a.toString(CryptoJS.enc.Utf8);
return r.toString()
}
url: https://www.endata.com.cn/BoxOffice/BO/Year/index.html
url = "https://www.endata.com.cn/API/GetData.ashx"
headers = {} # 略
data = {} # 略
ori_data = requests.post(url, headers=headers, data=data).text
"""
out:
AFB3D177A5D1D916CFEFBE70FEFC0C59C0463AE137DE1A099C4B169B8AB9DBC33EE55B1...(加密数据)
"""
通过断点找到加密的方法,方法经过了js混淆,直接复制方法,执行这个方法,根据提示补齐所有缺失的属性。
js代码中存在一个navigator属性,为环境属性,需要手动写入
global.navigator = {'userAgent': "node.js"}
"""js混淆"""
import execjs
import requests
url = "https://www.endata.com.cn/API/GetData.ashx"
headers = {
"User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36"
}
data = {
"year": 2023,
"MethodName": "BoxOffice_GetYearInfoData"
}
ori_data = requests.post(url, headers=headers, data=data).text
with open("./endata.js", 'r') as f:
js_code = f.read()
result = execjs.compile(js_code).call("webInstace.shell", ori_data)
print(result)
# (webInstace.shell(data));
有加密参数,找这个参数的js文件
**sign参数的组成:**token & 时间戳 & g & 请求参数,数据均没有变化
再经过h函数
# 请求的参数
data = '{"cid":"FactoryRankServiceWidget:FactoryRankServiceWidget","methodName":"execute","params":"{\\"extParam\\":{\\"methodName\\":\\"readRelatedRankEntries\\",\\"cateId\\":\\"10166\\",\\"size\\":\\"15\\",\\"pageNo\\":\\"1\\",\\"pageSize\\":\\"20\\",}}"}'
# sign = token & 时间戳 & g & 请求参数
token = "2a3e896698e27affa623d6ecd90aca5e"
i = int(time.time() * 1000)
g = "12574478"
# 拼接出字符串
j = f"{token}&{i}&{g}&{str(data)}"
# 使用js文件生成sign
with open("./h.js", "r", encoding="utf-8") as f:
js_code = f.read()
sign = execjs.compile(js_code).call("h", j)
# 设置header和params
headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36",
"Referer": "https://sale.1688.com/",
# 这里需要传入cookie,不然会提示token为空
"Cookie":"...",
}
# 这里的data、时间戳要和sign里的data一样
params = {"jsv": "2.6.1", "appKey": "12574478", "t": str(i), "sign": sign, "v": "1.0", "type": "jsonp", "isSec": 0,
"timeout": 20000, "api": "mtop.taobao.widgetService.getJsonComponent", "dataType": "jsonp",
"jsonpIncPrefix": "mboxfc", "callback": "mtopjsonpmboxfc3", "data": data}
url = "https://h5api.m.1688.com/h5/mtop.taobao.widgetservice.getjsoncomponent/1.0/"
print(requests.get(url, headers=headers, params=params).text)
url: https://webapi.cninfo.com.cn/#/marketDataDate
为base64加密
通过标头名或者路径搜索,找到js文件;该方法使用了js混淆。
url: https://www.qimai.cn/rank
通过英文搜索到接口(中文有编码)
接口的参数名搜索js,参数名:analysis,找到对应的js文件
在js文件中添加接口url的路径:/rank/indexPlus/brand_id/1
js使用了js混淆,需要根据调试根据判断使用了什么方法,具体请看注解
// 注释的代码为原始代码
function v(t) {
// 找到这里使用的方法
// t = z[V1](t)[T](/%([0-9A-F]{2})/g, function(n, t) {
t = encodeURIComponent(t).replace(/%([0-9A-F]{2})/g, function (n, t) {
// 这里的Y1是个固定字符
// return o(Y1 + t)
return o("0x" + t)
});
try {
// boat方法
// return z[Q1](t)
return btoa(t)
} catch (n) {
// 找到这个方法
// return z[W1][K1](t)[U1](Z1)
return Buffer.from(t).toString("base64")
}
}
function o(n) {
let f2 = '66';
let s2 = '72';
let d2 = '6f';
let m2 = '6d';
let l2 = '43';
let v2 = '68';
let p2 = '61';
let h2 = '64';
let y2 = '65';
t = "",
[f2, s2, d2, m2, l2, v2, p2, s2, l2, d2, h2, y2].forEach(function (n) {
t += unescape("%u00" + n)
});
var t, e = t;
// 调试时这里的e一直是undefined,通过打断点再调试确定为这个方法
// return z[b2][e](n)
return String.fromCharCode(n)
}
function h(n, t) {
t = t || u();
// 替换方法和值
// for (var e = (n = n[$1](_))[R], r = t[R], a = q1, i = H; i < e; i++)
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[I1](_)
return n.join("")
}
function url(pass) {
var s = 1359
var H = 0
var e, r = +new Date() - (s || H) - 1661224081041, a = [];
var v1 = "@#"
// 固定值
var d = "xyz517cda96abcd"
// a = a[Ot]()[I1](_),
a = a.sort().join("")
// 这里调用了一个去掉域名的方法,但我们传的是后面的接口路径,所以直接使用
// a = (a += v + t[Jt][T](t[Mt], _)) + (v + r) + (v + 3),
a = (a += v1 + pass) + (v1 + r) + (v1 + 3)
// 在调试工具里找到这两个方法,复制到上面
// e = (0,
// i[jt])((0,
// i[qt])(a, d))
e = (0,
v)((0,
h)(a, d))
return e
}
const pass = "/rank/indexPlus/brand_id/1"
console.log(url(pass));
![image-20230605145711223](/Users/chenzixin/Library/Mobile Documents/comappleCloudDocs/Documents/Code/python/spider_reverse/README.assets/image-20230605145711223.png)
找到方法,再进行搜索
![image-20230605150642725](/Users/chenzixin/Library/Mobile Documents/comappleCloudDocs/Documents/Code/python/spider_reverse/README.assets/image-20230605150642725.png)
找到webpack打包的代码,改写后补齐所有方法
{
key: "encryptApiKey",
value: function() {
var t = this.API_KEY
, e = t.split("")
, n = e.splice(0, 8);
return t = e.concat(n).join("")
}
}, {
key: "encryptTime",
value: function(t) {
var e = (1 * t + a).toString().split("")
, n = parseInt(10 * Math.random(), 10)
, r = parseInt(10 * Math.random(), 10)
, o = parseInt(10 * Math.random(), 10);
return e.concat([n, r, o]).join("")
}
}, {
key: "comb",
value: function(t, e) {
var n = "".concat(t, "|").concat(e);
return window.btoa(n)
}
},
{
key: "getApiKey",
value: function() {
var t = (new Date).getTime()
, e = this.encryptApiKey();
return t = this.encryptTime(t),
this.comb(e, t)
}
}
function encryptApiKey() {
let API_KEY = "a2c903cc-b31e-4547-9299-b6d07b7631ab"
var t = API_KEY
, e = t.split("")
, n = e.splice(0, 8);
return t = e.concat(n).join("")
}
function encryptTime(t) {
let a = 1111111111111
var e = (1 * t + a).toString().split("")
, n = parseInt(10 * Math.random(), 10)
, r = parseInt(10 * Math.random(), 10)
, o = parseInt(10 * Math.random(), 10);
return e.concat([n, r, o]).join("")
}
function comb(t, e) {
var n = "".concat(t, "|").concat(e);
return Buffer.from(n).toString("base64")
}
function getApiKey() {
var t = (new Date).getTime()
, e = encryptApiKey();
return t = encryptTime(t),
comb(e, t)
}
getApiKey()
def get_api_key():
"""获取加密字符串"""
times = int(time.time() * 1000)
# 固定的加密值
api_key = "a2c903cc-b31e-4547-9299-b6d07b7631ab"
# encryptApiKey()
key1 = api_key[0:8]
key2 = api_key[8:]
# 交换位置
new_key = key2 + key1
# encryptTime()
a = 1111111111111
new_time = str(1 * times + a)
random1 = str(random.randint(0, 9))
random2 = str(random.randint(0, 9))
random3 = str(random.randint(0, 9))
# 拼接
cur_time = new_time + random1 + random2 + random3
# 合并前面生成的两个值,并用base64加密
this_key = new_key + "|" + cur_time
n_k = this_key.encode("utf-8")
x_apikey = base64.b64encode(n_k)
return x_apikey
url:https://m.flight.qunar.com/h5/flight/
1、请求参数:__m__
![image-20230605172141802](/Users/chenzixin/Library/Mobile Documents/comappleCloudDocs/Documents/Code/python/spider_reverse/README.assets/image-20230605172141802.png)
2、请求头参数:键值对加密
![image-20230605172331402](/Users/chenzixin/Library/Mobile Documents/comappleCloudDocs/Documents/Code/python/spider_reverse/README.assets/image-20230605172331402.png)
f()为md5加密
![image-20230605223339438](/Users/chenzixin/Library/Mobile Documents/comappleCloudDocs/Documents/Code/python/spider_reverse/README.assets/image-20230605223339438.png)
u()为SHA1加密
![image-20230605223535368](/Users/chenzixin/Library/Mobile Documents/comappleCloudDocs/Documents/Code/python/spider_reverse/README.assets/image-20230605223535368.png)
// 所有方法
// 加密算法
function encryptFunction() {
/**
* 调试进入这两个算法中,找到他们对应的算法,找关键字,例如算法名称
* f:md5
* n: SHA1
*/
return [function (e) {
// t % 2 == 0执行这个方法
// e = t时间戳 + n字符串
var t = (0,
u.default)(e).toString();
return (0,
f.default)(t).toString()
}
// t % 2 == 1执行这个方法
, function (e) {
var t = (0,
f.default)(e).toString();
return (0,
u.default)(t).toString()
}
]
}
function dencryptCode(t) {
return t.map(function (e) {
return String.fromCharCode(e - 2)
}).join("")
}
function getQtTime(t) {
/*
* 如果有t这里直接返回t
* 没t这里把时间戳分割后的char值-2转为字符串
*/
return t ? Number(t.split(",").map(function (e) {
return String.fromCharCode(e - 2)
}).join("")) : 0
}
// 获取字符串方法
function getTokenStr() {
var t = this.dencryptCode(this.tokenStr);
// 这里选择了页面中一个id为t的元素的值
var n = document.getElementById(t).innerHTML;
// 这里会返回元素的值或者方法的值
return n ? n : (0,
s.default)(this.dencryptCode(this.cookieToken))
}
// 获取参数的方法
function encrypt() {
// t是页面元素的值
var t = this.getTokenStr()
// 这里n是时间戳
, n = this.getQtTime((0,
s.default)(this.dencryptCode(this.qtTime)))
// r是对时间戳取模
, r = n % 2;
return encryptFunction()[r](t + n)
}
// 加密的方法
function encryptToken(t) {
return (0,
f.default)(t).toString()
}
根据前面解析js代码,得到m参数的生成逻辑
import hashlib
import time
def md5_hash(text):
# 创建MD5哈希对象
md5_hasher = hashlib.md5()
# 更新哈希对象以包含待加密的文本
md5_hasher.update(text.encode('utf-8'))
# 返回MD5加密后的结果
return md5_hasher.hexdigest()
def sha1_hash(text):
# 创建SHA1哈希对象
sha1_hasher = hashlib.sha1()
# 更新哈希对象以包含待加密的文本
sha1_hasher.update(text.encode('utf-8'))
# 返回SHA1加密后的结果
return sha1_hasher.hexdigest()
def get_m():
# .data["__m__"] = u.default.encryptToken(u.default.encrypt());
# 获取需要被加密的参数,即u.default.encrypt()
# 页面存储的token"00008a002f1051a169b06202"
t = "00008a002f1051a169b06202"
# 时间戳
n = int(time.time() * 1000)
# n = 1686020493263
# 时间戳取余
r = n % 2
p1 = t + str(n)
# 根据r决定先用SHA1还是MD5
if r == 0:
# SHA1
p1 = sha1_hash(p1)
# MD5
p1 = md5_hash(p1)
else:
# MD5
p1 = md5_hash(p1)
# SHA1
p1 = sha1_hash(p1)
# 最后再用一次MD5加密
p1 = md5_hash(p1)
return p1
# fbc1646d57a2b22ceb5f5ef60018f67d
print(get_m())
// 生成key的方法,传入的参数是时间戳
function getRandomKey(t) {
var n = "";
// 从时间戳的第四位开始截取
var r = ("" + t).substr(4);
// 时间戳每一位映射的acsii编码
r.split("").forEach(function (e) {
n += e.charCodeAt()
});
// md5加密
var i = (0,
f.default)(n).toString();
// -6:
return i.substr(-6)
}
// headers的主要生成方法
function getToken() {
// dict
var t = {};
// this.getQtTime((0,s.default)(this.dencryptCode(this.qtTime))) 依然是时间戳
// value是__m__参数一样的加密方法
t[this.getRandomKey(this.getQtTime((0,
s.default)(this.dencryptCode(this.qtTime))))] = this.encrypt();
return t
}
def get_random_key(t):
""" 获取请求头参数的key """
# 截取4开始的字符串
t = str(t)[4:]
n = ""
# 转为ascii编码并拼接
for i in t:
n += str(ord(i))
# md5解密
key = md5_hash(n)
# 返回最后6位
return key[-6:]
def get_headers():
""" 获取请求头参数 键值对"""
t = int(time.time() * 1000)
# 获取key
key = get_random_key(t)
# 获取值
value = get_m(t)
return {key:value}
url: https://gz.meituan.com/meishi/
_token为加密参数。
![image-20230609204919138](/Users/chenzixin/Library/Mobile Documents/comappleCloudDocs/Documents/Code/python/spider_reverse/README.assets/image-20230609204919138.png)
查找到token的生成位置,找到加密的方法
![image-20230609210113287](/Users/chenzixin/Library/Mobile Documents/comappleCloudDocs/Documents/Code/python/spider_reverse/README.assets/image-20230609210113287.png)
_token参数为iP类进行加密后得到的,但目前只能得出iP的sign参数,对iP进行加密后没办法得到最终的值,如果你知道怎么做欢迎提交issue给我。
iP.sign为参数排序后进行base64加密。
def decode_sign(token_str):
token_str = token_str.replace(" ", "")
# base编码
# token_str = f"\"{str(token_str)}\""
# token_str = str(token_str)
# token_str = f"'{token_str}'"
print(f"str:::{token_str}")
encode1 = str(token_str).encode()
# 参数 压缩成 特殊的编码
compress = zlib.compress(encode1)
b_encode = base64.b64encode(compress)
# 转变 str
e_sign = str(b_encode, encoding="utf-8")
return e_sign
# 查询参数
params = {
'cityName': '广州',
'cateId': 0,
'areaId': 0,
'sort': '',
'dinnerCountAttrId': '',
'page': 1,
'userId': '234746173',
'uuid': 'cab1469b7bd84ca09ea5.1686484028.1.0.0',
'platform': 1,
'partner': 126,
'originUrl': 'https://gz.meituan.com/meishi/',
'riskLevel': 1,
'optimusCode': 10
}
# 生成sign
params_str = "\""
# 对key进行排序
keys = [i for i in params.keys()]
keys.sort()
# 拼接
for key in keys:
params_str += f"{key}={params.get(key)}&"
params_str = params_str[:-1]
params_str += "\""
# 加密
sign = decode_sign(params_str)
# iP
iP = {
'rId': 100900,
'ver': "1.0.6",
# 'ts': 1686369166338,
# 'cts': 1686369167064,
"ts":1686485181487,
"cts":1686485200311,
'brVD': [
1920,
150
],
'brR': [
[
1920,
1080
],
[
1920,
981
],
30,
30
],
'bI': [
'https://gz.meituan.com/meishi/',
''
],
'mT': [],
'kT': [],
'aT': [],
'tT': [],
'aM': '',
'sign': sign
}
url:https://www.aqistudy.cn/historydata/monthdata.php?city=%E5%8C%97%E4%BA%AC
通过js生产请求参数
- 完成登陆请求中的json_key参数加密方法
- 解析返回的接口
url:https://patents.qizhidao.com/
这个网站的反debug是带参数的,直接设置跳过会导致返回不了结果卡死。需要通过脚本返回空的值,注意:需要等页面加载后再执行。
func_constructor_ = Function.prototype.constructor;
Function.prototype.constructor = function (a) {
if (a == 'debugger') {
return function(){};
}
return func_constructor_(a);
};
复制接口的cURL到https://curlconverter.com/自动构造,代码有点长这里只放片段:
import requests
cookies = {
# ...
}
headers = {
'authority': 'app.qizhidao.com',
# ...
}
json_data = {
'text_ver': 'N',
# ...
'statement': '华为',
'filter': '',
'pageCount': 21073,
'checkResult': True,
}
response = requests.post(
'https://app.qizhidao.com/qzd-bff-patent/patent/simple-version/search',
cookies=cookies,
headers=headers,
json=json_data,
).json()
# 返回结果:
# {'code': 0, 'status': 0, 'success': True, 'msg': '成功', 'data1': 'ikEaI3qCl29i...', 'hasUse': 2}
data1:这个是加密后的参数,需要解密。AES加密
hasUse:加密的key的位置。key是个字典,2代表第二个value
- 搜索”encrypt“
- 搜索接口
交集在186ef37.js
// 第一个参数是密文,第二个参数是加密的key
// AES加密
_0xecb012 = function(_0x23e639, _0x5b5a7f) {
return function(_0x1a0f85, _0x3f0bf9) {
var _0x17296a = a0_0xe452
, _0x49db2a = _0x2ecfe6['a']['enc']['Utf8']['parse'](_0x3f0bf9 || '46cc793c53' + _0x17296a(0x1f7))
, _0x5bc6c6 = _0x2ecfe6['a'][_0x17296a(0x1dc)]['decrypt'](_0x1a0f85, _0x49db2a, {
// 加密模式是ECB
'mode': _0x2ecfe6['a'][_0x17296a(0x22f)][_0x17296a(0x27a)],
'padding': _0x2ecfe6['a'][_0x17296a(0x242)][_0x17296a(0x27f)]
});
return _0x2ecfe6['a'][_0x17296a(0x23b)][_0x17296a(0x238)][_0x17296a(0x1f9)](_0x5bc6c6)[_0x17296a(0x1d6)]();
}(_0x23e639, _0x5b3606[_0x5b5a7f]);
通过观察发现key不是固定的,存储在一个字典中。通过接口返回的值hasUse选择对应的key。
def AES_decrypt(data, hasUse:int):
key_list = [b"xc46VoB49X3PGYAg", b"KE3pb84wxqLTZEG3", b"18Lw0OEaBBUwHYNT", b"jxxWWIzvkqEQcZrd", b"40w42rjLEXxYhxRn",
b"K6hkD03WNW8N1fPM", b"I8V3IwIhrwNbWxqz", b"3JNNbxAP4zi5oSGA", b"7pUuESQl8aRTFFKK", b"KB4GAHN6M5soB3WV"]
# 解码
html = base64.b64decode(data)
# mode为ECB的AES加密
aes = AES.new(key_list[hasUse-1], AES.MODE_ECB)
# 解密
info = aes.decrypt(html)
# 填充
decrypt = unpad(info, AES.block_size).decode()
return decrypt
有两次请求,第一次获取js代码,第二次通过代码生成参数发送请求
尝试本地替换,发现没有进入方法。是在VM中执行的
进入debug,可以看到调用的位置,打断点调试,进入堆栈中的方法。
参数是arg2,查找这个参数。
返回原代码,找到生成arg2的代码,把混淆的代码还原。通过对比,只有arg1值是不固定的,在第一次请求中就包含了这个值,通过正则提取,传入js代码里生成就可以得到cookie。
String['prototype']['hexXor'] = function (_0x4e08d8) {
var _0x5a5d3b = '';
for (var _0xe89588 = 0x0; _0xe89588 < this['length'] && _0xe89588 < _0x4e08d8['length']; _0xe89588 += 0x2) {
var _0x401af1 = parseInt(this['slice'](_0xe89588, _0xe89588 + 0x2), 0x10);
var _0x105f59 = parseInt(_0x4e08d8['slice'](_0xe89588, _0xe89588 + 0x2), 0x10);
var _0x189e2c = (_0x401af1 ^ _0x105f59)['toString'](0x10);
if (_0x189e2c['length'] == 0x1) {
_0x189e2c = '\x30' + _0x189e2c;
}
_0x5a5d3b += _0x189e2c;
}
return _0x5a5d3b;
}
String['prototype']['unsbox'] = function () {
var _0x4b082b = [0xf, 0x23, 0x1d, 0x18, 0x21, 0x10, 0x1, 0x26, 0xa, 0x9, 0x13, 0x1f, 0x28, 0x1b, 0x16, 0x17, 0x19, 0xd, 0x6, 0xb, 0x27, 0x12, 0x14, 0x8, 0xe, 0x15, 0x20, 0x1a, 0x2, 0x1e, 0x7, 0x4, 0x11, 0x5, 0x3, 0x1c, 0x22, 0x25, 0xc, 0x24];
var _0x4da0dc = [];
var _0x12605e = '';
for (var _0x20a7bf = 0x0; _0x20a7bf < this['length']; _0x20a7bf++) {
var _0x385ee3 = this[_0x20a7bf];
for (var _0x217721 = 0x0; _0x217721 < _0x4b082b['length']; _0x217721++) {
if (_0x4b082b[_0x217721] == _0x20a7bf + 0x1) {
_0x4da0dc[_0x217721] = _0x385ee3;
}
}
}
_0x12605e = _0x4da0dc['join']('');
return _0x12605e;
}
function get_cookie(arg1) {
// 这个值是变化的
// var arg1 = 'E7FC4E89FD5A4E550E19A8BE2061BD16BE4C0DB3';
var _0x23a392 = arg1['unsbox']();
var _0x5e8b26 = '3000176000856006061501533003690027800375'
// arg2 = _0x23a392[_0x55f3('0x1b', '\x7a\x35\x4f\x26')](_0x5e8b26);
arg2 = _0x23a392['hexXor'](_0x5e8b26);
return arg2
}
console.log(get_cookie('E7FC4E89FD5A4E550E19A8BE2061BD16BE4C0DB3'));
import re
import execjs
import requests
headers = {
# ...
}
# 获取到第一次请求的值,返回生成cookie的代码,里面包含了需要的参数 arg1
response = requests.get('https://xueqiu.com/today', headers=headers).text
# with open("./ali.html", 'w', encoding='utf-8') as f:
# f.write(response)
# 正则提取arg1的值
# Regular expression pattern to match the argument assignment
pattern = r"var arg1='([A-F0-9]+)';"
# Search for the pattern in the JavaScript code
arg1 = re.search(pattern, response).group(1)
with open("./xueqiu.js") as f:
js_code = f.read()
cookie_acw_sc__v2 = execjs.compile(js_code).call("get_cookie", arg1)
print(cookie_acw_sc__v2)
cookies = {
'acw_sc__v2': cookie_acw_sc__v2,
# ...
}
headers = {
#...
}
response = requests.get('https://xueqiu.com/today', cookies=cookies, headers=headers).text
print(response)
// 调用的方法
[f(-302, -39, -274, -178, "mK8a") + o(430, 0, "sXA$") + "r"](t[c(-294, "o#Hz", 0, -62)](t[h(1402, 1326, 1439, "wJdL")], t[h(1596, 1182, 1426, "2QAW")]))[c(-180, "mK8a", 0, -159)](t[h(1113, 950, 1143, "Es!I")])
// 逐步拆解
[f(-302, -39, -274, -178, "mK8a") + o(430, 0, "sXA$") + "r"]
// => ['constructor']
(t[c(-294, "o#Hz", 0, -62)](t[h(1402, 1326, 1439, "wJdL")], t[h(1596, 1182, 1426, "2QAW")]))
// => debugger
[c(-180, "mK8a", 0, -159)]
// => call
(t[h(1113, 950, 1143, "Es!I")])
// => action
func_constructor_ = Function.prototype.constructor;
Function.prototype.constructor = function (a) {
if (a == 'debugger') {
return function(){};
}
return func_constructor_(a);
};
xhr中添加接口的路径:/info_api/policyType/showPolicyType
找到interceptors.response.use的位置,返回的结果中存在了加密数据data。
找到生成o的位置:
找到生成的方法:
这里缺少了Writer,断点进入,这个代码是在vm中的生成的,找到函数的位置,扣下来完整的代码,用变量接收:
var writer_;
commonjsGlobal = global;
!function (g) {
var r, e, t, i;
r = {...},
e = {},
t = [16],
i = function t(n) {
var o = e[n];
return o || r[n][0].call(o = e[n] = {
exports: {}
}, t, o, o.exports),
o.exports
}(t[0]),
i.util.global.protobuf = i,
module && module.exports && (module.exports = i)
// 方法存放在i变量中
writer_ = i;
}()
function PolicyInfoByTypeIdParam_encode(m, w) {
if (!w)
w = writer_.Writer.create()
if (m.policyType != null && Object.hasOwnProperty.call(m, "policyType"))
w.uint32(10).string(m.policyType)
if (m.centralId != null && Object.hasOwnProperty.call(m, "centralId"))
w.uint32(18).string(m.centralId)
if (m.province != null && Object.hasOwnProperty.call(m, "province"))
w.uint32(26).string(m.province)
if (m.city != null && Object.hasOwnProperty.call(m, "city"))
w.uint32(34).string(m.city)
if (m.downtown != null && Object.hasOwnProperty.call(m, "downtown"))
w.uint32(42).string(m.downtown)
if (m.garden != null && Object.hasOwnProperty.call(m, "garden"))
w.uint32(50).string(m.garden)
if (m.sort != null && Object.hasOwnProperty.call(m, "sort"))
w.uint32(56).uint32(m.sort)
if (m.pageNum != null && Object.hasOwnProperty.call(m, "pageNum"))
w.uint32(64).uint32(m.pageNum)
if (m.pageSize != null && Object.hasOwnProperty.call(m, "pageSize"))
w.uint32(72).uint32(m.pageSize)
if (m.homePageFlag != null && Object.hasOwnProperty.call(m, "homePageFlag"))
w.uint32(80).uint32(m.homePageFlag)
return w
}
/**
* 接收类型和页号,返回请求所需的参数
* @param type 类型,str
* @param page 页号,int
* @returns {*} 加密后的数据,字典格式,数据在"data"
*/
function get_data(type, page) {
var data = {
"policyType": type,
"province": "",
"city": "",
"downtown": "",
"garden": "",
"centralId": "",
"sort": 0,
"homePageFlag": 1,
"pageNum": page,
"pageSize": 7
}
result = PolicyInfoByTypeIdParam_encode(data).finish().slice()
return result;
}
console.log(get_data("3", 1))
import execjs
import requests
cookies = {
# 略
}
headers = {
'Accept': 'application/json, text/plain, */*',
# ... 略
}
with open("./spolicy.js") as f:
js_code = f.read()
# 获取type4的,第一页的请求加密参数
data = execjs.compile(js_code).call("get_data", "4", 1)
print(data)
response = requests.post(
'http://www.spolicy.com/info_api/policyType/showPolicyType',
cookies=cookies,
headers=headers,
# 转为bytes
data=bytes(data["data"]),
verify=False,
).json()
print(response)
url:https://ec.minmetals.com.cn/open/home/purchase-info
每次请求都有两个接口,public获取公钥,用于后面加密参数;by-lx-page是数据接口,参数param是加密后的请求参数
/minmetals.js:52
return A[e].call(t.exports, t, t.exports, g),
^
TypeError: Cannot read property 'call' of undefined
// ....其他代码省略
function get_param(public_key, param) {
// public_key
r = public_key
// e: params
e = param
t.setPublicKey(r),
a = m(m({}, e), {}, {
// 这里是md5加密,不用扣代码了,直接写一个
sign: md5Hash(JSON.stringify(e)),
timeStamp: +new Date
});
s = t.encryptLong(JSON.stringify(a))
return s
}
// 公钥
r = 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCSiymi3Afc6HgSatBwseBv/Q87Ul18dAPavud/9Jbr+w2xIoD9t/f1k8A/cv2apGmPwKnudecWa5IfXPVUvoqjG8GsTBR9kL4QKkBveZx46wx2KZEBSbjx9Ok92hgr6sCEHT4sO53VF6rhzYJ4WaqsugGdL3CrZEGV3x7MuvZ0tQIDAQAB'
// 请求参数
e = {
"inviteMethod": "",
"businessClassfication": "",
"mc": "",
"lx": "ZBGG",
"dwmc": "",
"pageIndex": 1
}
get_param(r, e)
分为两个步骤,1是获取public公钥,2是将参数和公钥进行加密,发起请求
import execjs
import requests
# 获取public
import requests
cookies = {
# ...
}
headers = {
# ...
}
# 获取public_key
public_key = requests.post('https://ec.minmetals.com.cn/open/homepage/public', cookies=cookies, headers=headers).text
print(public_key)
# 获取parms加密参数
with open("./minmetals.js") as f:
js_code = f.read()
param = {
"inviteMethod": "",
"businessClassfication": "",
"mc": "",
"lx": "ZBGG",
"dwmc": "",
"pageIndex": 2
}
param = execjs.compile(js_code).call("get_param", public_key, param)
json_data = {
'param': param
}
response = requests.post(
'https://ec.minmetals.com.cn/open/homepage/zbs/by-lx-page',
cookies=cookies,
headers=headers,
json=json_data,
).json()
print(response)
url:https://y.qq.com/n/ryqq/search
接口中sign参数为加密参数
加载器
n为加载器,把n的代码复制(webpack)
var loader_;
!function(e) {
// ...
// f是加载器,用全局变量接收
loader_ = f;
}([])
加载器方法
这里是具体方法,把整个代码复制,在原来的代码中导入
// 加载这个方法的所有代码
require("module")
var loader_;
!function(e) {
// ...
// f是加载器,用全局变量接收
loader_ = f;
}([])
使用相同的参数,网页中返回的加密参数值为:"zzb5f83fe81ctmp7buyrgfgmht5hfggeb4159a0"
// ....
// 对比和网页的加密结果是否一致
origin_result = "zzb5f83fe81ctmp7buyrgfgmht5hfggeb4159a0"
result = loader_(350).default(t_data)
// 并不一致
// zzb5f83fe81ctmp7buyrgfgmht5hfggeb4159a0
// zzb2938d59cvfb4kyj1do3c4ldgj6o9qe0f41793
加密方法是一样的,但是结果不一样,考虑是环境的问题,补充一些环境再测试
navigator = {
canGoBack: true,
canGoForward: false,
currentEntry: {
"id": "661fc912-9d2d-45e5-a144-fae79c979d88",
"index": 5,
"key": "0c8d1b22-53bb-4d51-98e0-5830e055d2b9",
"ondispose": null,
"sameDocument": true,
"url": "https://y.qq.com/n/ryqq/search?w=%E5%91%A8%E6%9D%B0%E4%BC%A6&t=song&remoteplace=txt.yqq.top"
},
"oncurrententrychange": null,
"onnavigate": null,
"onnavigateerror": null,
"onnavigatesuccess": null,
"transition": null
}
location = {
"ancestorOrigins": {},
"href": "https://y.qq.com/n/ryqq/search?w=%E5%91%A8%E6%9D%B0%E4%BC%A6&t=song&remoteplace=txt.yqq.top",
"origin": "https://y.qq.com",
"protocol": "https:",
"host": "y.qq.com",
"hostname": "y.qq.com",
"port": "",
"pathname": "/n/ryqq/search",
"search": "?w=%E5%91%A8%E6%9D%B0%E4%BC%A6&t=song&remoteplace=txt.yqq.top",
"hash": ""
}
// ....
console.log("zzb5f83fe81ctmp7buyrgfgmht5hfggeb4159a0");
console.log(loader_(350).default(t_data));
//zzb5f83fe81ctmp7buyrgfgmht5hfggeb4159a0
//zzb5f83fe81ctmp7buyrgfgmht5hfggeb4159a0
import time
import execjs
import requests
# cookies和headers一定要正确才能请求到
cookies = {
# ...
}
headers = {
# ...
}
# 请求参数
singer = "周杰伦"
page_num = 1
data = '{"comm":{"cv":4747474,"ct":24,"format":"json","inCharset":"utf-8","outCharset":"utf-8","notice":0,"platform":"yqq.json","needNewCode":1,"uin":"1152921504860531892","g_tk_new_20200303":1531704962,"g_tk":1531704962},"req_1":{"method":"DoSearchForQQMusicDesktop","module":"music.search.SearchCgiService","param":{"remoteplace":"txt.yqq.top","searchid":"68939297501935721","search_type":0,"query":"'+singer+'","page_num":'+str(page_num)+',"num_per_page":10}}}'
# 获取sign加密参数
with open("./loader.js") as f:
js_code = f.read()
time_str = round(time.time() * 1000)
sign = execjs.compile(js_code).call("get_sign", data)
params = {
# 时间戳
'_': time_str,
# 加密参数
'sign': sign,
}
# 这里data要编码
response = requests.post('https://u.y.qq.com/cgi-bin/musics.fcg', params=params, cookies=cookies, headers=headers, data=data.encode()).json()
print(response)
企查查的加密参数为接口的请求标头中的参数,key和value都不是固定的。
还有一个参数是X-Pid,可以在页面返回的数据中找到。
由于这个参数设置了headers的key和value,所以在代码里搜 "headers[" 或者 "headers." 找到加密的位置。
加密的位置就在这里,然后去扣所有的代码,最后会生成js文件,调用run方法即可生成接口参数。
可以看到最后生成的方法中需要两个参数,一个是path,一个是tid,最后的json_data是post请求的参数。
function run(path, tid, json_data){}
path:即请求的路径,例如请求接口:https://www.qcc.com/api/datalist/mainmember?keyNo=....
,path参数即为/api/datalist/mainmember?keyNo=....
tid:tid在请求的页面中,可以使用正则表达式提取出来。
pid:就在tid旁边,这个参数放在headers里,key是x-pid
至此就可以对企查查的接口进行爬取。
import re
import execjs
import requests
# TODO 这里补充一下cookies
cookies = {
}
def build_api_headers(url, key_no, pid, tid, json_data=None):
"""
构造请求头
Args:
url: 链接,包含参数
key_no: 企业编号
pid: 页面的加密参数pid
tid: 页面的加密参数tid
json_data: post请求的json数据
Returns:
response对象
"""
headers = {'authority': 'www.qcc.com', 'accept': 'application/json, text/plain, */*',
'accept-language': 'zh-CN,zh;q=0.9', 'cache-control': 'no-cache', 'content-type': 'application/json',
'origin': 'https://www.qcc.com', 'pragma': 'no-cache',
'sec-ch-ua': '"Not/A)Brand";v="99", "Google Chrome";v="115", "Chromium";v="115"',
'sec-ch-ua-mobile': '?0', 'sec-ch-ua-platform': '"macOS"', 'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors', 'sec-fetch-site': 'same-origin',
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36',
'x-requested-with': 'XMLHttpRequest', "referer": f'https://www.qcc.com/firm/{key_no}.html', 'x-pid': pid}
# 正则取出'/api'及其后面的所有内容
path = re.findall(r'(/api.*)', url)[0]
print(f"path: {path}")
# 执行qichacha.js中的run方法
with open('./qichacha_.js', 'r', encoding='utf-8') as f:
js = f.read()
if json_data:
ctx = execjs.compile(js).call('run', path, tid, json_data)
else:
ctx = execjs.compile(js).call('run', path, tid)
# ctx是个字典,便利出所有的key和value,添加到headers中,不要覆盖原来的headers
for k, v in ctx.items():
headers[k] = v
return headers
def post_api(url, key_no, pid, tid, json_data=None):
"""
请求企查查api接口,post请求
Args:
url: 链接,包含参数
key_no: 企业编号
pid: 页面的加密参数pid
tid: 页面的加密参数tid
json_data: post请求的json数据
Returns:
response对象
"""
headers = build_api_headers(url, key_no, pid, tid, json_data)
response = requests.post(url, cookies=cookies, headers=headers, json=json_data)
return response
def get_api(url, key_no, pid, tid, json_data=None):
"""
get请求企查查api接口
Args:
url: 链接,包含参数
key_no: 企业编号
pid: 页面的加密参数pid
tid: 页面的加密参数tid
Returns:
response对象
"""
headers = build_api_headers(url, key_no, pid, tid, json_data)
response = requests.get(url, cookies=cookies, headers=headers)
return response
if __name__ == '__main__':
# 以某企业为例
key_no = '5dffb644394922f9073544a08f38be9f'
pid = 'e19fd3c7ed52c1725cfff3226ba8af8c'
tid = 'd471a1659954b0cf6eb558c770dfdb3b'
# 请求可能需要会员,如果没有会员可以访问其他不需要会员的接口
# get请求
page_index = 1
get_url = f'https://www.qcc.com/api/datalist/mainmember?keyNo={key_no}&nodeName=IpoEmployees&pageIndex={page_index}'
main_member = get_api(get_url, key_no, pid, tid).json()
print(main_member)
# post请求
post_url = 'https://www.qcc.com/api/datalist/financiallist'
json_data = {
'keyNo': '5dffb644394922f9073544a08f38be9f',
'type': 'cm',
'reportType': 1,
'reportPeriodTypes': [
0,
4,
],
'currency': '',
'rate': 1,
}
financial = post_api(post_url, key_no, pid, tid, json_data=json_data).json()
print(financial)
url:https://www.uyanip.com/register
1、接收图片,识别出验证码,拿到Cookies
2、发送短信,拿到token
3、发送注册请求
def get_img():
"""
获取验证码
:return: 返回cookies和验证码
"""
headers = {} # 省略
response = requests.get('https://api.duyandb.com/auth/register/captcha', headers=headers)
print(response.cookies)
# 检查响应状态码是否为 200
if response.status_code == 200:
# 将响应的内容解析为图片
image = Image.open(BytesIO(response.content))
captcha_code = ocr.classification(image)
else:
print("请求失败,状态码:", response.status_code)
return None
# 这里的coockies用于下一次请求
return {
"cookies":response.cookies, "captcha_code":captcha_code
}
def send_code(phone_number:str, captcha_code, cookies):
"""
发送短信验证码
:param phone_number: 手机号
:param captcha_code: 图片验证码
:param cookies: 图片验证码拿到的Cookies
:return: {'data':'', 'errCode': 0}
"""
"""
发送验证码
:param phone_number: 手机号
:return: {'data':'', 'errCode': 0}
"""
headers={} # 省略
# 这里传入手机号和图片验证码
json_data = {
'value': phone_number,
'captchaCode': captcha_code,
}
response = requests.post('https://api.duyandb.com/auth/register/smscode', cookies=cookies, headers=headers,
json=json_data)
# 返回data和状态码,状态码为0则请求成功
return response.json()
手机验证码发送成功后,就可以请求注册接口了
def register(phone_number:str):
"""
注册
:param phone_number: 手机号
:return: 成功或None
"""
headers = {} # 省略
# 获取图片验证码和Cookies
img_obj = get_img()
if img_obj:
# 发送短信,返回token。验证码错误会失败
code_obj = send_code(phone_number, img_obj['captcha_code'], img_obj["cookies"])
if code_obj["errCode"] != 0:
print(code_obj['data'])
return None
print(f"code_obj:{code_obj}")
# 手机验证码
phone_code = ''
phone_code = input("请输入手机验证码:")
json_data = {
# 这里需要传入短信验证码接口返回的token
'vtoken': code_obj['data'],
# 手机号
'phone': phone_number,
# 手机短信验证码
'smscode': phone_code,
'password': 'mima123456',
'confirmPassword': 'mima123456',
# 随机用户名
'nickname': generate_username(),
'pinvitationCode': '',
'type': 0,
}
# 发送注册请求
response = requests.put('https://api.duyandb.com/auth/register/submit', headers=headers, json=json_data)
# 成功会返回{'errCode':0, 'data': ''}
return response.json()
如果你有短信验证码的接口这里可以进行批量注册。另外代码中没有添加代理,如果需要批量注册可能需要增加代理。
if __name__ == '__main__':
result = register('17118060285')
if result and result.get('errCode') == 0:
print("注册成功")
else:
print('注册失败,请重试')
password是加密的
使用xhr断点或者堆栈找到加密的位置
// 加密的js代码
var i = r(n(2132));
i.default.setMaxDigits(130);
var c = new i.default.RSAKeyPair("10001","", s['prod']);
i.default.encryptedString(c, encodeURIComponent(e))
下面我们来补全这些代码
这里的n是加载器,下标2132这个方法就是i。
首先先把加载器的代码拿下来,然后再去找2132对应的方法,把方法放进数组里。
点击n的位置跳转到代码,前面部分是加载器,后面是所有的方法。这里只拿加载器,就是最开头的部份,后面跟着的数组是方法。
下一部是把这个方法里的代码复制到加载器的数组里,
// 全局变量
var loader;
// 加载器
!function(e){
// ...
// 里面的方法都赋值给了o,所以这里用全局变量接收o
loader = o;
}([
// 这里面放方法,这个代码在第二步里找
function(e, t) {}
])
loader(0) // 这里就拿到了n(2132)对应的方法
最终的js代码是这样的。yuekeyun.js
// 全局变量
var loader;
// 加载器
!function(e){
// ...
// 里面的方法都赋值给了o,所以这里用全局变量接收o
loader = o;
}([
// 这里面放方法,这个代码在第二步里找
function(e, t) {}
])
// 公钥
s = {
"prod": "837ec9791ee734418f44220b56cd22252c53309f59c560ff231d71e2579d38ea7a4408b017b1af85c6683111da151af25dddc53904a01e219bd56495a1add8cb70e54428bb87d95cd40478f6f800414be8a334ac779f4b819ae94fec240dc2ace1f99df64de88eef7bcbde4aabbdeac0e70a55e61331a9ea3d0546fe647977f9",
}
// loader(0)是RSA加密的函数
var i = loader(0);
function get_password(e) {
i.setMaxDigits(130);
var c = new i.RSAKeyPair("10001", "", s['prod']);
// e参数是密码
return i.encryptedString(c, encodeURIComponent(e));
}