持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第21天, 点击查看活动详情
关于爬虫相关,欢迎先阅读一下我的前几篇文章😶🌫️😶🌫️😶🌫️:「Python」爬虫-1.入门知识简介 - 掘金 (juejin.cn)
「Python」爬虫-2.xpath解析和cookie,session - 掘金 (juejin.cn)
「Python」爬虫-3.防盗链处理 - 掘金 (juejin.cn)
「Python」爬虫-4.selenium的使用 - 掘金 (juejin.cn)
「Python」爬虫-5.m3u8(视频)文件的处理 - 掘金 (juejin.cn)
参考链接:
通过前面几篇文章的学习,这里我们以爬取网易云评论为例,来进行一次综合实战。本文涉及到的知识点主要是断点调试,讲述如何模拟加密。
网易云评论爬取综合案例
本次爬取的目标网站 -> 网易云音乐 (163.com)
本文以爬取毛不易的《呓语》这首歌的评论为例来进行具体的分析。
网址 -> 呓语 - https://music.163.com/#/song?id=1417862046
首先需要找到正确的请求(记得刷新页面)。具体抓包的方式之前有提到过。
先清空所有请求,然后只查看Fetch/XHR
,并刷新页面,然后就会找到一个以get?csrf_token=
开头的请求,我们直接点开预览,如下图:
很显然,我们看到了comments,也就是评论,这就是我们想要的东西。接着展开,如图:
可以看到评论的内容确实是在这个请求里面的。
请求url如下图:
我们再看到负载中-表单数据,发现表单中有两个参数params
以及encSecKey
。很显然,这极有可能就是被加密了,想要直接拿到评论内容应该是没有那么容易的。
然后我们跳转到发起程序,看到如图中的调用堆栈。直接点第一个(第一个是最近被调用的
点击之后就可得到json格式的信息,再点击左下角的花括号,格式化一下代码:
在定位的位置先打上断点
一直请求,直到到我们需要的位置为止.(我们需要的是含有comment的url
通过观察作用域中的request中的url
,知道出现comment
为止。
接下来需要找到加密的代码在什么位置
可以使用Ctrl + F
查找关键字,前面请求的数据是params
和encSecKey
,也可以找到加密的代码所处的位置。
另一种是观察堆栈的请求数据在哪个地方发生了变化导致的数据加密,也可以定位到相同的位置。
一个一个堆栈点击,并观察data里面的变化,u3x.be3x
的下一个堆栈就已经没有加密的东西了,所以加密发生在u3x.be3x
这个里面。
可以设置断点,一步一步的跑,直到观察到data发生变化时
先找到第一个调用的堆栈,然后在send处设置断点,然后点击刷新直接会跳转到加密的那个语句里面,如下图所示位置,这时候去控制台打印,就能得到所需要的东西。
搜索bKf2x
,跳转到13364
行
在控制台尝试输入:
bva9R(["流泪","强"])
bva9R(Tu4y.md)
返回结果如下:
这两串在控制台返回的是一样的值,所以在模拟加密的时候将其固定写死。
如何得到i
和encSecKey
?
定位到13365
行,打个断点:
上图已经找到了发生加密的位置,
继续寻找源头,如下图:
在h.ebcText
处打断点,然后下一步,就可以得到i
的值
这里给个搜索加密参数的技巧,搜索window.arsea
往往可以较为快速的找到加密的位置。
然后我们需要想办法已有的参数进行加密,然而每个网站的加密算法和逻辑都不一定一致,所以想网易云的api发送请求的时候,那必然是照着他的逻辑来了,不然你可能请求不到任何东西。
把return h.encText
那一段代码复制下来,进行处理:
d()函数和图中的是对应的:
function d(d, e, f, g) { d:数据 e:010001 f:很长 g:0CoJUm6Qyw8W8jud
根据上述图片,前面我们已经发现了bva9R(["流泪","强"])
和bva9R(Tu4y.md)
都是固定值。
即e
为010001
, f
是一串很长的字符串。
再看到以下代码:
var h = {} # 空对象
, i = a(16);
观察a()
函数
function a(a = 16) { # 返回16位随机的字符串
var d, e, b = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", c = "";
for (d = 0; a > d; d += 1) # 循环16次
e = Math.random() * b.length, # 随机数 1.2345
e = Math.floor(e), # 取整
c += b.charAt(e); # 去字符串中的xxx位置 b
return c
}
所以a函数的作用就是返回随机的字符串,i是16位的随机值,不妨把i设置成定值
再看下面:
return h.encText = b(d, g),
h.encText = b(h.encText, i),
h.encSecKey = c(i, e, f),
h
以上三行等价于 :
h.encText = b(d,g)
h.encText = b(h.encText, i)
h.encSecKey = c(i,e,f)
return h
先执行h.encText = b(d,g)
传进b()
中的参数中d
是数据,g
是密钥。
观察b()
函数:
function b(a, b) { # a是要加密的内容--即data, b--密钥
var c = CryptoJS.enc.Utf8.parse(b) # b是密钥
, d = CryptoJS.enc.Utf8.parse("0102030405060708")
, e = CryptoJS.enc.Utf8.parse(a) # e是数据
, f = CryptoJS.AES.encrypt(e, c, { # c 加密的密钥
iv: d, # 偏移量
mode: CryptoJS.mode.CBC # 模式:CBC
});
return f.toString()
}
然后在执行h.encText = b(h.encText, i)
,这里把之前的返回值h.encText
继续放到了b()
函数里,说明进行了两次加密。
数据 + g => b => 第一次加密返回值 + i => b => params
然后执行的是h.encSecKey = c(i,e,f)
,
观察c(i,e,f)
函数 , 已知e:010001
,i
是密钥
function c(a, b, c) { # c里面不产生随机数
var d, e;
return setMaxDigits(131),
d = new RSAKeyPair(b,"",c),
e = encryptedString(d, a)
}
c()
中的参数f
是通过e
函数得到的。而f.encText = c(a + e, b, d)
c函数是不产生随机数的,但是a()
函数产生随机数,所以只需要让a()
函数不产生随机数,也就是把function a(a)
中的参数a
固定,那么a()
函数返回的值就是固定的了,而不是随机的字符串。
整个分析的js
代码如下
# 1.找到未加密的参数 # window.arsea(参数,xxx,xxx)\
# 2.想办法把参数进行加密(必须参考网易的逻辑),params -> encText , encSecKey -> encSecKey
# 3.请求到网易,拿到评论信息
'''
处理加密过程
function() {
function a(a = 16) { # 返回16位随机的字符串
var d, e, b = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", c = "";
for (d = 0; a > d; d += 1) # 循环16次
e = Math.random() * b.length, # 随机数 1.2345
e = Math.floor(e), # 取整
c += b.charAt(e); # 去字符串中的xxx位置 b
return c
}
function b(a, b) { # a是要加密的内容
var c = CryptoJS.enc.Utf8.parse(b) # b是密钥
, d = CryptoJS.enc.Utf8.parse("0102030405060708")
, e = CryptoJS.enc.Utf8.parse(a) # e是数据
, f = CryptoJS.AES.encrypt(e, c, { # c 加密的密钥
iv: d, # 偏移量
mode: CryptoJS.mode.CBC # 模式:CBC
});
return f.toString()
}
function c(a, b, c) { # c里面不产生随机数
var d, e;
return setMaxDigits(131),
d = new RSAKeyPair(b,"",c),
e = encryptedString(d, a)
}
function d(d, e, f, g) { d:数据 e:010001 f:很长 g:0CoJUm6Qyw8W8jud
var h = {} # 空对象
, i = a(16); # i是16位的随机值,把i设置成定值
return h.encText = b(d, g),
h.encText = b(h.encText, i),
h.encSecKey = c(i, e, f),
h
}
/* 以上三行等价于 ->
* h.encText = b(d,g) # g是密钥
* h.encText = b(h.encText, i) # 返回的就是params i也是密钥
* h.encSecKey = c(i,e,f) # 得到的就是encSecKey e和f是固定的,所以这时候把i也固定,就说明返回的encSecKey也是固定的
* return h
* 两次加密:
* 数据 + g => b => 第一次加密 + i => b = params
*/
function e(a, b, d, e) {
var f = {};
return f.encText = c(a + e, b, d),
f
}
window.asrsea = d,
window.ecnonasr = e
}();
'''
至此,我们再来捋一下,我们需要请求的url为
url = "https://music.163.com/weapi/comment/resource/comments/get?csrf_token="
,
由于发送请求的参数是params
和encSecKey
,而这个参数往往是具有时效性的,所以我们需要模拟一下网易云的加密算法,从而得到params
和encSecKey
参数。再向api发送请求,并得到返回的text
,也就是我们想要的评论了!
/*请求的参数*/
csrf_token: ""
cursor: "-1"
offset: "0"
orderType: "1"
pageNo: "1"
pageSize: "20"
rid: "R_SO_4_1417862046"
threadId: "R_SO_4_1417862046"
对于function d(d, e, f, g)
函数中的参数如下:
# 服务于d的
f = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"
g = "0CoJUm6Qyw8W8jud"
e = "010001"
i = "d8YcSIZJWOho8lxf" # 手动固定的 ->别人的函数中是随机的
通过固定的i
结合前面分析的js代码,可以得到encSecKey
def get_encSecKey(): # 由于i是固定的,所以encSecKey就是固定的,c()函数的结果就是固定的
return "abad643b9dfb5ab1456db763d10c39f633729bec3edc4f22a433772d0eb1a0b6dcf44a22d734565b7525c0e32a3b930ff1ac79a2cbade5b91bf9a9887bd3fa04b0468a4f450cdfcf41afb00402272fc860ff21960eee003e3f7b29f1066a6385dd53f33a647c5ef7c83377d2ce4bd44e0e72cdd753a559a327327ecbd5d5080b"
前面的function a(a)
函数返回的是长度为16的字符串,所以这里构造一个转化为16长度倍数的函数如下:
# 转化成16的倍数,为下方的加密算法服务
def to_16(data):
pad = 16 - len(data) % 16
data += chr(pad) * pad
return data
整个加密过程如下:
# 加密过程
def enc_params(data, key): # 加密过程
iv = "0102030405060708" # 偏移量
data = to_16(data)
aes = AES.new(key=key.encode("utf-8"), IV=iv.encode("utf-8"), mode=AES.MODE_CBC) # 创建加密器
bs = aes.encrypt(data.encode("utf-8")) # 加密
# 加密的内容的长度必须是16的倍数
ans = str(b64encode(bs), "utf-8")
return ans # 转换成字符串返回
由前面分析可知,前面进行了两次加密所以函数设计如下:
# 把参数进行加密
def get_params(data): # 默认这里接收到的是字符串
first = enc_params(data, g) # 第一次加密
second = enc_params(first, i) # 第二次加密
return second # 返回的是params
最后向url发送带参数的请求即可:
# 发送请求,
resp = requests.post(url, data={
"params": get_params(json.dumps(data)),
"encSecKey": get_encSecKey()
})
完整代码如下:
import requests
import json
from Crypto.Cipher import AES
from base64 import b64encode
url = "https://music.163.com/weapi/comment/resource/comments/get?csrf_token="
# 请求的方式是post
data = {
"csrf_token": "",
"cursor": "-1",
"offset": "0",
"orderType": "1",
"pageNo": "1",
"pageSize": "20",
"rid": "R_SO_4_1417862046",
"threadId": "R_SO_4_1417862046"
}
# 服务于d的
f = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"
g = "0CoJUm6Qyw8W8jud"
e = "010001"
i = "d8YcSIZJWOho8lxf" # 手动固定的 ->别人的函数中是随机的
def get_encSecKey(): # 由于i是固定的,所以encSecKey就是固定的,c()函数的结果就是固定的
return "abad643b9dfb5ab1456db763d10c39f633729bec3edc4f22a433772d0eb1a0b6dcf44a22d734565b7525c0e32a3b930ff1ac79a2cbade5b91bf9a9887bd3fa04b0468a4f450cdfcf41afb00402272fc860ff21960eee003e3f7b29f1066a6385dd53f33a647c5ef7c83377d2ce4bd44e0e72cdd753a559a327327ecbd5d5080b"
# 转化成16的倍数,为下方的加密算法服务
def to_16(data):
pad = 16 - len(data) % 16
data += chr(pad) * pad
return data
# 加密过程
def enc_params(data, key): # 加密过程
iv = "0102030405060708" # 偏移量
data = to_16(data)
aes = AES.new(key=key.encode("utf-8"), IV=iv.encode("utf-8"), mode=AES.MODE_CBC) # 创建加密器
bs = aes.encrypt(data.encode("utf-8")) # 加密 加密的内容的长度必须是16的倍数
ans = str(b64encode(bs), "utf-8")
return ans # 转换成字符串返回
# 把参数进行加密
def get_params(data): # 默认这里接收到的是字符串
first = enc_params(data, g)
second = enc_params(first, i)
return second # 返回的是params
# 发送请求,
resp = requests.post(url, data={
"params": get_params(json.dumps(data)),
"encSecKey": get_encSecKey()
})
print(resp.text)
到此,断点调试相关内容就更新到这儿了,如果对你有帮助的话,就请留下足迹吧~🎈
往期好文推荐🪶
「MongoDB」Win10版安装教程