爬蟲實戰(3)--爬取網易雲音樂,做一個自己的音樂播放器(下)

前言

這一篇接着上一篇繼續寫。在上一篇裏,介紹了歌曲的 查找 功能和代碼實現,這裏繼續介紹 播放 功能,那各位觀衆姥爺一起來看下吧。

鏈接分析

我們打開下面這個網頁

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代碼,你可以在全局查找上面提到的兩個參數:paramsencSecKey,可

以發現,在 core 文件中(如果發現沒有出現這個文件,可以多刷新幾次頁面,原因我也不太清楚)
在這裏插入圖片描述
中發現,encSecKey 一共有三個,可以點擊左下角的 {} 可以代碼規範化,就不會亂糟糟的,我們發現
在這裏插入圖片描述
這幾句代碼,paramsencSecKey 都在一個叫 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函數是入口,將第一個參數傳進去,就可以獲取解密後的 paramsencSecKey ,對

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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章