话不多说直接开始干

首先明确我们的目标,爬取网易云评论数据(每条评论和评论总数),打开网页版网易云飙升榜
(作为学习用途进行探讨,不涉及任何商业用途,不公开任何数据,只探讨爬虫)

wyy2.png

首先我们很容易的就拿到榜单前100的链接,没有啥技术难度

注意的点是把链接中的#去掉,否则拿不到源码

    listurl = 'https://music.163.com/discover/toplist'
while True:
    response = requests.get(listurl, headers=header)
    html = etree.HTML(response.text)
    url = html.xpath('//ul[@class="f-hide"]/li/a/@href')
    song_name = html.xpath('//ul[@class="f-hide"]/li/a/text()')

拿到所有飙升榜歌曲url

wyy3.png

好了,然后干嘛呢?写个循环遍历挨个挨个拿嘛,简单!

来了

先看看原有数据(评论数,每条评论内容,评论人,评论的点赞数。。。。)
wyy4.png

写到一半突然发现,这个网页是异步加载的

能直接get到的网页都有哪些数据呢?
wyy5.png

好的,啥也没有

这个时候打开我们祖传的f12测试的网络

好样的,非常经典的异步加载
wyy6.png

看看啥类型

非常好,非常经典的post
wyy7.png

看看啥表单

完犊子,又是加密
虽然可以通过复制表单内容,取获取这些信息
但是下一页怎么办?一首歌就有几千页
100首歌,emmmmm,还是得破解一下加密信息
wyy8.png

重点来了

打开启动器,看看是哪个该死的文件发出的加密
wyy9.pngwyy10.png

展开起码上万行。。

我们直接定位这俩post参数,既然放在一起,这俩肯定是同时出现,很容易就找到了这一段js代码
wyy1.png

看明白了

我们的目标由找 paramsencSecKey变成了找 bVj8b.encTextbVj8b.encSecKey
继续搜
wyy11.png

好的我们得到了bVj8b的初始化函数,找到函数

wyy12.png
把这一行代码拿出来

!function () {
    function a (a) {
        var d, e, b = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
            c = "";
        for (d = 0; a > d; d += 1)
            e = Math.random() * b.length, e = Math.floor(e), c += b.charAt(e);
        return c
    }
    function b (a, b) {
        var c = CryptoJS.enc.Utf8.parse(b), d = CryptoJS.enc.Utf8.parse("0102030405060708"), e = CryptoJS.enc.Utf8.parse(a), f = CryptoJS.AES.encrypt(e, c, { iv: d, mode: CryptoJS.mode.CBC });
        return f.toString()
    }
    function c (a, b, c) {
        var d, e;
        return setMaxDigits(131), d = new RSAKeyPair(b, "", c), e = encryptedString(d, a)
    }
    function d (d, e, f, g) {
        var h = {}, i = a(16);

        return h.encText = b(d, g), h.encText = b(h.encText, i), h.encSecKey = c(i, e, f), h
    }
    function e (a, b, d, e) {
        var f = {};
        return f.encText = c(a + e, b, d), f
    }
    window.asrsea = d, window.ecnonasr = e
    }();
    var bVj8b = window.asrsea(JSON.stringify(i4m), bsR5W(["流泪", "强"]), bsR5W(Xp0x.md), bsR5W(["爱心", 
    "女孩", "惊恐", "大笑"])
);

这下逻辑清晰了

表单就是由上面的d函数生成的,传入的参数分别是
(别问为啥是这个)
JSON.stringify(i4m)
bsR5W(["流泪", "强"])
bsR5W(Xp0x.md)
bsR5W(["爱心","女孩", "惊恐", "大笑"])

每个参数深入挖掘

第一个,首先我们很容易确定内部的i4m是一个json格式的变量,传入的时候把它变成了一个字符串而已
ok来经典做法,取断点调试,控制台直接打印这个i4m
wyy13.png
看到了 这下就好理解了,
看看这些都是啥,不难猜到
csrf_token 应该是个人信息,但是不登录也可以访问,所以空着没事
cursor 这是一个奇怪的东西,就是反爬用的,源码中是这样定义的

cursor: pageNo === 1 || !flag ? -1 : map[options.key][lastPage].cursor

offset 偏移量,就写0就行
orderType 不管写1
pageNo 页数,多少页写多少
pageSize 每页的数量
rid 'R_SO_4_'+歌曲id
threadId 与rid相同
源码中初始代码是这样的

options.data = {
    rid: data.rid,
    threadId: data.rid,
    pageNo: pageNo,
    pageSize: options.limit,
    cursor: pageNo === 1 || !flag ? -1 : map[options.key][lastPage].cursor,
    offset: !flag ? 0 : (Math.abs(lastPage - pageNo) - 1) * options.limit,
    orderType: pageNo === 1 || !flag || pageNo > lastPage ? 1 : 0
};

所以修改这些post的信息就能获取整个网易云的所有评论信息,是不是很刺激
但是,除了第一页外,其他页数除了要修改pageNo 还要修改cursor,这个东西吧。。是根据上一页post的信息得到的

剩下三个参数

都是用bsR5W函数加密的,把函数抽出来看,发现函数内部返回的值是由参数确定的
二四两个函数的参数都是写死的,那就不用管了,直接像上面一样调试获取值,
结果看了一会发现三个函数的参数也是写死的,内容如下(有点奇葩,但确实是这样)

Xp0x.md=["色","流感","这边","弱","嘴唇","亲","开心","呲牙","憨笑","猫","皱眉","幽灵","蛋糕","发怒","大哭","兔子","星星","钟情","牵手","公鸡","爱意","禁止","狗","亲亲","叉","礼物","晕","呆","生病","钻石","拜","怒","示爱","汗","小鸡","痛苦","撇嘴","惶恐","口罩","吐舌","心碎","生气","可爱","鬼脸","跳舞","男孩","奸笑","猪","圈","便便","外星","圣诞"]

最后我们调式得到其他三个死值,一级棒

wyy15.png

知道四个参数后,我们就可以搞事情了

参数的格式我们已经弄清楚
现在就差加密函数了,
重新写一下原加密js代码

!function () {
    function a (a) {
        var d, e, b = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
            c = "";
        for (d = 0; a > d; d += 1)
            e = Math.random() * b.length, e = Math.floor(e), c += b.charAt(e);
        return c
    }
    function b (a, b) {
        var c = CryptoJS.enc.Utf8.parse(b), d = CryptoJS.enc.Utf8.parse("0102030405060708"), e = CryptoJS.enc.Utf8.parse(a), f = CryptoJS.AES.encrypt(e, c, { iv: d, mode: CryptoJS.mode.CBC });
        return f.toString()
    }
    function c (a, b, c) {
        var d, e;
        return setMaxDigits(131), d = new RSAKeyPair(b, "", c), e = encryptedString(d, a)
    }
    function d (d, e, f, g) {
        var h = {}, i = a(16);

        return h.encText = b(d, g), h.encText = b(h.encText, i), h.encSecKey = c(i, e, f), h
    }
    function e (a, b, d, e) {
        var f = {};
        return f.encText = c(a + e, b, d), f
    }
    window.asrsea = d, window.ecnonasr = e
}();


var bVj8b = window.asrsea(JSON.stringify(i4m), bsR5W(["流泪", "强"]), bsR5W(Xp0x.md), bsR5W(["爱心", "女孩", "惊恐", "大笑"]));

现在就是考验代码功底了

将这段js代码用python复现
为方便,直接拆成get_encSecKey(),get_params()

i4m = {
    "csrf_token": "",
    "cursor": "-1",
    "offset": "0",  # 偏移量,前面每页评论数的总和pageSize
    "orderType": "1",
    "pageNo": "1",  # 页数
    "pageSize": "20",  # 页评论数
    "rid": "R_SO_4_{}",
    "threadId": "R_SO_4_{}"
}

bsR5W2 = "010001"
bsR5W3 = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"
bsR5W4 = "0CoJUm6Qyw8W8jud"

random_num = "d5bpgMn9byrHNtAh"  # 随机值,取啥无所谓,这里直接固定了
iv = "0102030405060708"  #函数内部调用的一个固定值

def get_encSecKey():    # 因为random_num固定了,这里返回固定值
    return "1b5c4ad466aabcfb713940efed0c99a1030bce2456462c73d8383c60e751b069c24f82e60386186d4413e9d7f7a9c7cf89fb06e40e52f28b84b8786b476738a12b81ac60a3ff70e00b085c886a6600c012b61dbf418af84eb0be5b735988addafbd7221903c44d027b2696f1cd50c49917e515398bcc6080233c71142d226ebb"

def get_params(data):  
    first = enc_params(data, bsR5W4)
    second = enc_params(first, random_num)
    return second  

def to_16(data):  # 转化成16的倍数, 位下方的加密算法服务
    pad = 16 - len(data) % 16
    data += chr(pad) * pad
    return data

def enc_params(data, key):  # 加密过程
    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的倍数
    return str(b64encode(bs), "utf-8")  # 转化成字符串返回,


resp = requests.post(BASE_URL, data={
    "params": get_params(json.dumps(i4m)), # 记得format两个songid
    "encSecKey": get_encSecKey()
})

# 代码有参考网络

最后就可以得到我们加密后的post表单(其实你会发现只用到了参数1和4,其他俩参数都是打酱油的)
修改参数1 json内的各个变量,我们就可以拿下网易云了(bushi,corsor问题还没解决
但是不重要了,本来我的目的只是评论数
最终我们通过任意歌曲id,便可以获取评论信息,评论数量了

wyy16.png

最后提一嘴

这定死的参数有够奇葩的,不知道网易程序猿经历了啥。。

调试方法有很多种,因为我们确定了xhr网络请求
所以我们直接用Chrome内置的功能,非常方便
wyy17.png

最后加一个展示界面

wyy18.png
完结撒花✿✿ヽ(°▽°)ノ✿

最后修改:2022 年 03 月 28 日
来过的证明就是打钱