爬取网易云,搜索指定歌曲,一段为期六天的心酸心累爬歌史。

爬网易云音乐实现搜索,用了一个星期,头发都掉了一大撮。

主要是实现了通过程序在网易云音乐搜索某首歌曲然后下载的功能,可以批量下载。

这是个记录贴,很多废话,是解决问题,分析错误的记录。看代码的请直接移步文末!!!

最近我哥给了个歌单,走的是怀旧路线,2003年排行榜前100的歌,让我帮他下载这些歌。

作为一个程序员,当然是要爬虫,手动多low啊!!!

网上有很多爬网易云网络歌单或者在线排行榜的,然而本地歌单的话需要搜索歌名,但是这方面的博客资料基本找不到。

摸索吧,照抓翻页评论那样来找请求传输的两个参数吧。

paramsencSecKey 两个参数用了2天半。。。尝试 fiddler 用了一天多, 尝试 charles 用了大半天。。。缓存也清过,证书代理也设置过,浏览器也测试了360、IE、chrome、Firefox, 系统也尝试了Ubuntu 18.04win10……就是抓不到 js ,替换本地 js 也不管用,心态真的爆炸!!!boomboomboom !!!

遇到问题:

1. 直接用get取链接,获取不到任何内容

2. 按照那些爬评论的所说想办法得到 params 和 encKey。就要找到4个参数。浏览器的F12能找到js文件,但是使用 fiddlercharles 抓取不到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.csdn.net/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

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值