# 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 } """ ```