本系列文章共十篇:
【爬虫成长之路】(一)爬虫系列文章导读
【爬虫成长之路】(二)各篇需要用到的库和工具
【爬虫成长之路】(三)【大众点评】selenium爬虫
【爬虫成长之路】(四)【大众点评】selenium登录+requests爬取数据
【爬虫成长之路】(五)【大众点评】浏览器扫码登录+油猴直接爬取数据
【爬虫成长之路】(六)【大众点评】mitmproxy中间人代理爬虫
【爬虫成长之路】(七)【大众点评】PC微信小程序+requests爬取数据
【爬虫成长之路】(八)【大众点评】安卓APP爬虫
声明:本文相比于前面的文章来说,算是进阶爬虫了,由于涉及到较多的加解密算法,故只提供相关的加解密算法,不提供加解密算法的秘钥。读者在学习过程中,主要还是以学习爬虫思想为主
本文需要用到的工具:
Fiddler
、JSON解析工具
、小程序逆向工具
本文需要用到的库:requests
、json
、base64
、Crypto
、xxtea
、zlib
、urllib.parse
...
爬取目标数据:
- 指定城市的店铺列表及其评分数据
- 指定店铺下的用户评论数据
一、需求分析
这一篇总共需要爬取两个页面的数据,分别是:
- 某城市的
店铺列表
页面 - 某店铺的
评论列表
页面
二、获取目标页面的URL
由于这里的目标对象是微信小程序,获取接口就必须借助第三方工具了,文中使用的是Fiddler,当然你也可以用你顺手的工具,接下来简单讲下如何操作:
- 打开Fiddler,进入localhost://[port]获取Fiddler的证书,安装证书
- 配置好Fiddler,使其可以抓HTTPS的包
- 打开电脑客户端点评的小程序,进入到对应的页面,如果配置没有问题就能在Fiddler中看到数据包了
- 如果Fiddler里全是Tunnel to 443,请重新安装证书,并重启Fiddler,如果还有别的问题,请自行百度
- 开启代理后,小程序的很容易出现获取不到数据的状况,这个多刷新几次,到处点几下就好了,多试几次就能找到接口了
- 如果出来的过太多了,也可以在Fiddler中开启过滤,pass掉一部分无关URL
操作完后,获取到了店铺列表接口和评论列表接口,分别如下:
# 店铺列表接口
https://m.dianping.com/wxmapi/index/cnxh?...
# 评论列表接口
https://m.dianping.com/ugc/review/reviewlist?...
三、请求头Header、URL参数解析
获取到相应的接口后,就要去看里面的参数了,这里总共有三部分的参数需要查看,分别是:
- URL 后面带了参数
- response 返回的参数(json数据,未加密)
- 请求头Header里面还有参数
评论接口里面的相关参数:
序号 | 名称 | 参数个数 | 重点参数 | 是否加密 | 是否固定 |
---|---|---|---|---|---|
1 | 方法类型 | GET |
- | - | - |
2 | URL参数 | 14 |
shopUuid ,cx ,_token ,optimus_uuid
|
是 | 否 |
3 | Header参数 | 17 |
openid ,openidPlt ,token
|
是 | 否 |
4 | response | - | reviewList |
否 | 否 |
Header参数:
序号 | Header参数 | 值 | 说明 |
---|---|---|---|
1 | Host | m.dianping.com | 域名 |
2 | Connection | keep-alive | - |
3 | User-Agent | Mozilla/5.0 (Windows NT 6.1;WOW64) ***WindowsWechat | 用户代理 |
4 | channel | weixin | 渠道 |
5 | channelversion | 7.0.9 | 渠道版本 |
6 | content-type | application/json | 传输类型 |
7 | minaname | dianping-wxapp | 可固定 |
8 | minaversion | 6.7.0 | 可固定,也可适当变化 |
9 | openid |
Hv********** | 微信提供给小程序的唯一ID |
10 | openidPlt |
oPp*********** | 服务端生成 |
11 | platform | windows | 平台 |
12 | platformversion | 1064 | 平台版本win10,64位 |
13 | sdkversion | 2.13.2 | SDK版本 |
14 | token |
12a********** | 服务端生成 |
15 | wechatversion | 7.0.9 | 微信版本 |
16 | Referer | https://servicewechat.com/wx734c1ad7b3562129/231/page-frame.html | 来源 |
17 | Accept-Encoding | gzip,deflate,br | 接受的编码格式 |
URL 参数:
序号 | URL参数 | 值 | 说明 |
---|---|---|---|
1 | tagType | 1 | 评论类型,美食评论or其他类型评论 |
2 | tag | 全部 | 获取全部评论,也可进行筛选 |
3 | offset | 10 | 页偏移,从0开始,以10为步进 |
4 | shopUuid |
H58vsmtj2***** | 店铺ID,店铺列表获取 |
5 | cx |
WX__ver1.2.0_CCCC_QZ+jty4u********XU= | 本地生成 |
6 | mtsiReferrer | /packages/ugc/pages/reviewlist/reviewlist?msource=wxappmain&shopUuid=H58vsmtj2*****&tag=全部&tagType=1 | 类似于refer |
7 | _token |
eJx********* | 本地生成 |
8 | optimus_uuid |
176****** | 本地生成 |
9 | optimus_platform | 13 | 平台类型 |
10 | optimus_partner | 203 | 未知,可固定 |
11 | optimus_risk_level | 71 | 风险等级,可固定 |
12 | optimus_code | 10 | 可固定 |
13 | pullDown | false | 可固定 |
14 | reLoad | false | 可固定 |
店铺列表接口相关参数:
序号 | 名称 | 参数个数 | 重点参数 | 是否加密 | 是否固定 |
---|---|---|---|---|---|
1 | 方法类型 | GET |
- | - | - |
2 | URL参数 | 14 |
wxuuid ,sessionId ,optimus_uuid ,_token
|
是 | 否 |
3 | Header参数 | 17 |
openid ,openidPlt ,token
|
是 | 否 |
4 | response | - | reviewList |
否 | 否 |
Header参数(不变,同评论列表接口):
URL参数:
序号 | 名称 | 值 | 说明 |
---|---|---|---|
1 | cityId | 4 | 城市ID |
2 | wxuuid |
Hvc*** | 同微信openid |
3 | page | 2 | 页码 |
4 | lat | ** | 纬度 |
5 | lng | ** | 经度 |
6 | sessionId |
e7ef*** | 会话ID |
7 | mtsiReferrer | /pages/index/index?cityId=4&wxuuid=Hvc...&page=2&lat=...&lng=...&sessionId=e7ef... | 类似refer |
8 | optimus_uuid |
1763b93*****d | 本地生成的uuid参数 |
9 | optimus_platform | 13 | 平台类型 |
10 | optimus_partner | 203 | 可固定 |
11 | optimus_risk_level | 71 | 风险等级,可固定 |
12 | optimus_code | 10 | 可固定 |
13 | _token |
eJx***********pAA== | 本地生成 |
四、请求头Header、URL参数构造
在进行爬取时,我们必须要能够重新构造请求里的全部参数,因此,知道这些参数是如何生成的就十分有必要了,当然,这一步也是爬虫工作中最难、最费时间的一步,接下来教大家分析这些参数是如何生成的,其中对命名不准确的参数要大胆的去猜测其含义。
STEP 1:分析Header里的参数
参数一:
openid
从上面的Header
参数列表我们可以知道,Header里重点需要关注的参数就3个,分别是openid
、openidPlt
、token
,有过微信公众号或小程序开发的都应该知道,openid是微信为公众号或小程序提供的唯一openid,用于标识不同的用户,在不同公众号或小程序,同一用户的openid是不一致的;所以这个openid我们不能自行生成,必须要服务端返回给我们才行。如果没有相关开发经验,可以通过百度相关字段,也能在微信开发文档中找到其含义。参数二:
openidPlt
这个参数也带了openid,应该也与openid有关系,不是本地生成的就是服务端获取的,就这两种情况,通过对请求的数据进行分析,易知,这个字段是服务端返回的;那如何知道的呢?我们看请求列表中会有这样一个请求,
https://maccount.dianping.com/thirdlogin/ajax/auth
在它的response中,可以看到有openidPlt这个字段(图中没有框起来),这里定位这个字段,需要在请求中找到这个字段第一次出现的位置,否则也是不能确定其是如何产生的。我们经过简单的分析后,可以知道openidPlt这个字段就是通过.../thirdlogin/ajax/auth
这个请求从服务端获取的,因此这个字段是如何来的我们也确定了。
- 参数三:
token
这个token参数的分析同参数二openidPlt
,心细的同学可以发现,在参数二的获取时,其response响应返回的字段中,就有这么一个token参数,于是我们也可以知道,token
这个参数也是服务端返回的,不能再本地生成。
通过上面的分析,Header里的参数已经解决了,知道是怎么来的了。因为Header里的openid
、openidPlt
、token
这三个参数都是通过.../thirdlogin/ajax/auth
这个接口向服务端获取的,因此我们只需要模拟一下这个请求就好了。那我们就再来看一下这个.../thirdlogin/ajax/auth
请求,
- 先看Header,可以知道它使用的方法是
POST
,Header里也没有什么重要的参数,基本可以确定这个请求就是首次验证身份用的。 - 再看URL和Body,可以从下图
.../thirdlogin/ajax/auth的Header
看到,URL中并未带有其他参数,于是再看其body中是否带有相关参数,通过上图token参数获取接口及参数分析
可以看到,其body中有5个参数,分别是code
、sourceType
、directLogin
、cx
、_token
,分析时先从简单的入手,通过对比多个.../thirdlogin/ajax/auth
请求,而我们可以发现,sourceType
和directLogin
这两个字段是固定的,不用变动。而对于其他三个参数code
、cx
、_token
,明显经过加密,我们也不能从上下文的response响应中找到其身影,说明这三个参数就是本地生成的。
现在大概流程就比较清晰了,Header里的关键参数在.../thirdlogin/ajax/auth
的response中,发起.../thirdlogin/ajax/auth
请求需要构造code
、cx
、_token
这三个关键参数,而则三个关键参数是在本地生成的。通过一番百度,查看微信小程序开发手册可以得知,其中code
这个参数是微信APP提供给小程序的,每次提供的code
都是不一样的,这个和openid
不一样,同样是微信提供给小程序的,但openid
是始终不变的,而code
则是小程序通过调用接口,由微信APP生成的,并且有时效性,这个可以通过查看微信小程序登录的时序图来判断。而剩下的两个参数cx
、_token
由于是在本地生成的,就必须要去看小程序的源码了(其实code
也是通过查看小程序源码来确定的...),要查看小程序源码,就需要借助其他的工具了,关于微信小程序逆向又可以写一篇文章了,这不是这篇文章的重点,故不展开讲,下面提供一些微信小程序逆向的参考文章:
- 反编译获取任何微信小程序源码(微信小程序逆向教程)
- wxappUnpacker
- 逆向小程序破解js-(逆向篇)
- 微信小程序“反编译”实战(一):解包
- 小程序爆破之旅---分析微信源码来解析微信小程序文件格式
STEP 2:分析URL里的参数
其实通过上面的分析,也大概可以知道,就拿评论列表里的URL参数来说,关键的参数就3个,分别是cx
、_token
、optimus_uuid
,这里要注意,这里的_token
和前面提到的token
并不是同一个参数,这三个参数的分析思路也也前面的Header里的参数分析很相似,先从请求列表中寻找其首次出现的位置,如果首次首先的位置在请求的URL参数或Body中,则这个参数是本地生成的,如果是在响应的response中,就是从服务端获取到的。这里我就不做进一步的分析了,这三个参数其实也是通过分析反编译后小程序才能分析出来的,这三个参数都是其他参数经过了加密生成的。
这里反编译点评的微信小程序之后,最好还是能够导入到微信开发者工具中进行动态调试,无奈本人太菜,逆向过来的小程序导入后一堆错误,不想去改了,就直接静态分析了,费劲...
在获取到了token参数之后,还需要向服务器提交验证服务器(这一步不确定是否必须,没验证)
所以综合来说,最关键的参数总共有这么几个,即验证身份所需要的
code
、cx
、_token
,知道了这几个参数如何生成,这个小程序爬虫就解决了。其中cx
、_token
可以通过静态或动态分析小程序源码来解出,但code
这个参数由于是微信APP给小程序提供的,所以要获取这个参数,就必须逆向微信APP,然而逆向微信APP这个工程有点大,所以这里就换种思路。在实际测试过程中,我们发现token这个参数的有效时长还是蛮长的,所以我们就先将手动获取到的token
参数保存下来给后面的爬虫用。
通过观察发现,cx
、_token
这两个参数在每次请求时都发生了变化,故这两个参数里必然带了时间戳进行加密,由于是静态解密,所以要做的就是搜索关键字,阅读源码,理清逻辑结构,也没啥技术含量,就不在这里讲解如何操作了。
五、相关参数的加解密算法
这里就直接给出相关参数的加解密算法了,具体如何得来,就是依靠静态分析,当然能动静结合自然是更好的。
通过搜索关键字就能发现,参数加解密的算法全部在finger.js
和konan.js
这两个文件里面,可以多看看finger.js
这个文件,但这个文件太大了,我没看完,就看了部分和参数加解密相关的,需要的同学自行去查看。其中代码涉及到秘钥的部分会隐去,需要学习使用的同学请自行动手反编译小程序后查看。
cx:小程序原始js加密算法(部分代码)
wxPre = "WX__ver1.2.0_CCCC_",
getFinger = function(t) {
fingerData.app = config.app, fingerData.openid = config.openid, fingerData.unionid = config.unionid,
fingerData.mchid = config.mchid;
var e, r = wxPre;
try {
fingerData.location || getLocation(), getTimeStamp(), fingerData.userInfo ?
(e = JSON.stringify(fingerData), r += CBCencrypt(e), t && t(r)) :
getUserInfo(function() {
var e = JSON.stringify(fingerData);
r += CBCencrypt(e), t && t(r);
console.log(r);
});
} catch (e) {
t && t(r);
}
}
function CBCencrypt(t) {
function e() {
for (var t = [ "91EBA6DBE4E***", "C5F5FDF5F2F5F3F***" ], e = [], r = "", n = 0; n < t.length; n++) {
r = "";
for (var i = t[n], o = i.length, a = parseInt("0x" + i.substr(0, 2)), s = 2; s < o; s += 2) {
var c = parseInt("0x" + i.charAt(s) + i.charAt(s + 1));
r += String.fromCharCode(c ^ a);
}
e.push(r);
}
return e; /* e = ["z7Jut6Ywr***", "080706050***"] */
}
var r = e()[0], n = e()[1], i = cryptoJs.enc.Utf8.parse(r), o = { /* i.toString() -> 7a374a757436597**** */
iv: cryptoJs.enc.Utf8.parse(n), /* 偏移量 .toString() -> 30383037303630**** */
mode: cryptoJs.mode.CBC, /* 加密模式 */
padding: cryptoJs.pad.Pkcs7 /* 填充 */
};
return cryptoJs.AES.encrypt(t, i, o).toString(); /* t:待加密内容, i:秘钥, */
}
cx
加解密算法,python
版本
import base64
import urllib.parse
from Crypto.Cipher import AES
class AES_CBC:
def __init__(self):
self.key = 'z7Jut6Ywr***'
self.iv = '080706050***'
def add_to_16(self, value):
while len(value) % 16 != 0:
value += '\0'
return str.encode(value)
def encrypt(self, content):
aes = AES.new(self.add_to_16(self.key), AES.MODE_CBC, self.add_to_16(self.iv))
bs = AES.block_size
pad2 = lambda s: s + (bs - len(s) % bs) * chr(bs - len(s) % bs) # PKS7填充
# 执行加密并转码返回bytes
encrypt_aes = aes.encrypt(str.encode(pad2(content)))
# 用base64转成字符串形式
encrypted_content = str(base64.encodebytes(encrypt_aes), encoding='utf-8')
return encrypted_content.replace('\n', '')
def decrypt(self, en_content):
aes = AES.new(self.add_to_16(self.key), AES.MODE_CBC, self.add_to_16(self.iv))
base64_decrypted = base64.decodebytes(en_content.encode(encoding='utf-8')) # 优先逆向解密base64成bytes
decrypted_content = str(aes.decrypt(base64_decrypted), encoding='utf-8') # 执行解密并转码返回str
unpad = lambda s: s[0:-ord(s[-1])]
return unpad(decrypted_content)
对cx
解密完成后,结果如下。可以看到,这个cx
参数的生成几乎获取了手机的全部能获取到的参数。
{
"system": {
"albumAuthorized": true,
"benchmarkLevel": -1,
"bluetoothEnabled": false,
"brand": "microsoft",
"cameraAuthorized": true,
"errMsg": "getSystemInfo:ok",
"fontSizeSetting": 15,
"language": "zh_CN",
"locationAuthorized": true,
"locationEnabled": true,
"microphoneAuthorized": true,
"model": "microsoft",
"notificationAuthorized": true,
"notificationSoundEnabled": true,
"pixelRatio": 1.25,
"platform": "windows",
"power": 100,
"safeArea": {
"bottom": 676,
"height": 676,
"left": 0,
"right": 415,
"top": 0,
"width": 415
},
"screenHeight": 736,
"screenWidth": 415,
"statusBarHeight": 20,
"system": "win10",
"theme": "light",
"version": "7.0.9",
"wifiEnabled": true,
"windowHeight": 676,
"windowWidth": 415,
"SDKVersion": "2.11.0",
"devicePixelRatio": 1.25,
"WifiInfo": "",
"Beacons": "",
"LaunchOptionsSync": "{\"path\":\"\",\"query\":{},\"scene\":1001,\"referrerInfo\":{}}",
"networkType": "wifi",
"brightness": 0.88,
"StorageInfo": "{\"currentSize\":768,\"errMsg\":\"getStorageInfo:ok\",\"keys\":[\"$PIKE_LOADBALANCE_WIFI\",\"OH_LAST_SELECTED_CITY\",\"WXOWLKEY-unionId\",\"_lx_sdk_lxcuid\",\"_lx_sdk_wxid\",\"_pike_config\",\"city\",\"dp_categoryList\",\"dp_city_history\",\"dp_index_abtest\",\"dp_index_activity\",\"dp_index_group-seckill\",\"dp_index_guesslike\",\"dp_list\",\"dp_my_columns\",\"dp_my_tools\",\"dp_shop\",\"dp_shop_dinevote_showtips\",\"dp_shop_list\",\"dp_shop_pop\",\"dp_shop_style\",\"geo\",\"hotelCheckTime\",\"keyword_history\",\"loccity\",\"logan_days_info\",\"logan_session_token\",\"loganlog_2020-12-07_1_0\",\"mtweapp_api_https://portal-portm.meituan.com/weapp/dynamic/v6/common/caster-report\",\"mtweapp_api_https://portal-portm.meituan.com/weapp/dynamic/v6/common/launch\",\"mtweapp_api_https://portal-portm.meituan.com/weapp/dynamic/v6/dpweapp/shop-branch\",\"mtweapp_api_https://portal-portm.meituan.com/weapp/dynamic/v6/dpweapp/shop-mall\",\"mtweapp_api_https://portal-portm.meituan.com/weapp/dynamic/v6/dpweapp/tab-my-index\",\"openid\",\"quickConfig\",\"showData\",\"uuid\",\"webp\"],\"limitSize\":10000}",
"BatteryInfo": "{\"isCharging\":true,\"level\":\"100\",\"errMsg\":\"getBatteryInfo:ok\"}"
},
"app": 0,
"timestamp": 160799699656
}
_token
加解密算法,python
版本
import base64
import zlib
import urllib.parse
class Token:
def encrypt(self, data):
data_byte = json.dumps(data).encode() # 字典 ---> 字符串 ---> byte数据
compressed_data_byte = zlib.compress(data_byte) # byte数据 ---> 压缩数据
base64_data_byte = base64.b64encode(compressed_data_byte) # 压缩数据 ---> base64加密数据(byte数据)
decode_data = base64_data_byte.decode() # 加密后的byte数据 ---> 字符串token
final_data = urllib.parse.quote(decode_data)
return final_data
def decrypt(self, sign_or_token):
normal_data = urllib.parse.unquote(sign_or_token) # 对其中的url进行解码
decode_data_r = normal_data.encode() # 将字符数据转成byte数据
base64_data_byte_r = base64.b64decode(decode_data_r) # 将byte数据进行解密
compressed_data_byte_r = zlib.decompress(base64_data_byte_r) # 将经过base64解密过的byte数据解压缩
data_r = compressed_data_byte_r.decode() # 将解压后的数据转成字符串
return data_r
这里对_token
解密完成后,解密结果如下,可以看到里面还有一个被加密了的sign
参数:
token_data = {"rId": 100, "ts": int(time.time() * 1000), "cts": int(time.time() * 1000) + 1000000, "brVD": [415, 676],
"brR": [[519, 845], [519, 845], 24, 24], "bI": ["pages/detail/detail", "packages/search/pages/list/list"],
"mT": [], "kT": [], "aT": [], "tT": [],
"sign": "eJx1jl1rw***ByIXg=="}
其实这个sign
参数也是用的和_token
相同的加密算法。
那就还剩下一个optimus_uuid
了,这个怎么生成的我也没弄明白,因为在爬取的过程中发现它不会变动,所以也就不想去看了,逆向后的小程序关于optimus_uuid
的部分如下,所在文件是preyoda.js
和..wechat-sdk/lib/index.js
:
//preyoda.js
exports.default = new r.default({
type: "request",
resolve: function(e) {
var r = e.request;
try {
var a = i.default.getCurrentPage();
a.hasRedirectStatusInited || (u.default.initRedirectStatus(), a.hasRedirectStatusInited = !0);
var s = r.data;
s && s.mtsiReferrer && (s.optimus_uuid = s.optimus_uuid || t.default.get("lxcuid"),
s.optimus_platform = s.optimus_platform || 13, s.optimus_partner = s.optimus_partner || 203,
s.optimus_risk_level = 71, s.optimus_code = 10, e.request = r);
} catch (e) {}
return e;
}
});
// ..wechat-sdk/lib/index.js
Ct.lxcuid = function(e) {
function t(e, t) {
var n, r = 0;
for (n = 0; n < t.length; n++) r |= s[n] << 8 * n;
return e ^ r;
}
var n = S("lxcuid");
if (n) return n;
var r, i, a = function() {
for (var e = 1 * new Date(), t = 0; e === 1 * new Date() && t < 200; ) t++;
return e.toString(16) + t.toString(16);
}, o = +(Math.random() + "").slice(2), c = e.ua || "", s = [], u = 0;
for (r = 0; r < c.length; r++) i = c.charCodeAt(r), s.unshift(255 & i), 4 <= s.length && (u = t(u, s),
s = []);
0 < s.length && (u = t(u, s)), c = u;
var f = 0;
e.sc && (f = +(f = e.sc.split("*"))[0] * +f[1]);
var l = [ a(), o, c, f, a() ].map(function(e) {
return e.toString(16);
}).join("-");
return x("lxcuid", l), l;
}(Ct);
在爬取的过程中,还遇到了验证码,提交验证码有两步,触发验证的数据如下,验证码提交时也有一个重要的参数,是behavior,这里就不再去触发它的验证码了,在测试的过程中可以使用Fiddler劫持response,来达到触发验证码,下面就直接给出behavior的加解密算法,不再进行说明:
{
"msg": "您的网络好像不太给力,请稍后再试",
"code": 406,
"customData": {
"verifyPageUrl": "https://verify.meituan.com/v2/app/general_page?action=spiderindefence&requestCode=ca0d5061bbaf49***&platform=13&adaptor=auto&succCallbackUrl=https://optimus-mtsi.meituan.com/optimus/verifyResult",
"imageUrl": "https://verify.meituan.com/v2/captcha?action=spiderindefence&request_code=ca0d5061bbaf4951944***",
"requestCode": "ca0d5061bbaf49519***",
"verifyUrl": "https://optimus-mtsi.meituan.com/optimus/verify?request_code=ca0d5061bbaf4951***"
}
}
下面是验证码加解密相关的小程序JS源码,文件所在位置是konan.js
:
// XXTEA 加密算法
function xxtea_encrypt(str, key) {
var e, n, o, a, f, i, u = str.length, h = u - 1;
for (n = str[h], o = 0, i = 0 | Math.floor(6 + 0x34 / u); i > 0; --i) {
for (a = (o = o + 0x9E3779B9 & 0xFFFFFFFF) >>> 2 & 3, f = 0; f < h; ++f)
e = str[f + 1];
n = str[f] = str[f] + ((n >>> 5 ^ e << 2) + (e >>> 3 ^ n << 4) ^ (o ^ e) + (key[3 & f ^ a] ^ n)) & 0xFFFFFFFF;
e = str[0];
n = str[h] = str[h] + ((n >>> 5 ^ e << 2) + (e >>> 3 ^ n << 4) ^ (o ^ e) + (key[3 & h ^ a] ^ n)) & 0xFFFFFFFF;
}
return str;
}
function xxtea_decrypt(str, key) {
if (str == "") {
return "";
}
var v = str2long(str, false);
var k = str2long(key, false);
if (k.length < 4) {
k.length = 4;
}
var n = v.length - 1;
var z = v[n - 1], y = v[0], delta = 0x9E3779B9;
var mx, e, p, q = Math.floor(6 + 52 / (n + 1)), sum = q * delta & 0xffffffff;
while (sum != 0) {
e = sum >>> 2 & 3;
for (p = n; p > 0; p--) {
z = v[p - 1];
mx = (z >>> 5 ^ y << 2) + (y >>> 3 ^ z << 4) ^ (sum ^ y) + (k[p & 3 ^ e] ^ z);
y = v[p] = v[p] - mx & 0xffffffff;
}
z = v[n];
mx = (z >>> 5 ^ y << 2) + (y >>> 3 ^ z << 4) ^ (sum ^ y) + (k[p & 3 ^ e] ^ z);
y = v[0] = v[0] - mx & 0xffffffff;
sum = sum - delta & 0xffffffff;
}
return long2str(v, true);
}
behavior参数加解密算法,python版本:
# behavior 的解析和生成
class Behavior:
def gen_behavior(self, key):
t = time.time()
t1 = int(round(t)*1000)
raw_behavior = {
"env": {
"zone": [2**.8, 25.54***],
"client": [63.**, 36.3**],
"Timestamp": [t1, t1 + 2077],
# "Timestamp": [160907***, 160907***],
"count": 1,
"timeout": 0
},
"trajectory": [{
"point": [
[0, 89+random.randint(-2,2), 350, 2077+random.randint(-2,2)],
[0, 101+random.randint(-2,2), 350, 2226+random.randint(-2,2)],
[0, 108+random.randint(-2,2), 350, 2232+random.randint(-2,2)],
[0, 116+random.randint(-2,2), 350, 2258+random.randint(-2,2)],
[0, 120+random.randint(-2,2), 350, 2261+random.randint(-2,2)],
[0, 128+random.randint(-2,2), 350, 2262+random.randint(-2,2)],
[0, 136+random.randint(-2,2), 350, 2266+random.randint(-2,2)],
[0, 144+random.randint(-2,2), 350, 2287+random.randint(-2,2)],
[0, 152+random.randint(-2,2), 350, 2287+random.randint(-2,2)],
[0, 164+random.randint(-2,2), 350, 2289+random.randint(-2,2)],
[0, 177+random.randint(-2,2), 350, 2311+random.randint(-2,2)],
[0, 192+random.randint(-2,2), 350, 2312+random.randint(-2,2)],
[0, 208+random.randint(-2,2), 350, 2314+random.randint(-2,2)],
[0, 236+random.randint(-2,2), 350, 2335+random.randint(-2,2)],
[0, 257+random.randint(-2,2), 350, 2335+random.randint(-2,2)],
[0, 284+random.randint(-2,2), 350, 2336+random.randint(-2,2)],
[0, 306+random.randint(-2,2), 350, 2359+random.randint(-2,2)],
[0, 324+random.randint(-2,2), 350, 2359+random.randint(-2,2)],
[0, 336+random.randint(-2,2), 350, 2359+random.randint(-2,2)]
],
"vector":{
"orientation": "h"
}
}]
}
'''
[{"point":[
[0,93,350,1405],[0,100,350,1436],[0,102,350,1460],[0,106,350,1460],[0,113,352,1460],[0,124,353,1485],[0,136,353,1485],[0,148,353,1485],[0,158,353,1509],[0,176,353,1510],[0,188,353,1510],[0,204,353,1539],[0,217,353,1539],[0,230,353,1539],[0,244,353,1545],[0,258,353,1566],[0,272,353,1566],[0,284,353,1566],[0,298,353,1589],[0,310,353,1589],[0,322,353,1590],[0,332,353,1611],[0,340,353,1612]
],"vector":{"orientation":"h"}}]
'''
en1 = xxtea.encrypt(json.dumps(raw_behavior).replace(' ', ''), key)
en2 = base64.b64encode(en1).decode('utf-8')
return en2
def de_behavior(self, content, key):
en_xxtea = base64.b64decode(content)
raw_data = xxtea.decrypt(en_xxtea, key)
return raw_data.decode('utf-8')
六、优缺点分析
序号 | 优点 | 缺点 |
---|---|---|
1 | 程序运行更快 | 参数构造麻烦 |
2 | - | 需要对小程序进行逆向 |
七、结语
文章到此已经篇幅过大了,就不再对程序进行演示,由于爬虫越高级,就越有可能使服务器无法区分是否是爬虫,这样容易给对方服务器造成过大的压力。但就本文所提到的爬虫而言,由于用户无法生成openid
和code
这两个参数,所以想要利用小程序爬虫发起大规模的并发请求是不现实的,希望同学们仅供学习使用,不要恶意发起大量请求。
在测试过程中发现,小程序爬虫还是很容易触发验证码的,所以想要大量爬取数据,也只能放慢速度,慢慢爬取。
注:
- 如果您不希望我在文章提及您文章的链接,或是对您的服务器造成了损害,请联系我对文章进行修改;
- 本文仅爬取公开数据,不涉及到用户隐私;