网易云爬虫爬取用户粉丝信息
网易云爬虫爬取用户粉丝信息
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多了。。。