这一篇接着上一篇继续写。在上一篇里,介绍了歌曲的 查找 功能和代码实现,这里继续介绍 播放 功能,那各位观众姥爷一起来看下吧。
我们打开下面这个网页
https://music.163.com/#/song?id=444267215
先分析下这个链接地址,我们发现只有一个参数,就是 id ,也就是每首歌特有的id
接着,我们在本页面按 F12 调出调试页面,选择 network 然后点击一次 播放
开始分析,发现,系统向这个链接发送post请求
也就是这个链接
https://music.163.com/weapi/song/enhance/player/url/v1?csrf_token=
返回的json中包含歌曲的播放链接:
经过分析,我们发现他有两个参数
params:
encSecKey:
我们上面说了,现在只知道一个参数 id ,所以初步分析,这两个参数应该经过加密
我们在调试页面选择 Sources 查找js代码,你可以在全局查找上面提到的两个参数:params、encSecKey,可
以发现,在 core 文件中(如果发现没有出现这个文件,可以多刷新几次页面,原因我也不太清楚)
中发现,encSecKey 一共有三个,可以点击左下角的 {} 可以代码规范化,就不会乱糟糟的,我们发现
这几句代码,params、encSecKey 都在一个叫 bXY4c 的参数中获取,而 bXY4c 是经过一个叫
window.asrsea 的函数获取,然而这个函数一共有四个参数,我们一一分析。
首先,我们在这里打上断点:
然后点击 播放,会发现程序在这里停下来,再点击下一步,我们开始分析:
分别复制 **JSON.stringify(i2x), bqu6o([“流泪”, “强”]), bqu6o(QE6y.md), bqu6o([“爱心”, “女孩”, “惊恐”, “大笑”])**这
几个参数,在 Console 页面打印,查看值
经过多次的测试发现,
bqu6o([“流泪”, “强”]), bqu6o(QE6y.md), bqu6o([“爱心”, “女孩”, “惊恐”, “大笑”]
这几个值是固定值,主要的是 JSON.stringify(i2x) 为不固定的,我们来看下他的格式
JSON.stringify(i2x) = {
'csrf_token': "",
'encodeType': "aac",
'ids': "[444267215]",
'level': "standard"
}
一眼就能看出来,ids就是歌曲的id,其他的参数现在不知道是什么,不过,可以先尝试请求,如果可以就不用再折腾啦,如果不行,就继续分析。
接着,我们看下window.asrsea 这个函数,将鼠标放在上面,点击链接
我们看到下面的两句代码:
window.asrsea = d,
window.ecnonasr = e
得知,window.asrsea 这个函数就是 d 函数,现在参数也知道了
我们来看下代码
!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
}();
可以发现,在函数 d 中,对两个参数进行赋值,而其中,a调用一次,b函数调用2次,c函数调用1次,由于楼主js基础不太好(现在在恶补),只能得知:
a函数传一个int,可以获取这个参数长度的随机字符串
b函数是某种加密手段(百度得知为AES加密)并且加密了两次
c函数也是某种加密手段,只加密一次(有大佬说,这个值可以是固定值,但是我尝试过,并不能通过)
其中,
params 由两次b函数产出
encSecKey 由c函数产出
而,两个函数都经过 a 函数,大概思路理清楚,现在可以开始用python重写。
我自己写的代码太乱了,这里参考下大佬的代码,干净整洁
import os,json
from binascii import hexlify
from Crypto.Cipher import AES
import base64
class Encrypyed():
def __init__(self):
# 加密的固有参数
self.pub_key = "010001"
self.modulus = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff6" \
"8ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee34" \
"1f56135fccf695280104e0312ecbda92557c93870114af6c9d05c" \
"4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e820" \
"47b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"
self.nonce = "0CoJUm6Qyw8W8jud"
# 随机产生16位参数
def a(self, size):
return hexlify(os.urandom(size))[:16].decode('utf-8')
# 加密
def b(self,text, key):
iv = '0102030405060708'
pad = 16 - len(text) % 16
text = text + pad * chr(pad)
encryptor = AES.new(key, AES.MODE_CBC, iv)
result = encryptor.encrypt(text)
result_str = base64.b64encode(result).decode('utf-8')
return result_str
# 产生第二个参数
def c(self, text, pubKey, modulus):
text = text[::-1]
rs = pow(int(hexlify(text.encode('utf-8')), 16), int(pubKey, 16), int(modulus, 16))
return format(rs, 'x').zfill(256)
# 赋值加密
def d(self, text):
text = json.dumps(text)
i = self.a(16)
encText = self.b(text, self.nonce)
encText = self.b(encText,i)
encSecKey = self.c(i,self.pub_key,self.modulus)
data = {'params': encText, 'encSecKey': encSecKey}
return data
调用上面的代码,d函数是入口,将第一个参数传进去,就可以获取解密后的 params 、encSecKey ,对
https://music.163.com/weapi/song/enhance/player/url/v1?csrf_token=
import requests
from music import music_data as md
from bs4 import BeautifulSoup
import urllib
def jiemi():
# 构造请求字典
query = {
'csrf_token': "",
'encodeType': "aac",
'ids': "[566521546]",
'level': "standard"
}
# 解密
do = md.Encrypyed()
# 请求参数
data = do.d(query)
print(data)
# 开始请求
r = requests.session()
# 请求头
headers = {
'origin': 'https: // music.163.com',
'referer': 'https: // music.163.com /',
'user - agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36'
}
# 请求url
url = 'https://music.163.com/weapi/song/enhance/player/url/v1?csrf_token='
# 开始请求
html = r.post(url, data=data, headers=headers)
# 只取播放链接
song_url = html.json()['data'][0]['url']
print(song_url)
# print(html.json())
到这里,我们已经完成了解密以及下载,接下来就是界面的实现。
这里的界面使用 tkinter 库,比较简单,把功能实现了,但是界面有点简陋,可视化编程就不讲解了,直接贴代码
import tkinter as tk
from tkinter import ttk
from music import wyy_music as wyy
import pygame
import os
from music import open_music as op
# 搜索音乐
def file_music():
# 清除
delButton(treeview)
print(inputText.get())
if inputText.get() != "":
global data
data = wyy.find_music(inputText.get())
# 计数器归零
i = 0
for music in data:
# print(i)
treeview.insert('', i, values=(i, music['b'], music['c'], music['time']))
i += 1
# 清空表单
def delButton(treeview):
x = treeview.get_children()
for item in x:
treeview.delete(item)
# 绑定事件
def treeviewClick(event):
for item in treeview.selection():
item_text = treeview.item(item, "values")
print(item_text[0]) # 输出所选行的第一列的值
# 获取歌曲id
music_id = data[int(item_text[0])]['a'][9:]
# print(byte_obj)
# 对id进行判断,是否已经下载
find_mp3 = os.path.exists(r"my_music/"+music_id+'.mp3')
if find_mp3:
print('文件已经存在不用下载')
else:
print('正在下载...')
op.login_music(music_id)
pygame.mixer.music.load(r"my_music/"+music_id+".mp3")
# 播放音乐
pygame.mixer.music.play()
# 初始化播放器
pygame.mixer.init()
# 启动浏览器
wyy.open_chrome()
# 查找后的歌曲存放
data = {}
# 初始化Tk()
root = tk.Tk()
root.title("音乐播放器V1.0") # 设置窗口标题
root.geometry("1100x600") # 设置窗口大小 注意:是x 不是*
root.resizable(width=False, height=False) # 设置窗口是否可以变化长/宽,False不可变,True可变,默认为True
# 设置输入框
inputText = tk.Entry(root, show=None, foreground='black', font=('Helvetica', '15', 'bold'), insertbackground='green',
width=20)
inputText.place(x=400, y=10,)
# 设置按钮,以及放置的位置
searchBtn = tk.Button(root, text="搜索", fg="blue", bd=2, width=10, command=file_music) # command中的方法带括号是直接执行,不带括号才是点击执行
searchBtn.place(x=650, y=8, anchor='nw')
update_progress = tk.StringVar()
# 创建滚动条
scroll = tk.Scrollbar()
columns = ("编号", "歌曲", "演唱者", "时长")
treeview = ttk.Treeview(root, height=18, show="headings", columns=columns) # 表格
treeview.column("编号", width=100, anchor='center') # 表示列,不显示
treeview.column("歌曲", width=300, anchor='center')
treeview.column("演唱者", width=300, anchor='center')
treeview.column("时长", width=300, anchor='center')
treeview.heading("编号", text="编号") # 显示表头
treeview.heading("歌曲", text="歌曲")
treeview.heading("演唱者", text="演唱者")
treeview.heading("时长", text="时长")
# side放到窗体的哪一侧, fill填充
scroll.pack(side=tk.RIGHT, fill=tk.Y)
treeview.pack(side=tk.LEFT, fill=tk.Y)
# 关联
scroll.config(command=treeview.yview)
treeview.config(yscrollcommand=scroll.set)
treeview.pack()
treeview.place(x=45, y=120,)
# 双击触发
treeview.bind('' , treeviewClick)
# 进入消息循环
root.mainloop()
这里有一个小小的插曲,我们抓取的链接,是 .m4p 结尾的,但是我找的播放库,都是不支持 .m4p,需要先进行转
码,这里比较麻烦,我也没有找到解决的办法,不知道各位大佬有没有建议。
播放的思路是先将歌曲下载到本地,然后再进行播放,如果遇到网速比较慢的可能有点延迟,还有,播放前会先进行一个
判断,如果本地已经有这个音乐就不会重新下载,直接播放。
经过百度处理 .m4p 无果后,偶然得知一个接口。
'http://music.163.com/song/media/outer/url?id='+music_id+'.mp3'
这个链接可以获取 mp3 音乐,id 依然是音乐的id,下载后可以直接用 pygame 库播放。
(哎呀…之前那些工作有点多余呀,不过一番下来后,了解了一些没触及的知识,还是有所收获)
好了,完整的代码就这样子,我会将它上传到我的 github 库,上传后供大家下载~
有疑问可以问我哦,最后祝大家敲码愉快。
https://github.com/1040230345/wyy_crawler.git