编写一个网易云音乐爬虫程序

本次借助wxPython编写一个网易云音乐的爬虫程序,能够根据一个歌单链接下载其下的所有音乐

前置说明

网易云音乐提供了一个下载接口:

http://music.163.com/song/media/outer/url?id=xxx

所以只需要拿到歌单中每首歌曲对应的 id 即可

1. 分析歌单网页元素

打开网易云音乐,复制一个歌单链接

打开chrome,查看网页元素 这里有个细节:我们拿到的歌单url中有一个符号“/#”,

因为之前爬虫其他网站时,也是直接请求初始url,一般Elements标签中的内容就是response返回的内容,

所以刚开始我一直在请求这个url,但是发现这次返回的内容总是不对,响应内容和页面元素不一致;

后来切换到Network标签下的Doc菜单查看具体发送了哪些请求,如下图标记所示,

实际有效请求的url中没有"/#"这个符号,所以后面在定义初始url时,需要把这部分字符串替换掉

要提取的元素如下

(1)提取歌曲名称

(2)提起歌曲对应的id(下载歌曲时需要使用)

2. 解析响应内容

获取到歌单页面的响应内容后,下一步就是提取出想要的内容

方法有很多种,如BeautifulSoup、XPATH、pyquery、正则表达式

这次使用正则表达式提取,这里我提取了歌单名称、歌曲id、歌曲名称,如下

代码语言:javascript
复制
def parse_html(self, request_url):
        """解析歌单页面,提取元素"""
        global headers
        html_text = self.get_html_text(url=request_url, header=headers, method="get")  # 调用get_html_text()方法,获取歌单页面响应内容
        # print(html_text)
    ###########使用正则表达式提取歌单名称、歌曲名称以及歌曲id############
    try:
        title = re.search(r'<title>(.*?) -.*?</title>', html_text).group(1)  # 匹配歌单名称
        # print(title)


        pattern_1 = re.compile(r'<li><a.*?id=(\d+)">' # 匹配歌曲id
                             r'(.*?)</a>', re.S)  # 匹配歌曲名称
        musics = pattern_1.findall(html_text) # 查找所有结果,每组数据以一个元组形式,组成一个列表格式返回
        # print(musics)
        music_list = {
            "title": title,
            "music_list": musics
        }
        return music_list
    except Exception as e:
        print(&#34;请求歌单UR了出错,检查url是否正确,报错信息为:&#34;, e)</code></pre></div></div><p><strong>3. 构造程序界面</strong></p><p>因为这次要做一个界面程序,实现如下要求  </p><ul class="ul-level-0"><li>能够自定义选择保存路径 </li><li>在界面输入歌单url后,可以直接爬取其下歌曲 </li><li>下载过程能够展示在界面中 

以前写的几个界面工具都是用的python自带的tkinter,这次试着用一下wxPython,看下效果如何

(1)确保自己的电脑中安装了wxPython

(2)下载安装wxFormBuilder

这是一个可视化的GUI布局工具,并且可以生成对应的python代码,当然也可以通过一个一个的敲代码把界面布局搞好,但是如果元件过多的话,这种方式还是比较麻烦,相对来说还是觉界面拖拽布局比较直观

(3)界面布局 先来看下最终的效果

第一步

打开wxFormBuilder,新建一个project,切换到Forms标签,新建一个Frame

Frame是这个界面的主界面,可以在右侧属性栏修改一些属性,如大小、背景色, title表示工具栏显示的名称

下划至wxWindow有一个bg属性,可以改变背景色,

其他诸如窗口大小等也是在wxWindow下的size属性修改,可以自行探索

第二步

有了Frame后,还需要添加Layout,它规定了按钮、输入框、文本框等这些元件如何在界面中布局,给它们划定了位置,

没有添加Layout的话,是不能添加那些元件的

常用的有wxBoxSizer、wxStaticBoxSizer、wxGridBoxSizer、wxFlexGridBoxSizer等,

可以通过组合这些不同的布局方式形成多样化的展示页面(我也是边做边摸索,刚开始学弄的不太美观,别介意.....)

第三步

开始添加控件,如静态文本展示框、文本框、按钮、路径选择控件

切换到Common标签,可以在这里面添加文本框和按钮

(1)按钮一般需要绑定事件,点击触发对应的操作

可以先在右侧Events菜单定义事件名称(也就是函数名),后面在写功能代码时补充即可

(2)静态文本wxStaticText,我一般用来展示一些说明性的文字

这里有一点很厉害,可以给文本设置字体,如果你的电脑字库中安装了某些字体,可以直接选择展示(注意的是如果把程序拷贝到其他电脑,如果没有对应字体的话,会看不到效果的)

(3)文本框wxTextCtrl,用来设置输入框、输出框

例如可以设置一个文本框来接收输入的歌单url,或者用来把代码运行日志展示在文本框

同样的,它也可以设置文本框展示文字的字体和大小;

另外如果当做输出框展示的话,一般会把文本框设置的大一些,

同时,希望能够随着文本增加自动往下滚动(就是滚动条)

勾选右侧属性栏-window_style中的wxVSCROLL,可以添加垂直方向滚动条;

勾选wxHSCROLL可以添加横向滚动条

另外如果想换行展示文本,可以通过style中的 wxTE_CHARWRAP和wxTE_MULTILINE来实现,它可以识别输出文本中的换行符,实现换行效果

(4)下拉菜单wxComboBox,它可以实现下拉菜单的功能,自定义几个选项

(5)路径选择框,wxpython也提供了路径选择控件,可以直接使用

4. 将界面布局代码拷贝到python中

在进行页面布局的过程中,会实时在Bditor中的python下生成对应的python代码

接下来需要做2件事情

(1)打开pycharm新建一个py文件,比如新建一个Net_Music_GUI.py,然后把wxFormBuilder生成的代码拷贝这个文件中 这样做的目的是保持页面布局代码的独立性,方便后续调整页面布局

(2)再次新建一个py文件,比如新建一个download_music.py,这个文件是最终执行的文件,在这里面新建一个类并继承Net_Music_GUI.py中的MyFrame1类 这样的话就可以使用页面布局了

5. 完善download_music.py

这里说的完善,一是要继承之前的创建好的页面布局代码,二是揉和爬虫功能代码,三是补充之前定义的按钮绑定事件

之前定义了3个按钮,下面是对应的事件回调代码

代码语言:javascript
复制
def download(self, event):
        """定义下载按钮回调方法"""
        url =self.m_textCtrl1.GetValue().replace("/#", "") # 拿到url输入框的值,并去掉url中的/#符号
    if url:
        print(url)
        self.download_music(url)
    else:
        self.m_textCtrl1.SetValue(&#34;请输入url&#34;)

def reset(self, event):
    &#34;&#34;&#34;定义清空url输入框内容方法&#34;&#34;&#34;
    self.m_textCtrl1.Clear()

def clear(self, event):
    &#34;&#34;&#34;定义清空日志输出框的方法&#34;&#34;&#34;
    self.m_textCtrl2.Clear()</code></pre></div></div><p>还有一点需要说一下,因为是自定义保存路径,所以需要拿到界面工具自选的路径  </p><p>wxDirPickerCtrl有一个方法 GetPath(),可以获取当前显示的路径值</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-javascript"><code class="language-javascript" style="margin-left:0">root_dir = self.m_dirPicker1.GetPath() # 获取GUI界面自定义选择的路径</code></pre></div></div><p>贴一下完整代码  </p><p><strong>Net_Music_GUI.py</strong></p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-javascript"><code class="language-javascript" style="margin-left:0"># -*- coding: utf-8 -*-

###########################################################################

Python code generated with wxFormBuilder (version Jun 17 2015)

http://www.wxformbuilder.org/

PLEASE DO "NOT" EDIT THIS FILE!

###########################################################################

import wx
import wx.xrc

###########################################################################

Class MyFrame1

###########################################################################

class MyFrame1(wx.Frame):
def init(self, parent):
wx.Frame.init(self, parent, id=wx.ID_ANY, title=u"网易云音乐爬虫程序-by 我是冰霜", pos=wx.DefaultPosition,
size=wx.Size(579, 592), style=wx.DEFAULT_FRAME_STYLE | wx.TAB_TRAVERSAL)

    self.SetSizeHints(wx.DefaultSize, wx.DefaultSize)
    self.SetForegroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNTEXT))
    self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW))

    bSizer1 = wx.BoxSizer(wx.VERTICAL)

    self.m_staticText1 = wx.StaticText(self, wx.ID_ANY, u&#34;请输入歌单链接&#34;, wx.DefaultPosition, wx.DefaultSize, 0)
    self.m_staticText1.Wrap(-1)
    self.m_staticText1.SetFont(wx.Font(15, 70, 90, 90, False, &#34;站酷小薇LOGO体&#34;))
    self.m_staticText1.SetForegroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNTEXT))
    self.m_staticText1.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW))

    bSizer1.Add(self.m_staticText1, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 5)

    self.m_textCtrl1 = wx.TextCtrl(self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.Size(500, 30), 0)
    self.m_textCtrl1.SetFont(wx.Font(12, 70, 90, 90, False, wx.EmptyString))

    bSizer1.Add(self.m_textCtrl1, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL | wx.EXPAND, 5)

    self.m_dirPicker1 = wx.DirPickerCtrl(self, wx.ID_ANY, wx.EmptyString, u&#34;Select a folder&#34;, wx.DefaultPosition,
                                         wx.Size(300, -1), wx.DIRP_DEFAULT_STYLE)
    bSizer1.Add(self.m_dirPicker1, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL | wx.EXPAND, 5)

    self.m_panel2 = wx.Panel(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL)
    gSizer1 = wx.GridSizer(0, 2, 0, 0)

    self.m_button1 = wx.Button(self.m_panel2, wx.ID_ANY, u&#34;下载&#34;, wx.DefaultPosition, wx.DefaultSize, 0)
    self.m_button1.SetFont(wx.Font(12, 70, 90, 90, False, &#34;站酷小薇LOGO体&#34;))

    gSizer1.Add(self.m_button1, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 5)

    self.m_button2 = wx.Button(self.m_panel2, wx.ID_ANY, u&#34;重置&#34;, wx.DefaultPosition, wx.DefaultSize, 0)
    self.m_button2.SetFont(wx.Font(12, 70, 90, 90, False, &#34;站酷小薇LOGO体&#34;))

    gSizer1.Add(self.m_button2, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 5)

    self.m_panel2.SetSizer(gSizer1)
    self.m_panel2.Layout()
    gSizer1.Fit(self.m_panel2)
    bSizer1.Add(self.m_panel2, 1, wx.ALL | wx.EXPAND, 5)

    sbSizer1 = wx.StaticBoxSizer(wx.StaticBox(self, wx.ID_ANY, u&#34;结果展示区&#34;), wx.VERTICAL)

    self.m_button6 = wx.Button(sbSizer1.GetStaticBox(), wx.ID_ANY, u&#34;清空&#34;, wx.DefaultPosition, wx.DefaultSize, 0)
    self.m_button6.SetFont(wx.Font(12, 70, 90, 90, False, &#34;站酷小薇LOGO体&#34;))

    sbSizer1.Add(self.m_button6, 0, wx.ALL, 5)

    self.m_textCtrl2 = wx.TextCtrl(sbSizer1.GetStaticBox(), wx.ID_ANY, wx.EmptyString, wx.DefaultPosition,
                                   wx.Size(500, 600), wx.TE_CHARWRAP | wx.TE_MULTILINE | wx.VSCROLL)
    self.m_textCtrl2.SetFont(wx.Font(12, 70, 90, 90, False, &#34;杨任东竹石体-Regular&#34;))
    self.m_textCtrl2.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHTTEXT))

    sbSizer1.Add(self.m_textCtrl2, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL | wx.EXPAND, 5)

    bSizer1.Add(sbSizer1, 1, wx.EXPAND, 5)

    self.SetSizer(bSizer1)
    self.Layout()

    self.Centre(wx.BOTH)

    # Connect Events
    self.m_dirPicker1.Bind(wx.EVT_DIRPICKER_CHANGED, self.select_path)
    self.m_button1.Bind(wx.EVT_BUTTON, self.download)
    self.m_button2.Bind(wx.EVT_BUTTON, self.reset)
    self.m_button6.Bind(wx.EVT_BUTTON, self.clear)

def __del__(self):
    pass

# Virtual event handlers, overide them in your derived class
def select_path(self, event):
    event.Skip()

def download(self, event):
    event.Skip()

def reset(self, event):
    event.Skip()

def clear(self, event):
    event.Skip()</code></pre></div></div><p><strong>download_music.py</strong></p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-javascript"><code class="language-javascript" style="margin-left:0"># coding: utf-8

"""
author: 我是冰霜
describe: 爬虫网易云音乐歌单
create_time: 2020/03/07
"""

from common.Net_music_GUI import MyFrame1
import wx
import requests
import re
import os
import time
from requests.exceptions import RequestException

base_url = "http://music.163.com/song/media/outer/url?id=" # 定义一个全局变量,该链接为下载url前缀,id为歌曲唯一的id值
headers={
"authority": "music.163.com",
"method": "GET",
"path": "/",
"scheme": "https",
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3",
"accept-encoding": "gzip,deflate,br",
"accept-language": "zh-CN,zh;q=0.9",
"cache-control": "max-age=0",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.157 Safari/537.36"
}

class NetMusic(MyFrame1):

@staticmethod
def get_html_text(url, data=None, header=None, method=None, cookies=None):
    &#34;&#34;&#34;获取一个url的html格式文本内容&#34;&#34;&#34;

    if method == &#34;get&#34;:
        response = requests.get(url=url, params=data, headers=header, cookies=cookies, timeout=10)
    else:
        response = requests.post(url=url, data=data, headers=header, cookies=cookies, timeout=10)
    try:
        if response.status_code == 200:
            response.encoding = response.apparent_encoding
            # print(response.status_code)
            # print(response.text)
            return response.text
        return None
    except RequestException:
        print(&#34;请求失败&#34;)
        return None

@staticmethod
def get_content(url):
    &#34;&#34;&#34;请求最终下载文件的url,返回二进制内容&#34;&#34;&#34;
    # print(&#34;正在下载&#34;, url)
    try:
        r = requests.get(url, timeout=10)
        if r.status_code == 200:
            return r.content
        else:
            print(&#34;请求连接失败,url为:%s&#34; % url)
    except RequestException:
        return None

def parse_html(self, request_url):
    &#34;&#34;&#34;解析歌单页面,提取元素&#34;&#34;&#34;
    global headers
    html_text = self.get_html_text(url=request_url, header=headers, method=&#34;get&#34;)  # 调用get_html_text()方法,获取歌单页面响应内容
    # print(html_text)

    ###########使用正则表达式提取歌单名称、歌曲名称以及歌曲id############
    try:
        title = re.search(r&#39;&lt;title&gt;(.*?) -.*?&lt;/title&gt;&#39;, html_text).group(1)  # 匹配歌单名称
        # print(title)


        pattern_1 = re.compile(r&#39;&lt;li&gt;&lt;a.*?id=(\d+)&#34;&gt;&#39; # 匹配歌曲id
                             r&#39;(.*?)&lt;/a&gt;&#39;, re.S)  # 匹配歌曲名称
        musics = pattern_1.findall(html_text) # 查找所有结果,每组数据以一个元组形式,组成一个列表格式返回
        # print(musics)
        music_list = {
            &#34;title&#34;: title,
            &#34;music_list&#34;: musics
        }
        return music_list
    except Exception as e:
        print(&#34;请求歌单UR了出错,检查url是否正确,报错信息为:&#34;, e)

def download_music(self, music_url):
    &#34;&#34;&#34;下载文件至本地&#34;&#34;&#34;

    global base_url

    root_dir = self.m_dirPicker1.GetPath() # 获取GUI界面自定义选择的路径
        # os.path.dirname(os.path.abspath(&#39;.&#39;)) # 表示获取当前文件所在目录的上一级目录
    &#34;&#34;&#34;
    os.path.abspath(&#39;.&#39;), 获取当前文件所在路径;
    os.path.dirname(path),返回path的目录;
    &#34;&#34;&#34;
    music_data = self.parse_html(music_url)  # 调用parse_html()方法,获取歌单页面解析出来的数据

    title = music_data[&#34;title&#34;]  # 获取歌单名称
    # print(title)
    if not os.path.exists(root_dir + &#39;/music&#39;):
        os.makedirs(root_dir + &#39;/music&#39;)  # 在上一级目录下新建一个music文件夹
    if not os.path.exists(root_dir + &#34;/music/&#34; + title):
        os.makedirs(root_dir + &#34;/music/&#34; + title) # 在music下新建一个歌单目录
    # print(root_dir)

    music_list = music_data[&#34;music_list&#34;]
    # print(music_list)
    i = 1  # 标记位,表示第i首音乐
    j = 0  # 标记位,表示下载成功总个数
    k = 0  # 标记位,表示下载失败总个数
    # print(len(music_list)) # 获取歌单包含音乐总数
    print(&#34;当前歌单共有{}首音乐,开始下载******&#34;.format(len(music_list)))
    for music in music_list:

        music_url = base_url + music[0]
        music_name = music[1]
        try:
            file_path = root_dir + &#34;/music/&#34; + title + &#39;/&#39; + music_name + &#34;.mp3&#34;
            # print(mote_pics_collection_path + &#39;/&#39; + img.split(&#39;/&#39;)[-1])
            if not os.path.exists(file_path):  # 判断是否存在文件,不存在则爬取
                print(&#34;正在下载第{}首音乐:{}&#34;.format(i, &#34;《&#34;+ music_name +&#34;》&#34;))
                self.m_textCtrl2.AppendText(&#34;正在下载第{}首音乐:{}{}&#34;.format(i, &#34;《&#34;+ music_name +&#34;》&#34;, &#34;\n&#34;))  # 把日志追加到界面程序显示
                # print(self.get_content(music_url))
                try:
                    with open(file_path, &#39;wb&#39;) as f:
                        f.write(self.get_content(music_url))
                        f.close()
                    i = i+1
                    j = j+1

                except Exception as e:
                    print(&#34;遇到错误:&#34;, e)
                    print(&#34;第{}首下载失败,对应的歌曲url为:{}&#34;.format(i, music_url))
                    self.m_textCtrl2.AppendText(&#34;第{}首下载失败,对应的歌曲url为:{}{}&#34;.format(i, music_url, &#34;\n&#34;))
                    i = i+1
                    k = k+1

            elif os.path.exists(file_path):
                if os.path.getsize(file_path):
                    print(&#34;文件夹已经包含第{}首音乐:{}+{}&#34;.format(i, &#34;《&#34;+ music_name +&#34;》&#34;, &#34;\n&#34;))
                    self.m_textCtrl2.AppendText(&#34;文件夹已经包含第{}首音乐:{}{}&#34;.format(i, &#34;《&#34;+ music_name +&#34;》&#34;, &#34;\n&#34;))
                    i = i + 1
                else:
                    print(&#34;第{}首下载失败,对应的歌曲url为:{}&#34;.format(i, music_url))
                    self.m_textCtrl2.AppendText(&#34;第{}首下载失败,对应的歌曲url为:{}{}&#34;.format(i, music_url, &#34;\n&#34;))
                    i = i + 1
                    k = k+1


        except FileNotFoundError as e:
            j = j + 1
            print(&#34;遇到错误:&#34;, e)
            continue

    print(&#34;下载失败 %s 首&#34; % k)
    print(&#34;下载成功 %s 首&#34; % j)


def download(self, event):
    &#34;&#34;&#34;定义下载按钮回调方法&#34;&#34;&#34;
    url =self.m_textCtrl1.GetValue().replace(&#34;/#&#34;, &#34;&#34;) # 拿到url输入框的值,并去掉url中的/#符号

    if url:
        print(url)
        self.download_music(url)
    else:
        self.m_textCtrl1.SetValue(&#34;请输入url&#34;)

def reset(self, event):
    &#34;&#34;&#34;定义清空url输入框内容方法&#34;&#34;&#34;
    self.m_textCtrl1.Clear()

def clear(self, event):
    &#34;&#34;&#34;定义清空日志输出框的方法&#34;&#34;&#34;
    self.m_textCtrl2.Clear()

if name == 'main':
app = wx.App()
main_win = NetMusic(None)
main_win.Show()
app.MainLoop()

看一下最后的效果

备注:

到这一步还未结束,这里有个坑,因为这两天爬取次数过多,发现ip会暂时被封,所以这个程序用几次后就啥也爬不到了 所以后面得学一下如何添加ip代理池~