爬取网易云评论

0x00 查看所要数据是否在网页源代码

例:选取歌曲马思唯《不怪她》,进入有该歌曲评论的页面。

Ctrl+U查看网页源代码,复制一条评论:不怪她 怪我太爱吃豆瓣酱

在网页源代码中Ctrl+F搜索此条评论发现:评论不存在于网页源代码中。

0x01 查看XHR中是否有所要数据

XMLHttpRequestXHR)对象用于与服务器交互。通过XMLHttpRequest可以在不刷新页面的情况下请求特定的URL,获取数据。这允许网页在不影响用户操作的情况下,更新页面的局部内容。XMLHttpRequestAJAX编程中被大量使用。

尽管名称如此,XMLHttpRequest可以用于获取任何类型的数据,而不仅仅是XML它甚至支持。HTTP以外的协议(包括文件://和FTP),尽管可能受到更多出于安全等原因的限制。

如果您的通信流程需要从服务器端接收事件或消息数据,请考虑通过EventSource接口使用服务器发送的事件。对于全双工的通信,WebSocket可能是更好的选择。

  1. 在页面中F12/Ctrl+Shift+I进入开发者工具调试页面
  2. Network
  3. XHR
  4. Ctrl+R刷新
  5. get?csrf_token=中找到评论信息
  • 记录需要请求的url:”https://music.163.com/weapi/comment/resource/comments/get?csrf_token=”
  • 请求方式为POST

0x03 寻找网易云数据加密方法的过程

  • 找到未加密的参数
  • 想办法把参数进行加密(必须参考网易的逻辑),params,ensSecKey
  • 请求到网易,拿到评论信息
get?csrf_token= 中找到网易云加密逻辑

在Initiator中找到Request call stack

Requests call stack中的请求是自下而上逐个发送的。进入第一行即最后一个请求的地址:

不要点击页面中代码,点击大括号{}切换代码显示格式:

切换后显示如下,在send函数处设置断点,刷新页面,观察右侧:

F8放开请求直到此处为get?csrf_token=

观察e8e中的request的data中存在params表示此处的请求被加密:

逐个往回查看请求中的参数,发现到此处的(anoymous)时,request中的data为未加密的:data: "rid=R_SO_4_1462875618&threadId=R_SO_4_1462875618&pageNo=1&pageSize=20&cursor=-1&offset=0&orderType=1"

但t8l.be8W中的data为加密的:data: "params=sFxBk2Z6Y%2BTq3Gje92YzL4Scro9s4WF4BVXSdAJrzfJmc3HRArb6DWX0XNfdulAGKUTelfF9DQ7nlOjHMxknan4PDoP1VOkt3gB3tMZccgaKWVPiu9xHOBE0aY%2B50%2FRtvs5KcoLEMMa8ozNV5swlUzUg6VXZ2PvYOuvAiEd3BvPY%2BSd%2FqVKWwLnTN4f4vp6KbEdjJb74S6AI8H6wLDoQzrYG%2B%2B%2FDwm7lU%2BKyO%2BRFZrRlO9YKfOzoe9QpOEZEGA606MaRb8yPxW%2BGUdy02VI%2B%2BSuq5w173i6TrxEgwm0r6kI%3D&encSecKey=b63324d2d3fd967f3b060670d968c68ad71897f0e1d3b9f8a6c3d9824307c9be67619bba00ca60ec0384d5865732047f71099b572c1214da786aa1f3f434bf1508673497f1a4d960f23e94a0242becf27ccf9a91945f5128497eedb76b7455001f2e0feac78306cc6d1a2a51e3255a6d94ceee0695c97721dc91cae61f95ebe6"

可知:数据在倒数第四个请求中被加密。且在代码中可以发现实现加密的代码段为:

在这个函数的入口设置一个断点并在var bWv6p = window.asrsea()处设置一个断点后发现:

真正的参数是i8a。F10向下执行一步在执行window.asrsea后,Local中多了bMv6p:

继续向下观察代码,发现bWv6p的encText和encSecKey被赋值给了e8e.data:

最后总结出:网易云的加密策略是:

var bWv6p = window.asrsea(JSON.stringify(i8a), bsK3x(["流泪", "强"]), bsK3x(XR5W.md), bsK3x(["爱心", "女孩", "惊恐", "大笑"]));
            e8e.data = j8b.cr9i({
                params: bWv6p.encText,
                encSecKey: bWv6p.encSecKey
            })

真实的参数是:

csrf_token: ""
cursor: "-1"
offset: "0"
orderType: "1"
pageNo: "1"
pageSize: "20"
rid: "R_SO_4_1462875618"
threadId: "R_SO_4_1462875618"

0x04 破解网易云数据加密的过程

加密的参数:

  • params => encText
  • encSecKey => encSecKey

加密过程:

function a(a) {
    var d, e, b = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", c = "";
    for (d = 0; a > d; d += 1) # 循环16次
        e = Math.random() * b.length, # 随机数 ep:1.2345
        e = Math.floor(e), # 取整 ep:1
        c += b.charAt(e); # 取字符串中的xxx位置 ep:取b 会取得16个字母或数字
    return c
}
function b(a, b) { # a是要加密的内容
    var c = CryptoJS.enc.Utf8.parse(b) # c的数据是b传来的 b就是密钥
      , d = CryptoJS.enc.Utf8.parse("0102030405060708")
      , e = CryptoJS.enc.Utf8.parse(a) # e是数据
      , f = CryptoJS.AES.encrypt(e, c, { # AEC加密方法:c是加密的密钥
        iv: d, # AES加密中的iv是偏移量
        mode: CryptoJS.mode.CBC # mode: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), # b的数据是g传过来的 g就是密钥
    h.encText = b(h.encText, i), # 返回的就是params i也是密钥 d是变化的数据
    h.encSecKey = c(i, e, f), # 得到的就是encSecKey e和f是定值 c函数的随机只与i有关,如果此时把i固定就可以确定c返回的结果
    h
}
function e(a, b, d, e) {
    var f = {};
    return f.encText = c(a + e, b, d),
    f
}
    window.asrsea = d,  # 入口是d
    window.ecnonasr = e
    
# 两次加密:
数据+g -> b加密 -> 第一次加密+i -> b加密 -> params

加密过程中只有i是随机的,且c函数中不产生随机数,所以除了d(数据)之外唯一的变量就是i的值。

若要获取此首歌的评论,只需提前抓取此首歌中i的值,并在程序中将i设置为常量。

通过在d函数中的h.encText = b(h.encText, i)处设置断点,放开请求,获取到此首歌中i的值为i: "LVpsmYgr1qUy4XoO"

得到了参数i,则服务于d()函数的参数全部获取完毕,下一步处理加密过程:

报错:TypeError: Object type class ‘str’ cannot be passed to C code

解决:明文、密钥、IV都要编码。并且根据AES加密算法的加密规则:加密的内容必须是16的倍数,所以定义将内容转化为16的倍数的方法:

    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的倍数
def to_16(data):
    pad = 16 - len(data) % 16
    data += chr(pad) * pad
    return data

0x05 程序源代码

from Crypto.Cipher import AES
from base64 import b64encode
import requests
import json

url = "https://music.163.com/weapi/comment/resource/comments/get?csrf_token="


# 服务于d的参数
f = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"
g = "0CoJUm6Qyw8W8jud"
e = "010001"
i = "LVpsmYgr1qUy4XoO"  # 手动固定(在网易中是随机的)

data = {
    "csrf_token": "",
    "cursor": "-1",
    "offset": "0",
    "orderType": "1",
    "pageNo": "1",
    "pageSize": "20",
    "rid": "R_SO_4_1462875618",
    "threadId": "R_SO_4_1462875618"
}


def get_encSecKey(): # 由于i是固定的 所以encSecKey是固定的 c()函数的结果就是固定的
    return "9f6b28c353754e75b7dcc4f93d17d28ba9c7bde876182bbfcab4f47b4c57fbc682fbdb7959139f27978171b4f0f731f6c4d1f3200988d4bbcbff204dda7d4171d6c08f864f26e25bb3f030d3e8bd3cc19821e9a699cc5c268b7eb3da4cf1a67589f132dc98f26ac6c269d1a7cbf101d8f4dc16db1ee91429cbf3f795f4f049d4"


# 把参数进行加密
def get_params(data):  # 默认这里接受的是字符串
    first = enc_params(data, g)
    second = enc_params(first, i)
    return second  # 返回的是params

# 转化成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的倍数
    return str(b64encode(bs), "utf-8")  # 转化成字符串返回

# 发送请求 得到评论结果
resp = requests.post(url, data={
    "params": get_params(json.dumps(data)),  # 此处需把data数据从字符串转化为json类型
    "encSecKey": get_encSecKey()
})
print(resp.text)
标签: