使用wxpython編寫一個網易雲音樂爬蟲程序


本次借助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、歌曲名稱,如下

 1     def parse_html(self, request_url):
 2         """解析歌單頁面,提取元素"""
 3         global headers
 4         html_text = self.get_html_text(url=request_url, header=headers, method="get")  # 調用get_html_text()方法,獲取歌單頁面響應內容
 5         # print(html_text)
 6 
 7         ###########使用正則表達式提取歌單名稱、歌曲名稱以及歌曲id############
 8         try:
 9             title = re.search(r'<title>(.*?) -.*?</title>', html_text).group(1)  # 匹配歌單名稱
10             # print(title)
11 
12 
13             pattern_1 = re.compile(r'<li><a.*?id=(\d+)">' # 匹配歌曲id
14                                  r'(.*?)</a>', re.S)  # 匹配歌曲名稱
15             musics = pattern_1.findall(html_text) # 查找所有結果,每組數據以一個元組形式,組成一個列表格式返回
16             # print(musics)
17             music_list = {
18                 "title": title,
19                 "music_list": musics
20             }
21             return music_list
22         except Exception as e:
23             print("請求歌單UR了出錯,檢查url是否正確,報錯信息為:", e)        

 3. 構造程序界面

因為這次要做一個界面程序,實現如下要求

  • 能夠自定義選擇保存路徑
  • 在界面輸入歌單url后,可以直接爬取其下歌曲
  • 下載過程能夠展示在界面中

以前寫的幾個界面工具都是用的python自帶的tkinter,這次試着用一下wxPython,看下效果如何

(1)確保自己的電腦中安裝了wxPython,這一步略過,貼幾個學習網站

痞子衡嵌入式:極易上手的可視化wxPython GUI構建工具(wxFormBuilder) - 痞子衡 - 博客園

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個按鈕,下面是對應的事件回調代碼

 1     def download(self, event):
 2         """定義下載按鈕回調方法"""
 3         url =self.m_textCtrl1.GetValue().replace("/#", "") # 拿到url輸入框的值,並去掉url中的/#符號
 4 
 5         if url:
 6             print(url)
 7             self.download_music(url)
 8         else:
 9             self.m_textCtrl1.SetValue("請輸入url")
10 
11     def reset(self, event):
12         """定義清空url輸入框內容方法"""
13         self.m_textCtrl1.Clear()
14 
15     def clear(self, event):
16         """定義清空日志輸出框的方法"""
17         self.m_textCtrl2.Clear()

還有一點需要說一下,因為是自定義保存路徑,所以需要拿到界面工具自選的路徑

wxDirPickerCtrl有一個方法 GetPath(),可以獲取當前顯示的路徑值

root_dir = self.m_dirPicker1.GetPath() # 獲取GUI界面自定義選擇的路徑

貼一下完整代碼

Net_Music_GUI.py

  1 # -*- coding: utf-8 -*-
  2 
  3 ###########################################################################
  4 ## Python code generated with wxFormBuilder (version Jun 17 2015)
  5 ## http://www.wxformbuilder.org/
  6 ##
  7 ## PLEASE DO "NOT" EDIT THIS FILE!
  8 ###########################################################################
  9 
 10 import wx
 11 import wx.xrc
 12 
 13 
 14 ###########################################################################
 15 ## Class MyFrame1
 16 ###########################################################################
 17 
 18 class MyFrame1(wx.Frame):
 19     def __init__(self, parent):
 20         wx.Frame.__init__(self, parent, id=wx.ID_ANY, title=u"網易雲音樂爬蟲程序-by 我是冰霜", pos=wx.DefaultPosition,
 21                           size=wx.Size(579, 592), style=wx.DEFAULT_FRAME_STYLE | wx.TAB_TRAVERSAL)
 22 
 23         self.SetSizeHints(wx.DefaultSize, wx.DefaultSize)
 24         self.SetForegroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNTEXT))
 25         self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW))
 26 
 27         bSizer1 = wx.BoxSizer(wx.VERTICAL)
 28 
 29         self.m_staticText1 = wx.StaticText(self, wx.ID_ANY, u"請輸入歌單鏈接", wx.DefaultPosition, wx.DefaultSize, 0)
 30         self.m_staticText1.Wrap(-1)
 31         self.m_staticText1.SetFont(wx.Font(15, 70, 90, 90, False, "站酷小薇LOGO體"))
 32         self.m_staticText1.SetForegroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNTEXT))
 33         self.m_staticText1.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW))
 34 
 35         bSizer1.Add(self.m_staticText1, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 5)
 36 
 37         self.m_textCtrl1 = wx.TextCtrl(self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.Size(500, 30), 0)
 38         self.m_textCtrl1.SetFont(wx.Font(12, 70, 90, 90, False, wx.EmptyString))
 39 
 40         bSizer1.Add(self.m_textCtrl1, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL | wx.EXPAND, 5)
 41 
 42         self.m_dirPicker1 = wx.DirPickerCtrl(self, wx.ID_ANY, wx.EmptyString, u"Select a folder", wx.DefaultPosition,
 43                                              wx.Size(300, -1), wx.DIRP_DEFAULT_STYLE)
 44         bSizer1.Add(self.m_dirPicker1, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL | wx.EXPAND, 5)
 45 
 46         self.m_panel2 = wx.Panel(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL)
 47         gSizer1 = wx.GridSizer(0, 2, 0, 0)
 48 
 49         self.m_button1 = wx.Button(self.m_panel2, wx.ID_ANY, u"下載", wx.DefaultPosition, wx.DefaultSize, 0)
 50         self.m_button1.SetFont(wx.Font(12, 70, 90, 90, False, "站酷小薇LOGO體"))
 51 
 52         gSizer1.Add(self.m_button1, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 5)
 53 
 54         self.m_button2 = wx.Button(self.m_panel2, wx.ID_ANY, u"重置", wx.DefaultPosition, wx.DefaultSize, 0)
 55         self.m_button2.SetFont(wx.Font(12, 70, 90, 90, False, "站酷小薇LOGO體"))
 56 
 57         gSizer1.Add(self.m_button2, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 5)
 58 
 59         self.m_panel2.SetSizer(gSizer1)
 60         self.m_panel2.Layout()
 61         gSizer1.Fit(self.m_panel2)
 62         bSizer1.Add(self.m_panel2, 1, wx.ALL | wx.EXPAND, 5)
 63 
 64         sbSizer1 = wx.StaticBoxSizer(wx.StaticBox(self, wx.ID_ANY, u"結果展示區"), wx.VERTICAL)
 65 
 66         self.m_button6 = wx.Button(sbSizer1.GetStaticBox(), wx.ID_ANY, u"清空", wx.DefaultPosition, wx.DefaultSize, 0)
 67         self.m_button6.SetFont(wx.Font(12, 70, 90, 90, False, "站酷小薇LOGO體"))
 68 
 69         sbSizer1.Add(self.m_button6, 0, wx.ALL, 5)
 70 
 71         self.m_textCtrl2 = wx.TextCtrl(sbSizer1.GetStaticBox(), wx.ID_ANY, wx.EmptyString, wx.DefaultPosition,
 72                                        wx.Size(500, 600), wx.TE_CHARWRAP | wx.TE_MULTILINE | wx.VSCROLL)
 73         self.m_textCtrl2.SetFont(wx.Font(12, 70, 90, 90, False, "楊任東竹石體-Regular"))
 74         self.m_textCtrl2.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHTTEXT))
 75 
 76         sbSizer1.Add(self.m_textCtrl2, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL | wx.EXPAND, 5)
 77 
 78         bSizer1.Add(sbSizer1, 1, wx.EXPAND, 5)
 79 
 80         self.SetSizer(bSizer1)
 81         self.Layout()
 82 
 83         self.Centre(wx.BOTH)
 84 
 85         # Connect Events
 86         self.m_dirPicker1.Bind(wx.EVT_DIRPICKER_CHANGED, self.select_path)
 87         self.m_button1.Bind(wx.EVT_BUTTON, self.download)
 88         self.m_button2.Bind(wx.EVT_BUTTON, self.reset)
 89         self.m_button6.Bind(wx.EVT_BUTTON, self.clear)
 90 
 91     def __del__(self):
 92         pass
 93 
 94     # Virtual event handlers, overide them in your derived class
 95     def select_path(self, event):
 96         event.Skip()
 97 
 98     def download(self, event):
 99         event.Skip()
100 
101     def reset(self, event):
102         event.Skip()
103 
104     def clear(self, event):
105         event.Skip()
View Code

download_music.py

  1 # coding: utf-8
  2 """
  3 author: 我是冰霜
  4 describe: 爬蟲網易雲音樂歌單
  5 create_time: 2020/03/07
  6 """
  7 
  8 from common.Net_music_GUI import MyFrame1
  9 import wx
 10 import requests
 11 import re
 12 import os
 13 import time
 14 from requests.exceptions import RequestException
 15 
 16 
 17 base_url = "http://music.163.com/song/media/outer/url?id=" # 定義一個全局變量,該鏈接為下載url前綴,id為歌曲唯一的id值
 18 headers={
 19         "authority": "music.163.com",
 20         "method": "GET",
 21         "path": "/",
 22         "scheme": "https",
 23         "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3",
 24         "accept-encoding": "gzip,deflate,br",
 25         "accept-language": "zh-CN,zh;q=0.9",
 26         "cache-control": "max-age=0",
 27         "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"
 28     }
 29 
 30 class NetMusic(MyFrame1):
 31 
 32     @staticmethod
 33     def get_html_text(url, data=None, header=None, method=None, cookies=None):
 34         """獲取一個url的html格式文本內容"""
 35 
 36         if method == "get":
 37             response = requests.get(url=url, params=data, headers=header, cookies=cookies, timeout=10)
 38         else:
 39             response = requests.post(url=url, data=data, headers=header, cookies=cookies, timeout=10)
 40         try:
 41             if response.status_code == 200:
 42                 response.encoding = response.apparent_encoding
 43                 # print(response.status_code)
 44                 # print(response.text)
 45                 return response.text
 46             return None
 47         except RequestException:
 48             print("請求失敗")
 49             return None
 50 
 51     @staticmethod
 52     def get_content(url):
 53         """請求最終下載文件的url,返回二進制內容"""
 54         # print("正在下載", url)
 55         try:
 56             r = requests.get(url, timeout=10)
 57             if r.status_code == 200:
 58                 return r.content
 59             else:
 60                 print("請求連接失敗,url為:%s" % url)
 61         except RequestException:
 62             return None
 63 
 64     def parse_html(self, request_url):
 65         """解析歌單頁面,提取元素"""
 66         global headers
 67         html_text = self.get_html_text(url=request_url, header=headers, method="get")  # 調用get_html_text()方法,獲取歌單頁面響應內容
 68         # print(html_text)
 69 
 70         ###########使用正則表達式提取歌單名稱、歌曲名稱以及歌曲id############
 71         try:
 72             title = re.search(r'<title>(.*?) -.*?</title>', html_text).group(1)  # 匹配歌單名稱
 73             # print(title)
 74 
 75 
 76             pattern_1 = re.compile(r'<li><a.*?id=(\d+)">' # 匹配歌曲id
 77                                  r'(.*?)</a>', re.S)  # 匹配歌曲名稱
 78             musics = pattern_1.findall(html_text) # 查找所有結果,每組數據以一個元組形式,組成一個列表格式返回
 79             # print(musics)
 80             music_list = {
 81                 "title": title,
 82                 "music_list": musics
 83             }
 84             return music_list
 85         except Exception as e:
 86             print("請求歌單UR了出錯,檢查url是否正確,報錯信息為:", e)
 87 
 88     def download_music(self, music_url):
 89         """下載文件至本地"""
 90 
 91         global base_url
 92 
 93         root_dir = self.m_dirPicker1.GetPath() # 獲取GUI界面自定義選擇的路徑
 94             # os.path.dirname(os.path.abspath('.')) # 表示獲取當前文件所在目錄的上一級目錄
 95         """
 96         os.path.abspath('.'), 獲取當前文件所在路徑;
 97         os.path.dirname(path),返回path的目錄;
 98         """
 99         music_data = self.parse_html(music_url)  # 調用parse_html()方法,獲取歌單頁面解析出來的數據
100 
101         title = music_data["title"]  # 獲取歌單名稱
102         # print(title)
103         if not os.path.exists(root_dir + '/music'):
104             os.makedirs(root_dir + '/music')  # 在上一級目錄下新建一個music文件夾
105         if not os.path.exists(root_dir + "/music/" + title):
106             os.makedirs(root_dir + "/music/" + title) # 在music下新建一個歌單目錄
107         # print(root_dir)
108 
109         music_list = music_data["music_list"]
110         # print(music_list)
111         i = 1  # 標記位,表示第i首音樂
112         j = 0  # 標記位,表示下載成功總個數
113         k = 0  # 標記位,表示下載失敗總個數
114         # print(len(music_list)) # 獲取歌單包含音樂總數
115         print("當前歌單共有{}首音樂,開始下載******".format(len(music_list)))
116         for music in music_list:
117 
118             music_url = base_url + music[0]
119             music_name = music[1]
120             try:
121                 file_path = root_dir + "/music/" + title + '/' + music_name + ".mp3"
122                 # print(mote_pics_collection_path + '/' + img.split('/')[-1])
123                 if not os.path.exists(file_path):  # 判斷是否存在文件,不存在則爬取
124                     print("正在下載第{}首音樂:{}".format(i, ""+ music_name +""))
125                     self.m_textCtrl2.AppendText("正在下載第{}首音樂:{}{}".format(i, ""+ music_name +"", "\n"))  # 把日志追加到界面程序顯示
126                     # print(self.get_content(music_url))
127                     try:
128                         with open(file_path, 'wb') as f:
129                             f.write(self.get_content(music_url))
130                             f.close()
131                         i = i+1
132                         j = j+1
133 
134                     except Exception as e:
135                         print("遇到錯誤:", e)
136                         print("第{}首下載失敗,對應的歌曲url為:{}".format(i, music_url))
137                         self.m_textCtrl2.AppendText("第{}首下載失敗,對應的歌曲url為:{}{}".format(i, music_url, "\n"))
138                         i = i+1
139                         k = k+1
140 
141                 elif os.path.exists(file_path):
142                     if os.path.getsize(file_path):
143                         print("文件夾已經包含第{}首音樂:{}+{}".format(i, ""+ music_name +"", "\n"))
144                         self.m_textCtrl2.AppendText("文件夾已經包含第{}首音樂:{}{}".format(i, ""+ music_name +"", "\n"))
145                         i = i + 1
146                     else:
147                         print("第{}首下載失敗,對應的歌曲url為:{}".format(i, music_url))
148                         self.m_textCtrl2.AppendText("第{}首下載失敗,對應的歌曲url為:{}{}".format(i, music_url, "\n"))
149                         i = i + 1
150                         k = k+1
151 
152 
153             except FileNotFoundError as e:
154                 j = j + 1
155                 print("遇到錯誤:", e)
156                 continue
157 
158         print("下載失敗 %s 首" % k)
159         print("下載成功 %s 首" % j)
160 
161 
162     def download(self, event):
163         """定義下載按鈕回調方法"""
164         url =self.m_textCtrl1.GetValue().replace("/#", "") # 拿到url輸入框的值,並去掉url中的/#符號
165 
166         if url:
167             print(url)
168             self.download_music(url)
169         else:
170             self.m_textCtrl1.SetValue("請輸入url")
171 
172     def reset(self, event):
173         """定義清空url輸入框內容方法"""
174         self.m_textCtrl1.Clear()
175 
176     def clear(self, event):
177         """定義清空日志輸出框的方法"""
178         self.m_textCtrl2.Clear()
179 
180 if __name__ == '__main__':
181     app = wx.App()
182     main_win = NetMusic(None)
183     main_win.Show()
184     app.MainLoop()
View Code

 看一下最后的效果


 備注:

到這一步還未結束,這里有個坑,因為這兩天爬取次數過多,發現ip會暫時被封,所以這個程序用幾次后就啥也爬不到了

所以后面得學一下如何添加ip代理池~

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM