# Python 爬蟲 - 網易雲音樂評論
[TOC]
## 思路
1. 想要爬取評論內容,但發現**網頁原始碼**、**框架原始碼**中皆沒有想要的數據。
2. **`F12`** 選到 `XHR` ---> 找到評論是位於哪個資料內。
>
>
3. 進到 `Headers` 查看 :
>
>
> 得到 **`URL`** 以及請求方式是 **`POST`**。
>
> **`Form Data`** 中有 **`params`**、**`encSecKey`** 兩個參數,可以發現他們明顯是被加密過的。
4. 所以我們要**找到原本未加密過的參數**以及**網易雲加密的方式**。
## 如何找未加密的參數
1. 點選 **`Initiator`** 查看。
>
>
> `Request call stack` 表示發送請求到當前位置的時候,一共經共哪些 `JS` 腳本執行的過程,而最開始執行的放在最底下。
2. 點擊最上面的腳本開始檢查功能。
>
>
> * 先設置斷點 ---> **`F5`** 看右邊的參數。
> * 目前的是 `url = "cdns?csrf_token=`,不是我們需要的,所以點擊  進行下一次攔截。
> * 不斷攔截直到 `url = "comments/get?csrf_token="`。
3. 觀察找到的參數。
> 
>
> 因為此時 `data` 已經被加密過,所以我們往回找,看他之前是否已經被加密。
4. 透過 **`Call Stack`** 往回繼續尋找。
>
>
> 其實 `Call Stack` 的內容跟剛剛的 `Initaitor` 的內容是一樣的。
---
> 
>
> 到這步 `data` 還是被加密過的,所以我們必須一直往回找到未加密的參數。
---
>
>
> 找到未加密的參數 !!!!
5. 向上找一個看看
> 
>
> * 由此可以確定一件事
> ---> 參數是透過 **`u0x.be1x`** 進行加密的。
## 如何找到加密方式
1. 剛剛已經知道參數是透過 **`u0x.be1x`** 進行加密的,所以我們先研究裡面做了什麼。
> 
>
> 1. 從開頭設置一個斷點 -> **`F5`**。
> 2. 攔截找到需要的 `url`。
> 3. 透過  一行一行執行,觀察 `data` 是在執行哪行程式碼時進行加密的。
2. 找到從哪裡加密
> 
>
> * 執行完 **`window.asrsea()`** 出現了 `bUE3x`,裡面存了加密後的數值,可以知道這裡應該就是加密的函數了。
> * 而 `i0x` 作為參數丟入裡面,可以推測他就是原始的參數。
> * 最後會將 `e0x` 重新 `bUE3x` 根據賦值,就會得到我們看到的加密數值。
> 這裡也可以知道 **`params = encText`**、**`encSecKey = encSecKey`**。
3. 搜尋 **`window.asrsea()`**
>
>
> * **`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"
>> ```
>>
>> 
>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" # 手動固定,原本應該是隨機的
>> ```
>>
>>
>>
```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
}
"""
```