前言
這一篇接着上一篇繼續寫。在上一篇裏,介紹了歌曲的 查找 功能和代碼實現,這裏繼續介紹 播放 功能,那各位觀衆姥爺一起來看下吧。
鏈接分析
我們打開下面這個網頁
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 ,所以初步分析,這兩個參數應該經過加密
分析js
我們在調試頁面選擇 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=
進行post請求,就可以獲取帶有播放鏈接的json啦。
代碼
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('<Double-Button-1>', 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