網易雲爬蟲爬取用戶粉絲信息
0x01 前言
前不久聽說女神挺喜歡小狗小貓的,就給女神發了些小貓圖片和視頻,女神於是就給推薦了個網易雲的博主。

發現女神平常用網易雲聽歌,就突想知道平常女神都喜歡聽一些什么歌。之前有見女神分享自己網易雲圖片,隱約記得賬戶名中有‘chocolate’,就登上網易雲一通搜索,一個一個用戶信息挨着去看,也沒發現符合的;突然靈機一閃,女神不是也關注了之前給分享的那個“萌寵圖刊”的賬戶嗎,可以去他的粉絲里去找啊!說干就干,打開之后,emmm……將近四萬的粉絲,一個一個去翻要到猴年馬月了……直接寫個爬蟲爬取粉絲用戶名再搜索吧!
0x02 網站內容分析
打開網易雲網站,發現不需要登錄就能搜索查看用戶粉絲,發現簡單了不少,不用關心登錄了!
搜索“萌寵圖刊”然后點擊粉絲,F12,挨着一個一個查看響應內容,很快就找到了我們需要的內容:

然后雙擊打開,確實為我們需要的響應包:

查看其請求內容,提交了兩個參數,明顯為加密之后的:

先嘗試獲得第一頁的粉絲昵稱!
0x03 爬取單頁粉絲昵稱
1 import urllib.request 2 import urllib.parse 3 import json 4 5 headers = { 6 "Referer": "http://music.163.com", 7 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4350.7 Safari/537.36", 8 } 9 url = "https://music.163.com/weapi/user/getfolloweds?csrf_token=" 10 11 formdata = { 12 "params": "DxYWvntrh5oSYJ0o3vIBh/q/zJSFyiqQ1jAWx/B9tH5j6RdTabrnEP1WCj3wmx1l+WfGB0FQh3L3/lqcoozHZRXSMMqF+thJcWpR7Wwq53EGDa4XSiKhnBWSUdHBq9Io9H8WUmXBRVoa/3O4yG5gKg==", 13 "encSecKey": "bbee09bca20f8a7f73a9d6ecf4e6f1a008235e696fd2f1e756a05b94a6a936ff6ee50b0a8dda8b9cf3ac5d3a1fcc231afbc2585b7fbfc6c4751a14f989ddd44c5f76c9516921173c44c3d844064a07a49f615494501e227d57ead86a058d73bf99260be7e6cb92fea65e56174c2dd63e1a93322d655bc654f728bbab6eafac78" 14 } 15 16 temp_data = json.loads(urllib.request.urlopen(urllib.request.Request(url=url, data=urllib.parse.urlencode(formdata).encode("utf-8"),headers=headers)).read().decode("utf-8"))["followeds"] 17 18 for eve_data in temp_data: 19 nickname = eve_data["nickname"] 20 print(nickname)
運行結果正常

0x04 分析加密參數
由於獲取粉絲信息是由兩個請求參數獲得的,要對這兩個參數進行分析。
查看不同頁數兩個請求參數值內容不同,因此可以確定翻頁功能確實是由這兩個參數控制的!

這兩個參數很明顯是經過加密之后的,而這個請求又與一個js文件息息相關,因此判斷有可能是請求數據通過這個js加密之后再發出請求的,打開這個js文件格式化后進行分析,對兩個參數進行搜索。

通過分析發現最終所傳遞的參數應該就是從這個地方來的,想要知道他到底傳的參數是什么內容,我可以對這個js進行簡單的修改,即在這兩個語句之前加一個alert語句,讓 bZj0x 內的四個變量進行彈出:
1 alert("wyy" + JSON.stringify(i2x) + "||" + bkk0x(["流淚", "強"]) + "||" + bkk0x(YS7L.md) + "||" + bkk0x(["愛心", "女孩", "驚恐", "大笑"]))

然后利用 charles 軟件,將原本網站的js文件用修改過的文件進行替換,再次訪問粉絲頁面

訪問第一頁:

訪問第二頁:

分析彈窗的內容:
wyy{"userId":"30982607","offset":"0","total":"true","limit":"20","csrf_token":""}||010001||00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7||0CoJUm6Qyw8W8jud
wyy{"userId":"30982607","offset":"20","total":"false","limit":"20","csrf_token":""}||010001||00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7||0CoJUm6Qyw8W8jud
可以看出,四個參數用 “||” 隔開,且只有第一個參數有變化,其他三個參數為常量。
其中 “userId” 為該用戶的id,“offset” 發生了變化,且剛好對應每一頁粉絲個數為20.因此 “offset“ 用來控制翻頁。
接下來就是重新回到js文件中,查看加密的方法。
0x05 加密流程分析
在js文件中重新查找encText 和 encSecKey

可以看出,加密部分就是在這里。
1 function a(a) { 2 var d, e, b = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", c = ""; 3 for (d = 0; a > d; d += 1) e = Math.random() * b.length, e = Math.floor(e), c += b.charAt(e); 4 return c 5 } 6 7 function b(a, b) { 8 var c = CryptoJS.enc.Utf8.parse(b), d = CryptoJS.enc.Utf8.parse("0102030405060708"), 9 e = CryptoJS.enc.Utf8.parse(a), f = CryptoJS.AES.encrypt(e, c, {iv: d, mode: CryptoJS.mode.CBC}); 10 return f.toString() 11 } 12 13 function c(a, b, c) { 14 var d, e; 15 return setMaxDigits(131), d = new RSAKeyPair(b, "", c), e = encryptedString(d, a) 16 } 17 18 function d(d, e, f, g) { 19 var h = {}, i = a(16); 20 return h.encText = b(d, g), h.encText = b(h.encText, i), h.encSecKey = c(i, e, f), h 21 } 22 23 function e(a, b, d, e) { 24 var f = {}; 25 return f.encText = c(a + e, b, d), f 26 }
根據相同的加密流程,進行處理。
1 def get_params_first(userId, offset): 2 params_first = '{"userId":"' + userId + '","offset":"' + str(offset) + '","total":"false","limit":"100","csrf_token":"6f033a3138de5a06cea24807ba88e40b"}' 3 return params_first 4 #userid 是用戶id offset 是控制翻頁 5 #params_first = '{"userId":"30982607","offset":"20","total":"false","limit":"20","csrf_token":""}' 6 params_second = "010001" 7 params_thrid = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7" 8 params_forth = "0CoJUm6Qyw8W8jud" 9 10 #params 需要第一個參數和第四個參數 11 #encSecKey 需要第二個和第三個參數,還需要一個隨機的16個字符串 12 13 def aesEncrypt(text, key): 14 # 文本 15 pad = 16 - len(text) % 16 16 text = text + pad * chr(pad) 17 key = key.encode('utf-8') 18 encryptor = AES.new(key, 2, b'0102030405060708') 19 ciphertext = encryptor.encrypt(text.encode('utf-8')) 20 ciphertext = base64.b64encode(ciphertext) 21 return ciphertext 22 23 def get_params(text, userId, offset): 24 # 第一個參數 25 params_first = get_params_first(userId, offset) 26 params = aesEncrypt(params_first, params_forth).decode('utf-8') 27 params = aesEncrypt(params, text) 28 return params 29 30 def rsaEncrypt(pubKey, text, modulus): 31 #進行rsa加密 32 text = text[::-1] 33 rs = int(codecs.encode(text.encode('utf-8'), 'hex_codec'), 16) ** int(pubKey, 16) % int(modulus, 16) 34 return format(rs, 'x').zfill(256) 35 36 def get_encSecKey(text): 37 pubKey = params_second 38 moudulus = params_thrid 39 encSecKey = rsaEncrypt(pubKey, text, moudulus) 40 return encSecKey
至此,加密部分已經完成,可以通過控制“offset”的數值來控制翻頁。
0x06 最終完整代碼
1 import urllib.request 2 import urllib.parse 3 import json 4 import base64 5 from Crypto.Cipher import AES 6 import codecs 7 8 headers = { 9 "Referer": "http://music.163.com", 10 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4350.7 Safari/537.36", 11 } 12 url = "https://music.163.com/weapi/user/getfolloweds?csrf_token=" 13 14 def get_params_first(userId, offset): 15 params_first = '{"userId":"' + userId + '","offset":"' + str(offset) + '","total":"false","limit":"20","csrf_token":"6f033a3138de5a06cea24807ba88e40b"}' 16 return params_first 17 #userid 是視頻的標志 offset 是控制翻頁 18 #params_first = '{"userId":"30982607","offset":"20","total":"false","limit":"20","csrf_token":""}' 19 params_second = "010001" 20 params_thrid = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7" 21 params_forth = "0CoJUm6Qyw8W8jud" 22 23 #params 需要第一個參數和第四個參數 24 #encSecKey 需要第二個和第三個參數,還需要一個隨機的16個字符串 25 26 def aesEncrypt(text, key): 27 # 文本 28 pad = 16 - len(text) % 16 29 text = text + pad * chr(pad) 30 key = key.encode('utf-8') 31 encryptor = AES.new(key, 2, b'0102030405060708') 32 ciphertext = encryptor.encrypt(text.encode('utf-8')) 33 ciphertext = base64.b64encode(ciphertext) 34 return ciphertext 35 36 def get_params(text, userId, offset): 37 # 第一個參數 38 params_first = get_params_first(userId, offset) 39 params = aesEncrypt(params_first, params_forth).decode('utf-8') 40 params = aesEncrypt(params, text) 41 return params 42 43 def rsaEncrypt(pubKey, text, modulus): 44 #進行rsa加密 45 text = text[::-1] 46 rs = int(codecs.encode(text.encode('utf-8'), 'hex_codec'), 16) ** int(pubKey, 16) % int(modulus, 16) 47 return format(rs, 'x').zfill(256) 48 49 def get_encSecKey(text): 50 pubKey = params_second 51 moudulus = params_thrid 52 encSecKey = rsaEncrypt(pubKey, text, moudulus) 53 return encSecKey 54 55 userId = "30982607" 56 offset = 0 57 path = "./" + userId + ".txt" 58 while True: 59 text = "A"* 16 60 params = get_params(text, userId, offset) 61 encSecKey = get_encSecKey(text) 62 formdata = { 63 "params": params, 64 "encSecKey": encSecKey 65 } 66 temp_data = json.loads(urllib.request.urlopen(urllib.request.Request(url=url, data=urllib.parse.urlencode(formdata).encode("utf-8"), headers=headers)).read().decode("utf-8"))["followeds"] 67 length = len(temp_data) 68 69 for eve_data in temp_data: 70 nickname = eve_data["nickname"] 71 print(nickname) 72 with open(path, "a") as file: 73 file.write(nickname + "\n") 74 if offset < 38168: 75 offset = offset + 20 76 #print(offset) 77 else: 78 break
0x07 杯具
運行了半天才發現,只能爬到最近關注的1000個粉絲。。。同樣,網站上也是只能查看50頁。。。也就是“offset“的值最大為1000
不死心的我,再次看了下第一個參數,發現還有一個“limit“字段,於是嘗試改變limit值,發現其控制的是一頁顯示粉絲的數量,但是經過測試,其上限為100
改變“limit”值再次運行,發現一次能爬取100個粉絲昵稱,速度大大提高,但也只能夠爬取到1100個粉絲昵稱。
既然網站只能查看50頁的粉絲,pc客戶端呢?

看到638頁,感覺有戲!直接點擊最后一頁,然后,emmmm。。。

再去看手機app,還好,並沒有被限制。。。
只能去一點點手動翻了,只能慶幸粉絲只有3w多了。。。
