所有文章 > API开发 > Python应用 | 网易云音乐热评API获取教程

Python应用 | 网易云音乐热评API获取教程

“十年前你说生如夏花般绚烂,十年后你说平凡才是唯一的答案”,这是网易云音乐里歌曲《生如夏花》点赞量最多的一条评论。网易云音乐aka“网抑云音乐”,因其歌曲评论区的精彩评论往往能够引起人们的各种情感共鸣而频频出圈。本文主要讲解如何使用Python来实现根据歌曲名获取歌曲精彩评论的功能。该功能已上线至个人小程序,欢迎点击使用网易云音乐歌曲热评,目前功能有:

  • 获取歌曲精彩评论;
  • 获取歌曲歌词。

项目信息

项目介绍

根据歌曲名获取网易云音乐的精彩评论。

项目地址

项目gitee地址:https://gitee.com/shawn_chen_rtz/netease_cloud_music.git,欢迎star。

环境准备

需要安装依赖requests、pycrypto模块,可通过pip命令。

pip install requests==2.27.1
pip install pycrypto==2.6.1

获取歌曲id

通过歌曲名获取到歌曲id对于后续的获取精彩评论和歌词信息至关重要。

接口信息

网易云音乐的根据歌曲名称获取对应歌曲id的接口信息:http://music.163.com/api/search/get,请求method为GET,入参格式{“s”:song_name,”type”:1,”limit”:1},如下,

GET http://music.163.com/api/search/get
入参格式:
{
's': song_name, # 搜索关键字,本文中项目实现传入待查询歌曲名称
'type': 1, # 搜索类型:1为单曲,2为专辑,3为歌手,4为歌词
'limit': 1, # 返回数量,本文中项目实现,设置为1,即返回1首
}

其中入参的参数‘s’表示查询关键字,接收我们传入的歌曲名称;

参数‘type’表示查询对象分类,在项目实现中设置值为1(表示搜索类型为歌曲);

参数‘limit’表示返回数据数量,在项目实现中设置值为1(只取1首)。

代码实现

在项目中定义一个查询歌曲id的方法search_song_id(),具体代码实现如下,

import requests
import json

def search_song_id(song_name):
# 网易云音乐API地址
url = 'http://music.163.com/api/search/get'
# 参数设置
params = {
's': song_name, # 搜索关键字
'type': 1, # 搜索类型:1为单曲,2为专辑,3为歌手,4为歌词
'limit': 1, # 返回数量
}
# 发送GET请求
response = requests.get(url, params=params)
# 解析JSON数据
data = response.json()
# 检查是否有结果
if data['result']['songs']:
# 返回歌曲ID
return data['result']['songs'][0]['id']
else:
return None

if __name__ == "__main__":
# 使用函数查找歌曲ID
song_id = search_song_id('Your Song Name')
print(song_id)
song_id = search_song_id('偏偏喜欢你')
print(song_id)

接口http://music.163.com/api/search/get请求后返回的数据组织结构如下,

根据接口返回的数据组织结构,歌曲id即data[‘result’][‘songs’][0][‘id’]。

获取歌曲歌词

通过上一节获取到的歌曲id查询歌曲歌词。

接口信息

网易云音乐对应接口信息:http://music.163.com/api/song/lyric?id=song_id+&lv=1&tv=-1,其中song_id替换为目标歌曲id,该接口请求Method为GET。

代码实现

定义了一个获取歌曲歌词的方法get_lyric(),具体代码实现如下,

import requests
import json

def get_lyric(song_id):
headers = {
"user-agent":"Mozilla/5.0",
"Referer":"http://music.163.com",
"Host":"music.163.com"
}
if not isinstance(song_id,str):
song_id = str(song_id)
url = f"http://music.163.com/api/song/lyric?id={song_id}+&lv=1&tv=-1"
r = requests.get(url,headers=headers)
r.raise_for_status()
r.encoding = r.apparent_encoding
json_obj = json.loads(r.text)
lyric = json_obj['lrc']['lyric']
#print(type(lyric))
return lyric

方法return返回歌曲歌词lyric。

获取歌曲精彩评论


歌曲精彩评论接口信息

首先要找到返回歌曲精彩评论数据的目标接口,访问网易云音乐网页,搜索某歌曲进入歌曲页面,F12打开浏览器开发者工具—网络,

刷新页面,筛选请求列表,定位到目标接口https://music.163.com/weapi/comment/resource/comments/get?csrf_token=,请求Method为POST,接口表单参数有两个,分别是paramsencSecKey,如下面2张图所示,

可以看到向接口提交的两个表单参数都是一长串的字符串,显然是经过加密的,在下一节将对参数的加密逻辑进行探索分析。

接口参数的加密逻辑分析

对接口参数加密方式的分析可以说是整个项目中最具挑战的点,事实上就是一种逆向工程,我们要大胆的假设,小心地求证。当然这种大胆假设是在有一定技术基础的前提下做出的合理假设,在本项目中涉及有前端、后端、加/解密等技术。F12浏览器开发者工具-网络,重复刷新歌曲页面,可以看到接口参数的值发生了变化但是值对应的字符串长度保持不变,可以猜到参数的加密过程中有随机性操作且明文的结构可能是固定的。进一步点击目标接口请求的启动器标签,观察其请求启动器链,可以看到有一个js文件,如下图所示,

js文件的域名为https://s3.music.126.net,于是去开发者工具-源代码/来源中去找这个js文件,找到如下图,

根据域名信息找到该js文件,可以看到其源码。全文搜索参数paramsencSecKey,果然有定位到,

从此处可以确认这两个参数是经过js文件中的代码实现的加密功能。接下来,就是分析这个js文件,确认加密逻辑和所加密的明文文本信息
在js文件中,找到这么一串代码片段(js代码),

!function() {
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) {
var c = CryptoJS.enc.Utf8.parse(b)
, d = CryptoJS.enc.Utf8.parse("0102030405060708")
, e = CryptoJS.enc.Utf8.parse(a)
, f = CryptoJS.AES.encrypt(e, c, {
iv: d,
mode: CryptoJS.mode.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) {
var h = {}
, i = a(16);
return h.encText = b(d, g),
h.encText = b(h.encText, i),
h.encSecKey = c(i, e, f),
h
}
function e(a, b, d, e) {
var f = {};
return f.encText = c(a + e, b, d),
f
}
window.asrsea = d,
window.ecnonasr = e
}();

var bVi0x = window.asrsea(JSON.stringify(i1x), bse6Y(["流泪", "强"]), bse6Y(Qu9l.md), bse6Y(["爱心", "女孩", "惊恐", "大笑"]));
e1x.data = j1x.cr1x({
params: bVi0x.encText,
encSecKey: bVi0x.encSecKey
})

从上面两个js代码片段可以一步一步地逆向分析:

  1. 参数paramsencSecKey分别对应的是对象变量bVi0x的属性encText和encSecKey;
  2. bVi0x对象又是从window.asrsea()方法返回的,该方法有4个参数——JSON.stringify(i1x), bse6Y([“流泪”, “强”]), bse6Y(Qu9l.md), bse6Y([“爱心”, “女孩”, “惊恐”, “大笑”]);
  3. js文件中搜索window.asrsea,定位到其定义位置,看到window.asrsea = d,是d函数本身的引用,js中函数是第一类对象(first class object),同Python,可以作为参数传给函数,也可以作为函数的返回值。d函数的4个参数d、e、f、g对应上一步骤调用window.asrsea()时传入的4个参数;事实上d函数就是我们要找的加密函数;
  4. d函数体中又调用了函数a、b、c,首先先确定函数a、b、c的功能。
  • a函数
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
}

接收一个参数,可以看出a函数实现的功能是从大、小写字母和0~9数字这个范围中随机挑选字符组成长度为参数a的随机字符串返回。比如调用a函数,a(16)即返回一个长度为16的随机字符串;

  • b函数
function b(a, b) {
var c = CryptoJS.enc.Utf8.parse(b)
, d = CryptoJS.enc.Utf8.parse("0102030405060708")
, e = CryptoJS.enc.Utf8.parse(a)
, f = CryptoJS.AES.encrypt(e, c, {
iv: d,
mode: CryptoJS.mode.CBC
});
return f.toString()
}

接收两个参数,对参数a进行AES加密,加密模式为CBC,将结果转为字符串形式返回;

  • c函数
function c(a, b, c) {
var d, e;
return setMaxDigits(131),
d = new RSAKeyPair(b,"",c),
e = encryptedString(d, a)
}

接收三个参数,对参数a进行RSA加密,返回的结果是十六进制的字符串形式;

  • 再回头看d函数的定义,
function d(d, e, f, g) {
var h = {}, i = a(16);
return h.encText = b(d, g),
h.encText = b(h.encText, i),
h.encSecKey = c(i, e, f),
h
}

可以明确d函数体中的处理逻辑:

  • 声明一个空对象h,一个由a函数生成的16位随机字符串赋给变量i,
  • b函数接收d函数的一个参数d和第四个参数g,也就是对d函数的参数d进行一次AES加密;
  • 加密结果和16位字符串变量i再次传给b函数进行了第二次AES加密,结果赋给了对象h的属性encText,也就是我们接口请求中的其中一个参数encText;
  • 16位字符串i、d函数的第二个参数e、d函数的第三个参数f,传给c函数进行一次RSA加密,事实上就是对16位随机字符串进行的加密操作,结果赋给了对象h的属性encSecKey,也就是我们接口请求中的其中一个参数encSecKey。

以上就是js文件中定义的完整加密处理过程,加密逻辑确认达成函数定义了之后,js需要调用以上函数用以生成加密后的参数,这个过程的关键点在于明确加密函数d被调用时接收的4个参数值,即明文文本信息和相关。如何明确呢?F12打开浏览器开发者工具打断点进行调试。

在图示js文件标记的所在行打断点,刷新网易云歌曲页面,点击右侧的箭头,不断的继续执行脚本,同时观察页面元素的加载情况(随着不断地往下执行脚本,页面元素像歌词、评论等信息会逐个显示出来);在浏览器开发者工具—控制台中打印加密方法的4个参数JSON.stringify(i1x), bse6Y([“流泪”, “强”]), bse6Y(Qu9l.md), bse6Y([“爱心”, “女孩”, “惊恐”, “大笑”]),每次点击继续执行脚本执行到断点处,都打印一次,可以看到后面的三个参数bse6Y([“流泪”, “强”]), bse6Y(Qu9l.md), bse6Y([“爱心”, “女孩”, “惊恐”, “大笑”])的值都是固定不变的,分别是’010001’、’00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7’、‘0CoJUm6Qyw8W8jud’,如下图,

同时可以看到第一个参数在调试过程中不断地发生变化,不像其他参数是固定不变的,那么如何确定其值呢?每首歌对应的是各自的评论数据,一一对应关系,不会对不上号,不会错号,所以合理推断参数值中应该会有歌曲的id信息。在调试的过程中,不断执行脚本,不断在断点处悬停,当页面上的评论数据第一次出现时,届时打印出的console.log(JSON.stringify(i1x))值就是精彩评论接口参数加密函数被调用时传入的正确值。经调试得到结果,第一个参数的值是'{“rid”:”R_SO_4_139357″,”threadId”:”R_SO_4_139357″,”pageNo”:”1″,”pageSize”:”20″,”cursor”:”-1″,”offset”:”0″,”orderType”:”1″,”csrf_token”:””}’,可以看到其中“R_SO_4_139357”中的139357正是对应歌曲的id,这也验证了我们之前的推测。以上就全部确定了js文件中调用加密函数d(window.asrsea()方法)时所传入的4个参数值,即window.asrsea(‘{“rid”:”R_SO_4_139357″,”threadId”:”R_SO_4_139357″,”pageNo”:”1″,”pageSize”:”20″,”cursor”:”-1″,”offset”:”0″,”orderType”:”1″,”csrf_token”:””}’,’010001′,’00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7′,‘0CoJUm6Qyw8W8jud’)。明文文本信息和相关确认达成接下来就是使用Python代码替换JavaScript重写上述a、b、c函数功能,并按照d函数体中的处理过程进行调用,获取到加密参数值,此内容将在下一节进行介绍,read on.

参数加密过程的Python实现

js中的a函数,对应Python实现如下,

import random
def generate_random_strs(length):
"""
生成固定长度的字符串
:param length: 指定生成的字符串长度
:return: 返回length长度的字符串
"""
string = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
i = 0
random_strs = ""
while i < length:
e = random.random() * len(string)
e = math.floor(e)
random_strs = random_strs + string[e]
i = i + 1
return random_strs

js中的b函数(AES加密),对应Python实现如下,

from Crypto.Cipher import AES
import base64

def AESencrypt(msg,key):
"""
AES加密
:param msg:
:param key:
:return:
"""
# 查缺
padding = 16 - len(msg) % 16
# 补缺
msg = msg + padding * chr(padding)
# 用来加密或者解密的初始向量(必须是16位)
iv = "0102030405060708"
cipher = AES.new(key,AES.MODE_CBC,iv)
# 加密后得到的是bytes类型的数据
encryptedbytes = cipher.encrypt(msg.encode('utf-8'))
# 使用Base64进行编码,返回byte字符串
encodestrs = base64.b64encode(encryptedbytes)
# 对byte字符串按utf-8进行解码
enctext = encodestrs.decode("utf-8")
return enctext

js中的c函数(RSA加密),对应Python实现如下,

import codecs

def RSAencrypt(randomstrs,key,f):
"""
RSA加密
:param randomstrs:
:param key:
:param f:
:return:
"""
string = randomstrs[::-1]
text = bytes(string,'utf-8')
seckey = int(codecs.encode(text,encoding='hex'),16) ** int(key,16) % int(f,16)
return format(seckey,'x').zfill(256)

接下来,我们只要按照js d函数的处理逻辑对generate_random_strs(length)、AESencrypt(msg,key)、RSAencrypt(randomstrs,key,f)函数依次调用,即可获得加密参数值。在上一节的最后部分,已经确定了js加密函数d被调用时所接收的参数值,其中3个是固定不变的值,第1个参数是含有歌曲id的固定格式的值,用Python拼装一下,获取加密参数的代码如下,

def get_params(song_id):
"""
获取加密参数encText,encSecKey
:params song_id: str
:return:
"""
# 用于获取某首歌的评论数据
msg = '{"rid":"R_SO_4_' + song_id + '","threadId":"R_SO_4_' + song_id + '","pageNo":"1","pageSize":"20","cursor":"-1","offset":"0","orderType":"1","csrf_token":""}'
key = '0CoJUm6Qyw8W8jud'
f = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7'
e = '010001'

enctext = AESencrypt(msg,key)
i = generate_random_strs(16)
encText = AESencrypt(enctext,i)

encSecKey = RSAencrypt(i,e,f)
return encText,encSecKey

调用get_params(song_id),最终返回加密参数值encText,encSecKey。


获取歌曲精彩评论代码实现

根据上一节中返回的加密参数值进行接口请求,最终获取歌曲精彩评论数据,代码实现如下,

import requests

from utils import get_params
from get_song_id import search_song_id
from get_song_lyric import get_lyric

song_name = input("请输入歌曲名:")
song_id = search_song_id(song_name)
# 返回的是数字类型,转成字符串类型
song_id = str(song_id)
print("查找到的歌曲id:",song_id)
if song_id:
url = "https://music.163.com/weapi/comment/resource/comments/get?csrf_token="
params = get_params(song_id)
data = {"params":params[0],"encSecKey":params[1]}
resp = requests.post(url=url,data=data)
result = resp.json()
hot_comments = result['data']['hotComments']
if hot_comments:
hot_comments = [{"content":hot['content'],"timeStr":hot['timeStr'],"likedCount":hot['likedCount'],"user":hot['user']['nickname'],"avatarUrl":hot['user']['avatarUrl']} for hot in hot_comments]

for hot in hot_comments:
print(hot)
else:
print("该歌曲暂无热评!")
else:
print("未查到该歌曲!")

总结

本文提供了一个完整的教程,教读者如何使用Python来获取网易云音乐中特定歌曲的热评和歌词,包括详细的代码实现和步骤说明。

总结

目的:开发一个Python程序,用于获取网易云音乐中特定歌曲的精彩评论和歌词。

功能

  1. 获取歌曲精彩评论。
  2. 获取歌曲歌词。

项目地址:https://gitee.com/shawn_chen_rtz/netease_cloud_music

环境准备:需要安装requests和pycrypto模块,可通过pip命令安装。

获取歌曲id:通过歌曲名搜索,获取歌曲id。使用网易云音乐的搜索API http://music.163.com/api/search/get。

获取歌曲歌词:使用歌曲id调用歌词API http://music.163.com/api/song/lyric?id=song_id&lv=1&tv=-1,替换其中的song_id。

获取歌曲精彩评论

  • 挑战:分析和逆向工程加密参数的逻辑。
  • 加密逻辑:通过JavaScript文件中的函数分析加密过程,包括生成随机字符串、AES加密和RSA加密。
  • Python模拟实现:将JavaScript中的加密逻辑转换为Python代码,实现加密参数的生成。

结果展示

  • 热评展示:如果存在热评,展示热评内容,包括评论者昵称、评论内容、点赞数和用户头像链接。
  • 无热评提示:如果该歌曲暂无热评,则提示用户。

文章转自微信公众号@仰望天空的蜗牛

#你可能也喜欢这些API文章!