爬网易云音乐实现搜索,用了一个星期,头发都掉了一大撮。
主要是实现了通过程序在网易云音乐搜索某首歌曲然后下载的功能,可以批量下载。
这是个记录贴,很多废话,是解决问题,分析错误的记录。看代码的请直接移步文末!!!
最近我哥给了个歌单,走的是怀旧路线,2003年排行榜前100的歌,让我帮他下载这些歌。
作为一个程序员,当然是要爬虫,手动多low啊!!!
网上有很多爬网易云网络歌单或者在线排行榜的,然而本地歌单的话需要搜索歌名,但是这方面的博客资料基本找不到。
摸索吧,照抓翻页评论那样来找请求传输的两个参数吧。
找 params 和 encSecKey 两个参数用了2天半。。。尝试 fiddler 用了一天多, 尝试 charles 用了大半天。。。缓存也清过,证书代理也设置过,浏览器也测试了360、IE、chrome、Firefox, 系统也尝试了Ubuntu 18.04,win10……就是抓不到 js ,替换本地 js 也不管用,心态真的爆炸!!!boomboomboom !!!
遇到问题:
1. 直接用get取链接,获取不到任何内容
2. 按照那些爬评论的所说想办法得到 params 和 encKey。就要找到4个参数。浏览器的F12能找到js文件,但是使用 fiddler 和 charles 抓取不到js文件。各种设置都徒劳,使用 AutoResponder 和 map local 都不能实现加载本地修改过的js文件。
3. 找到4个参数后,用写好的加密代码报错:
ValueError: Input strings must be a multiple of 16 in length
找到原因:加密的字符串里面不能有中文?中文才报错。可是我的歌名是中文的啊!what fuck。
感谢https://www.jianshu.com/p/0de709b3f64f解决了我的问题!!!
“用python进行aes加密的时候,只能加密数字和字母,不能对中文进行加密,会报错
Input strings must be a multiple of 16 in length
解决方方法是在cbc加密的模式下,在对字符串补齐为长度为16的倍数时,长度指标不能用中文,要先把他转为unicode编码的长度才可以。”
所以这个解决好了,又来错误了,我运行@平胸小仙女在知乎回答的原代码也出现这个错误:
TypeError: can't concat str to bytes
原因是:第一层加密之后输出的 h_encText 是一个 bytes 类型的而不是str。
将第一层的输出 h_encText 转换成str后,得到:
b'l0DReXb5aAvihEQDNTMbTa7+nPPGN/H00lOvE/h4C7Jbe8xoNDCJD7J4fMb+crzbZbu19Fk/icpW+RfQSWqvxA=='
当然是不行的,json.loads(json_text) 时就会得到很多人都遇到的的这个错误:
JSONDecodeError: Expecting value: line 1 column 1 (char 0)
这是因为你转换的str多了b和两边的引号,数据已经变了,当然就得到错误的 params ,后面的 get_json()也就得到的是空。
所以用 lstrip()和 rstrip() 把多出来的去掉。最后代码变成下面这样就能够成功运行了。
# 以下代码是2019.6.19日对@平胸小仙女代码的一点修改使之能够顺利运行
#coding = utf-8
from Crypto.Cipher import AES
import base64
import requests
import json
headers = {
'Cookie': 'appver=1.5.0.75771;',
'Referer': 'http://music.163.com/'
}
first_param = "{rid:\"\", offset:\"0\", total:\"true\", limit:\"20\", csrf_token:\"\"}"
second_param = "010001"
third_param = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"
forth_param = "0CoJUm6Qyw8W8jud"
def get_params():
iv = "0102030405060708"
first_key = forth_param
second_key = 16 * 'F'
h_encText = AES_encrypt(first_param, first_key, iv)
print(h_encText)
print(str(h_encText).lstrip('b\'').rstrip('\''))
h_encText = AES_encrypt(str(h_encText).lstrip('b\'').rstrip('\''), second_key, iv)
return h_encText
def get_encSecKey():
encSecKey = "257348aecb5e556c066de214e531faadd1c55d814f9be95fd06d6bff9f4c7a41f831f6394d5a3fd2e3881736d94a02ca919d952872e7d0a50ebfa1769a7a62d512f5f1ca21aec60bc3819a9c3ffca5eca9a0dba6d6f7249b06f5965ecfff3695b54e1c28f3f624750ed39e7de08fc8493242e26dbc4484a01c76f739e135637c"
return encSecKey
def AES_encrypt(text, key, iv):
pad = 16 - len(text) % 16
text = text + pad * chr(pad)
encryptor = AES.new(key, AES.MODE_CBC, iv)
encrypt_text = encryptor.encrypt(text)
encrypt_text = base64.b64encode(encrypt_text)
return encrypt_text
def get_json(url, params, encSecKey):
data = {
"params": params,
"encSecKey": encSecKey
}
response = requests.post(url, headers=headers, data=data)
return response.content
if __name__ == "__main__":
url = "http://music.163.com/weapi/v1/resource/comments/R_SO_4_30953009/?csrf_token="
params = get_params();
encSecKey = get_encSecKey();
json_text = get_json(url, params, encSecKey)
json_dict = json.loads(json_text)
print(json_dict.keys())
print(json_dict['hotComments'])
print (json_dict['total'])
for item in json_dict['comments']:
print (item['content'])
代码运行结果是:
这个问题解决了,回到正题,也就是实现搜索的问题上。
把 h_encText 纠正了之后,再 json.loads(json_text),程序不出错了,能够运行,但是的出来的是:
{"msg":"参数错误","code":400}
{'msg': '参数错误', 'code': 400}
我的天,这又是啥啊?
就还是 params 和 encSecKey 获取得有问题呗。想着只生成了 params , 我的 encSecKey 也要自己动手生成啊,不要用固定的那个啊。
然后参考 https://www.cnblogs.com/new-june/p/9403562.html 中大神的代码,发现这里面两个参数都是按照加密方法加密得来的,在简单的删掉 aesEncrypt 函数中的三处 bytearray(xxxx,'utf-8') 的这个 bytearray 和 utf-8之后便能够正常运行了。
运行结果如下:(并且在当前文件夹生成了一个comments文件夹,当一首歌的评论爬完之后在comments里生成一个txt)
然鹅, 把这个 get_params 和 get_encSecKey 搬到我的程序里, 还是不对不对!!!!还是:
JSONDecodeError: Expecting value: line 1 column 1 (char 0)
于是我猜想是不是搜索和评论翻页用的编码 js 不一样, 然后我就把翻评论页和搜索时候的core.js保存下来,然后把相关的函数都进行对比,发现是一模一样的。所以就用网上那些编码方式应该是对的啊!为什么搞不对呢!!!气死我了。
今天是第三天要完了。啊啊啊,还是没弄出来!有毒有毒!我要被气死了!!!不过我觉得越来越接近真相了。
------------------------------------------------------------------第四天----------------------------------------------------------------------------
搜索同一首歌 “lucky”, 刷新网页,在 Network 里面找到不同的 params 的值,和同一个 encSecKey 的值直接给 json.loads(), 发现不能够得到搜索结果,这说明同评论翻页不一样,这里的 encSecKey 不能是一个固定值,也必须实时加密得到,和 params 是一一对应的,对应的点应该是那个随机的 “second_key = 16 * 'F'”?毕竟反观core.js 中 encSecKey得到的不同只与变化的 i (即这个second_key)有关,所以要保证这个 i 和生成 params 的 i 相同 ?
啊,要不我按照 core.js 中的函数一一实现?通过 source 中的单步调试得到参数的值,看看我们的python加密出来的结果是不是一样。
在把程序中的 second_key 改成与这个里面的 i 值相同之后,即 = "S856GDWDq725eSUM", 一步一步stepover,得到 params需要经过两次加密,这里的b(),我程序中的 aesEncrypt()。下图可以看到,第一次加密得到的encText:
encText1 = "yn32X7ciiZV75Vinljri+S7q1DPP+UVXiZFZf6J/rsrzU6uqopRC3auK6N9Y7zfEN3K2r9kUZ5sT9L5IsI33oA=="
再继续单步执行可以看到下图的第二次加密得到的encText,这也就是最终的 params:
encText(params) = "2OjNmXsDZ5UDG6jnltAcNEWI6g5V+517gNsFzH28IEZczFnDQOgyE3/2/IBIz8v8qHm6JERk77JfY5lAqQCJvNWh+2Z7EQRZyx9UZcnEAx6LqvzPJ89ax2WqzNAsxB9y"
然后巧了,我的程序中打印出两次加密的结果恰如其分一模一样(上半部分输出框是我程序的输出,与下面从调试工具里复制出来的一毛一样):
所以,params的加密是没有什么问题的!!!那么?喵喵喵就是老哥的 encSecKey 的加密有问题?那就来看看吧。
首先看看core.js中加密的函数:还是有点看不懂的。那就在调试里看参数变化吧。
setMaxDigits()
setMaxDigits(),到底应该传值多少?
在JS文件中给出公式为:n * 2 / 16。其中n为密钥长度。
如果n为1024,则值应为 1024 * 2 / 16 = 128。
经过测试,传128后台解密会报错;正确的值应该大于128。
个人喜好的公式是:n * 2 / 16 + 3
即 密钥长度若为1024,其值为 131
密钥长度若为2048,其值为 259
其实和下面这段.javascript 加密代码一个套路,就是先生成公钥,然后用公钥加密内容。
function rsa_pwd(content){
//十六进制公钥
var rsa_n = "DB89C01D4550F9974C30AF5370214F3....";
setMaxDigits(131); //131 => n的十六进制位数/2+3
var key = new RSAKeyPair("10001", '', rsa_n); //10001 => e的十六进制 // 第一个参数是加密因子,第二个参数是解密因子,因为浏览器端不需要解密,所以第二个参数传入空字符串,//第三个参数 modulus 是解密钥匙
content_rsa = encryptedString(key, content); //加密,不支持汉字
return content_rsa;
}
然后程序里这个 rsaEncrypt() 函数里 text = text [::-1]什么意思? 试了一下,原来是让字符串完全颠倒过来。
额,不看了,原来老哥rsa加密也是正确的,搞得还把加密都看了一圈……佛了。可以看到程序的输出和调试得到的参数又是一毛一样。
那……到底是怎么慧思。为什么分开来看没问题啊,拼在一起咋就参数错误呢!!!
哎呀,好像找到问题了。输出的 params 是 bytes 类型的,尝试一下转一转成字符串。不顶用……
我擦,什么原理???
------------------------------------------------------------------第五天----------------------------------------------------------------------------
发现一个问题,系统生成出来的 params 总是比在调试台和程序生成出来的要长很多。 encSecKey 长度相同,我严重怀疑是不是第一个包含歌名的那个参数错了,不止那么长。参见 https://www.jianshu.com/p/0de709b3f64f 中的那个参数是:
d = '{"hlpretag":"<span class=\"s-fc7\">","hlposttag":"</span>","s":"可能否","type":"1","offset":"0","total":"true","limit":"30","csrf_token":""}'
弄上去生成的 params 长度基本相近但是还是短一点。为什么在调试台单步执行出来的参数是不正确的呢?
找不到原因、暂时搁置。
还是试试大哥https://www.jianshu.com/p/0de709b3f64f的代码吧。
呜呜呜~别人的怎么可以搜索下载。快参考一下别人的代码⑧!
移植一下,终于得到了json内的内容也就是搜索结果的歌曲信息列表:
通过各方面的了解,搜索结果中每首歌曲里面的部分重要的 keys 解释如下:
'name':歌名 / 'id':id / 'ar':艺术家/ 'alia':别名/ 'pop':流行指数/ 'al':专辑/ 'dt':时间(ms)/
'h':高质量版本(其中的size是文件大小单位是B)/ 'm':中等版本/ 'l':低质量版本/
'publishTime':出版时间(是与1970年1月1日0:0:0相差的毫秒数,
为什么这样表示:https://blog.youkuaiyun.com/sundacheng1989/article/details/51350767
如何转换:https://stackoverflow.com/questions/4964634/how-to-convert-long-type-datetime-to-datetime-with-correct-time-zone)/
'privilege':版权
所以我们就可以提取重要的歌曲信息了, 并且设置条件进行筛选。
最终下载效果如下:
终于解决了。可惜自己的尝试和探索都失败了,还看了一些加密的知识,最终还是搬了大牛老哥的代码。写了好多废话,都是做一步记录一步问题和解决方法,和我当时的无奈绝望和想放弃的心情。如果不想看废话连篇的可以直接看下面的最终代码。
代码参见:https://github.com/Haonana/netease-search-scrap
References:
https://www.zhihu.com/question/36081767
https://www.jianshu.com/p/0de709b3f64f
https://www.cnblogs.com/new-june/p/9403562.html