爬虫实战(3)--爬取网易云音乐,做一个自己的音乐播放器(下)

爬虫实战(3)--网易云音乐爬取

  • 前言
  • 链接分析
    • 分析js
    • 代码分析
    • 代码
  • 界面实现
    • 彩蛋
  • 仓库

前言

这一篇接着上一篇继续写。在上一篇里,介绍了歌曲的 查找 功能和代码实现,这里继续介绍 播放 功能,那各位观众姥爷一起来看下吧。

链接分析

我们打开下面这个网页

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中包含歌曲的播放链接:
爬虫实战(3)--爬取网易云音乐,做一个自己的音乐播放器(下)_第1张图片
经过分析,我们发现他有两个参数

params: 
encSecKey: 

我们上面说了,现在只知道一个参数 id ,所以初步分析,这两个参数应该经过加密

分析js

我们在调试页面选择 Sources 查找js代码,你可以在全局查找上面提到的两个参数:paramsencSecKey,可

以发现,在 core 文件中(如果发现没有出现这个文件,可以多刷新几次页面,原因我也不太清楚)
爬虫实战(3)--爬取网易云音乐,做一个自己的音乐播放器(下)_第2张图片
中发现,encSecKey 一共有三个,可以点击左下角的 {} 可以代码规范化,就不会乱糟糟的,我们发现
在这里插入图片描述
这几句代码,paramsencSecKey 都在一个叫 bXY4c 的参数中获取,而 bXY4c 是经过一个叫

window.asrsea 的函数获取,然而这个函数一共有四个参数,我们一一分析。

首先,我们在这里打上断点:

在这里插入图片描述
然后点击 播放,会发现程序在这里停下来,再点击下一步,我们开始分析:
在这里插入图片描述
分别复制 **JSON.stringify(i2x), bqu6o([“流泪”, “强”]), bqu6o(QE6y.md), bqu6o([“爱心”, “女孩”, “惊恐”, “大笑”])**这

几个参数,在 Console 页面打印,查看值

爬虫实战(3)--爬取网易云音乐,做一个自己的音乐播放器(下)_第3张图片

经过多次的测试发现,
bqu6o([“流泪”, “强”]), bqu6o(QE6y.md), bqu6o([“爱心”, “女孩”, “惊恐”, “大笑”]

这几个值是固定值,主要的是 JSON.stringify(i2x) 为不固定的,我们来看下他的格式

JSON.stringify(i2x) = {
        'csrf_token': "",
        'encodeType': "aac",
        'ids': "[444267215]",
        'level': "standard"
    }

一眼就能看出来,ids就是歌曲的id,其他的参数现在不知道是什么,不过,可以先尝试请求,如果可以就不用再折腾啦,如果不行,就继续分析。

接着,我们看下window.asrsea 这个函数,将鼠标放在上面,点击链接
在这里插入图片描述
爬虫实战(3)--爬取网易云音乐,做一个自己的音乐播放器(下)_第4张图片
我们看到下面的两句代码:

 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('', 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

你可能感兴趣的:(python爬虫)