---
# System prepended metadata

title: Python 爬蟲 - 網易雲音樂評論

---

# Python 爬蟲 - 網易雲音樂評論

[TOC]

## 思路
1. 想要爬取評論內容，但發現**網頁原始碼**、**框架原始碼**中皆沒有想要的數據。
2. **`F12`** 選到 `XHR` ---> 找到評論是位於哪個資料內。
>![](https://i.imgur.com/ihmNSjB.png)
>
3. 進到 `Headers` 查看 : 
>![](https://i.imgur.com/vZmUyMZ.png)
>
> 得到 **`URL`** 以及請求方式是 **`POST`**。
 
>![](https://i.imgur.com/bLn0ofa.png)
> **`Form Data`** 中有 **`params`**、**`encSecKey`** 兩個參數，可以發現他們明顯是被加密過的。

4. 所以我們要**找到原本未加密過的參數**以及**網易雲加密的方式**。


## 如何找未加密的參數
1. 點選 **`Initiator`** 查看。

>![](https://i.imgur.com/5YAUH53.png)
>
> `Request call stack` 表示發送請求到當前位置的時候，一共經共哪些 `JS` 腳本執行的過程，而最開始執行的放在最底下。

2. 點擊最上面的腳本開始檢查功能。
>![](https://i.imgur.com/dHQEsAx.png)
>
> * 先設置斷點 ---> **`F5`** 看右邊的參數。
> * 目前的是 `url = "cdns?csrf_token=`，不是我們需要的，所以點擊 ![](https://i.imgur.com/XM7YGCF.png) 進行下一次攔截。
> * 不斷攔截直到 `url = "comments/get?csrf_token="`。

3. 觀察找到的參數。
> ![](https://i.imgur.com/EPrAkAI.png)
> 
> 因為此時 `data` 已經被加密過，所以我們往回找，看他之前是否已經被加密。

4. 透過 **`Call Stack`** 往回繼續尋找。
>![](https://i.imgur.com/XGLxIbh.png)
>
> 其實 `Call Stack` 的內容跟剛剛的 `Initaitor` 的內容是一樣的。
---
> ![](https://i.imgur.com/kHZhxwB.png)
>
> 到這步 `data` 還是被加密過的，所以我們必須一直往回找到未加密的參數。
---
>![](https://i.imgur.com/ZsnnDLK.png)
>
> 找到未加密的參數 !!!!

5. 向上找一個看看
> ![](https://i.imgur.com/tsyJ9eW.png)
>
> * 由此可以確定一件事
> ---> 參數是透過 **`u0x.be1x`** 進行加密的。


## 如何找到加密方式
1. 剛剛已經知道參數是透過 **`u0x.be1x`** 進行加密的，所以我們先研究裡面做了什麼。
> ![](https://i.imgur.com/Ruy3ZGH.png)
>
> 1. 從開頭設置一個斷點 -> **`F5`**。
> 2. 攔截找到需要的 `url`。
> 3. 透過 ![](https://i.imgur.com/KKWvEYh.png) 一行一行執行，觀察 `data` 是在執行哪行程式碼時進行加密的。

2. 找到從哪裡加密
> ![](https://i.imgur.com/Fn3l9pP.png)
>
> * 執行完 **`window.asrsea()`** 出現了 `bUE3x`，裡面存了加密後的數值，可以知道這裡應該就是加密的函數了。
> * 而 `i0x` 作為參數丟入裡面，可以推測他就是原始的參數。
> * 最後會將 `e0x` 重新 `bUE3x` 根據賦值，就會得到我們看到的加密數值。
> 這裡也可以知道 **`params = encText`**、**`encSecKey = encSecKey`**。

3. 搜尋 **`window.asrsea()`**
>![](https://i.imgur.com/0zXuiIX.png)
>
> * **`window.asrsea()`** 被賦值 **`d`**，所以我們只需要看 `function d` 即可。



## 觀察加密方式 
* function d( )
>1. 首先觀察 function 傳入的每個數值代表甚麼。
>> * `d = JSON.stringify(i0x)` 表示 `d` 是個**要被加密的數據**。
>> * `e、f、g` 分別都是 **`bsl6f`** 這個函數，那我們要怎麼知道他們代表什麼呢 ?
>> ---> 最簡單的方式就是複製函數，丟到 **`Console`** 裡面執行就可以了。
>> ---> 要注意的點是，應該要多輸入幾次，用來確認結果存不存在隨機性。
>> ---> 最後我們發現三個變數**皆為定值**。
>> ```python=
>> # e = "010001"
>> # f = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"
>> # g = "0CoJUm6Qyw8W8jud"
>> ```
>>
>> ![](https://i.imgur.com/2ugIMQi.png)
>2. 接下來是 **`i = a(16)`**，他是一串 16 位的隨機數。
>3. 再來觀察比較簡單的 **`h.encSecKey = c(i, e, f)`**。
>>* `e、f` 透過剛剛已經知道是個**定值**，再看到 **`c`**，可以發現其中只有用到了 `RSA加密`，並不存在隨機性。
>>* 那麼為了讓程式看起來比較簡單，如果我們讓 `i` 也成為一個**定值**，就表示 `c` 也一定是個定值了。
>>:::warning
>>前提是 `c` 不存在隨機性，否則也是需要另外做處理。
>>:::
>4. 最後觀察 **`h.encText = b(d, g), h.encText = b(h.encText, i)`**。
>>* 總共進行兩次 `AES` 加密 -> **`b(要加密的數據, 密鑰)`**。
>> ---> 第一次 : **`b(未加密參數, 密鑰g)`**。
>> ---> 第二次 : **`b(第一次加密的參數, 密鑰i)`**。
 ```python=
var bUE3x = window.asrsea(JSON.stringify(i0x), 
                            bsl6f(["流泪", "强"]),
                            bsl6f(WT9K.md), 
                            bsl6f(["爱心", "女孩", "惊恐", "大笑"]));

""""
    function d(d, e, f, g) {    d = 數據, e=定值, f=定值, g=>定值
        var h = {}
          , i = a(16);
        return h.encText = b(d, g),     # AES加密兩次
        h.encText = b(h.encText, i),    # i, g = 密鑰
        h.encSecKey = c(i, e, f),  # 因為c裡面不存在隨機性，e、f又是定值
                                   # 所以如果i也能固定，c也是個定值。
        h
    }
"""
```

---

* function c( )
> 1. 進行 `RSA` 加密。
> 2. 透過觀察，`b、c` 已經為定值，若 `a ( i )` 也能變成定值，那麼回傳的 `encSecKey` 也就是定值了。
```python=
"""
    function c(a, b, c) {
        var d, e;
        return setMaxDigits(131),
        d = new RSAKeyPair(b,"",c),
        e = encryptedString(d, a)
    }
"""
```

---

* function a( )
> 1. 回傳一個 16 位的字串。
> 2. 如何找到**定值** `i` ? 
>> * 設置一個斷點，再觀察 `i` 的數值，作為定值。
>> ```python=
>> # i = "rEIzAGHKdWrSrg2B" # 手動固定，原本應該是隨機的
>> ```
>> 
>>![](https://i.imgur.com/IS7KVyA.png)
>>
```python=
"""
    # 建立 16 位的隨機字串
    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( )
> 1. 進行 `AES加密`，模式為 `CBC`。
>> ---> **`b(要加密的數據, 密鑰, 偏移量, mode=CBC)`**。
> :::warning
> `AES加密` 有一個很重要的規定 ---> 加密內容長度必須符合 **16 的倍數**。
> * 如果不符合 : 
>> data = "0123456789",
>> data' = "0123456789chr(6)chr(6)chr(6)chr(6)chr(6)chr(6)"。
> * 剛剛好等於 16 的倍數也不行 : 
>> data = "0000000000000000",
>> data' = "0000000000000000chr(16)chr(16)chr(16)chr(16)chr(16)chr(16)chr(16)chr(16)chr(16)chr(16)chr(16)chr(16)chr(16)chr(16)chr(16)chr(16)"
> * 意思就是要補足 **`chr(16 - len(data) % 16)`**。
> :::
```python=
"""
    function b(a, b) {  # a = 要加密的數據, b = 密鑰
        var c = CryptoJS.enc.Utf8.parse(b)  # c = 密鑰
          , d = CryptoJS.enc.Utf8.parse("0102030405060708")
          , e = CryptoJS.enc.Utf8.parse(a)  # e = 數據
          , f = CryptoJS.AES.encrypt(e, c, {
            iv: d,  # d = 偏移量
            mode: CryptoJS.mode.CBC     # 模式 = CBC
        });
        return f.toString()
    }
"""
```


## 實作
:::success
概念 : 
1. 找到未加密的參數。
2. 把參數進行加密 ( 必須參考網易雲的邏輯 )。
---> **`params (encText), encSecKey (encSecKey)`**。
3. 請求到網易雲音樂，拿到評論信息。
:::

* 先將已知的數據記錄起來，包括 **`url`**、**`未加密的參數`**、**`加密參數`**。
```python=10
url = "https://music.163.com/weapi/comment/resource/comments/get?csrf_token="

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

# 加密參數
e = "010001"
f = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"
g = "0CoJUm6Qyw8W8jud"
i = "rEIzAGHKdWrSrg2B"  # 手動固定，原本應該是隨機的
```

---

* 處理 encSecKey。
```python=31
# 由於 i 是固定的，那encSecKey也是固定的
def get_encSecKey():
    return "dc1ca7a066ebeeb17df734aaa027f84415e102166e803cdb82feda666af6444e399ea8ead9477a97df210e6116e8931cec7c9a6914c98d703a0150e8ea3652b50434812352f8009841421f661c25a86648ffc5e4cc089e5b039cdb090191940ae26d38efa09358637bde5cd203ff4a15b6a753c48c33fe4e825fd2dd5d8301af"
```

---

* 處理 encText。
> 1. 總共進行兩次 `AES加密`。
>```python=37
># 將參數進行加密
>def get_params(data):   # 默認接收到字符串 (dic無法透過這樣加密)
>    first = encryp_params(data, g)
>    second = encryp_params(first, i)
>    return second
>```
>
> 2. `AES加密`。
> * 進行 `AES加密` 需要 **`from Crypto.Cipher import AES`**。
>```python=6
>from Crypto.Cipher import AES
>from base64 import b64encode
>```
>
>* `data` 需先進行 **`to_16`** 轉換成 16 的倍數。
>* 建立一個加密器，並將參數丟入。
>> `iv (偏移量) = "0102030405060708"` ( 透過 `function b` 得知 )。
>> `key = key`。
>> `mode = CBC`。
>:::warning
> 必須將資料都進行 `utf-8` 編碼，因為只有 **`字節(byte)`** 才能進行編碼，
> 而 **`字串(string)`** 不能。
>:::
>
>* 將 `data` 丟入進行加密。
>* 最後將回傳的資料轉換為 `string`，但是回傳的 `encrypted_data` 無法被 `.decode("utf-8")` 識別，所以需要使用 **`b64`** 進行編碼再轉換成 `string`。
>
>```python=43
># 加密過程
>def encryp_params(data, key):
>    data = to_16(data)
>    iv = "0102030405060708"
>    aes = AES.new(key=key.encode("utf-8"), IV=iv.encode("utf-8"), mode=AES.MODE_CBC) # 創建加密器, key需要字節，而i、g皆為字符串
>    encrypted_data = aes.encrypt(data.encode("utf-8"))   # 加密，AES加密規定內容長度必須是16的倍數 
>    return str(b64encode(encrypted_data), "utf-8") # 無法直接 .encode("utf-8")，因為無法被識別
>
># 轉換成16的倍數，為了符合AES的規定
>def to_16(data):
>    pad = 16 - len(data) % 16
>    data += chr(pad) * pad
>    return data
>```
>
```python=37
# 將參數進行加密
def get_params(data):   # 默認接收到字符串 (dic無法透過這樣加密)
    first = encryp_params(data, g)
    second = encryp_params(first, i)
    return second

# 加密過程
def encryp_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) # 創建加密器, key需要字節，而i、g皆為字符串
    encrypted_data = aes.encrypt(data.encode("utf-8"))   # 加密，AES加密規定內容長度必須是16的倍數 
    return str(b64encode(encrypted_data), "utf-8") # 無法直接 .encode("utf-8")，因為無法被識別

# 轉換成16的倍數，為了符合AES的規定
def to_16(data):
    pad = 16 - len(data) % 16
    data += chr(pad) * pad
    return data
```

---

* 將 `data` 一起發送請求到網易雲。
:::warning
`data` 原本是屬於 `dic`，不能被進行加密。
所以使用 **`json.dumps()`** 將資料轉換成 `json格式 (str)`。
:::
```python=59
# 發送請求，得到評論結果
if __name__ == '__main__':
    resp = requests.post(url, data={
        "params": get_params(json.dumps(unencrypted_data)),
        "encSecKey": get_encSecKey()
        # "params": "/DLSPjIHdFMkBbabm+lUh8nLOkNK1J06zvtmCTmD/2GPjZm7BQW8bK0Zta6eNVuHxw8iU6k2Pkv6g4/G0oovPIsA0ddA3aOa8wWtszegx7iWpN90d4GHmaa0iBh60BEFRn/w8dPd4u9kh5IF1CqYtSPyNcB3VLs7nsGv5/Xydmrp8Izh0yWgOYMLwyD03HrrYkdy1cgBoL/8Omk5N3Gz6GFOIYKVdSBdnjY6wGMXPf1s8I5UOvvf6zNiOBAC4wMnhfYfUkZkem1rvcdHYHN9No9TENkWvJ7U9WOLtinSIdw=",
        # "encSecKey": "914ef3efab9e8244029eec5749578e284c882fc7a85a61eeaaa38a83b72ac7c13d22a0b9666cb7d840cc6a3f48a7b533c3fd47ad2b31869ee9aa14cee20107b4abf1cf0948285ca459ab371c2e9ceeda99ea2092a789e3a745b77ceab1e9b8a7e00c3827a7fc964dc7a31df104005e2dea78bdb68bb4f50c74e458ac3955dd18"
    })
    
    print(resp.text)
    resp.close()
```


## 程式碼
```python=
# 1. 找到未加密的參數
# 2. 把參數進行加密(必須參考網易的邏輯), params (encText), encSecKey (encSecKey)
# 3. 請求到網易雲音樂，拿到評論信息

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

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

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

# 加密參數
e = "010001"
f = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"
g = "0CoJUm6Qyw8W8jud"
i = "rEIzAGHKdWrSrg2B"  # 手動固定，原本應該是隨機的



# 由於 i 是固定的，那encSecKey也是固定的
def get_encSecKey():
    return "dc1ca7a066ebeeb17df734aaa027f84415e102166e803cdb82feda666af6444e399ea8ead9477a97df210e6116e8931cec7c9a6914c98d703a0150e8ea3652b50434812352f8009841421f661c25a86648ffc5e4cc089e5b039cdb090191940ae26d38efa09358637bde5cd203ff4a15b6a753c48c33fe4e825fd2dd5d8301af"



# 將參數進行加密
def get_params(data):   # 默認接收到字符串 (dic無法透過這樣加密)
    first = encryp_params(data, g)
    second = encryp_params(first, i)
    return second

# 加密過程
def encryp_params(data, key):
    data = to_16(data)
    iv = "0102030405060708"
    aes = AES.new(key=key.encode("utf-8"), IV=iv.encode("utf-8"), mode=AES.MODE_CBC) # 創建加密器, key需要字節，而i、g皆為字符串
    encrypted_data = aes.encrypt(data.encode("utf-8"))   # 加密，AES加密規定內容長度必須是16的倍數 
    return str(b64encode(encrypted_data), "utf-8") # 無法直接 .encode("utf-8")，因為無法被識別

# 轉換成16的倍數，為了符合AES的規定
def to_16(data):
    pad = 16 - len(data) % 16
    data += chr(pad) * pad
    return data



# 發送請求，得到評論結果
if __name__ == '__main__':
    resp = requests.post(url, data={
        "params": get_params(json.dumps(unencrypted_data)),
        "encSecKey": get_encSecKey()
        # "params": "/DLSPjIHdFMkBbabm+lUh8nLOkNK1J06zvtmCTmD/2GPjZm7BQW8bK0Zta6eNVuHxw8iU6k2Pkv6g4/G0oovPIsA0ddA3aOa8wWtszegx7iWpN90d4GHmaa0iBh60BEFRn/w8dPd4u9kh5IF1CqYtSPyNcB3VLs7nsGv5/Xydmrp8Izh0yWgOYMLwyD03HrrYkdy1cgBoL/8Omk5N3Gz6GFOIYKVdSBdnjY6wGMXPf1s8I5UOvvf6zNiOBAC4wMnhfYfUkZkem1rvcdHYHN9No9TENkWvJ7U9WOLtinSIdw=",
        # "encSecKey": "914ef3efab9e8244029eec5749578e284c882fc7a85a61eeaaa38a83b72ac7c13d22a0b9666cb7d840cc6a3f48a7b533c3fd47ad2b31869ee9aa14cee20107b4abf1cf0948285ca459ab371c2e9ceeda99ea2092a789e3a745b77ceab1e9b8a7e00c3827a7fc964dc7a31df104005e2dea78bdb68bb4f50c74e458ac3955dd18"
    })
    
    print(resp.text)
    resp.close()


# 加密過程  
"""
    # 建立 16 位的隨機字串
    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) {  # a = 要加密的數據, b = 密鑰
        var c = CryptoJS.enc.Utf8.parse(b)  # c = 密鑰
          , d = CryptoJS.enc.Utf8.parse("0102030405060708")
          , e = CryptoJS.enc.Utf8.parse(a)  # e = 數據
          , f = CryptoJS.AES.encrypt(e, c, {
            iv: d,  # d = 偏移量
            mode: CryptoJS.mode.CBC     # 模式 = 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) {    d = 數據, e=定值, f=定值, g=定值
        var h = {}
          , i = a(16);
        return h.encText = b(d, g),     # AES加密兩次
        h.encText = b(h.encText, i),    # i, g = 密鑰
        h.encSecKey = c(i, e, f),   # 因為c裡面不存在隨機性，e、f又是定值，
                                    # 所以如果i也能固定，c也是個定值。
        h
    }
"""
```