一、主题式网络爬虫设计方案(15分)
1.主题式网络爬虫名称
爬取网易云歌单信息
2.主题式网络爬虫爬取的内容与数据特征分析
内容:
网易云歌单信息
数据特征:
歌单名称、歌单初创时间、用户名称、用户链接、收藏数量、分享的数量、评论的数量、歌单的标签、播放量
3.主题式网络爬虫设计方案概述(包括实现思路与技术难点)
在获取歌单列表时还需要进一步进入歌单详情进行爬取歌单信息
二、主题页面的结构特征分析(15分)
1.主题页面的结构特征
列表页-详情页
2.Htmls页面解析
列表页解析出详情页地址

 详情页解析出歌单信息


3.节点(标签)查找方法与遍历方法
(必要时画出节点树结构)
 使用类选择器找到对应的标签,然后进行地址拼接
eg:获取所有歌单列表
ul = soup.select('#m-pl-container > li')
 获取歌单名称:
soup.select('.cntc > .hd h2')[0].string
三、网络爬虫程序设计(60分)
爬虫程序主体要包括以下各部分,要附源代码及较详细注释,并在每部分程序后面提供输出结果的截图。
代码
  1 # coding:utf-8
  2 import hashlib
  3 import requests
  4 import chardet
  5 from bs4 import BeautifulSoup
  6 from selenium import webdriver
  7 import re
  8 import pymysql as ps
  9 import pandas as pd
 10 import matplotlib.pyplot as plt
 11 import numpy as np
 12
 13 class FormHotspot(object):
 14     def __init__(self):
 15         self.new_craw_url = set()
 16         self.old_craw_url = set()
 17         # 无头启动 selenium
 18         opt = webdriver.chrome.options.Options()
 19         opt.set_headless()
 20         self.browser = webdriver.Chrome(chrome_options=opt)
 21         self.host = 'localhost'
 22         self.user = 'root'
 23         self.password = ''
 24         self.database = 'wyy'
 25         self.con = None
 26         self.curs = None
 27
 28     '''
 29     爬取
 30     '''
 31     def craw(self, url):
 32         print("-----歌单列表根地址:%s" % url)
 33         # 歌单内列表下载
 34         downSongList = self.down_song_list(url)
 35         # 解析歌单列表
 36         songList = self.parser_song_list(downSongList)
 37         # 增加新的地址
 38         for new_url in songList:
 39             self.add_new_craw_url(new_url)
 40         # ----------------------- 歌单部分-----------------------
 41         oldSize = self.new_craw_url_size()
 42         print("》》》预计有%s个歌单待爬取"%oldSize)
 43         success = 0
 44         while (self.has_new_craw_url()):
 45             try:
 46                 # -----------下载
 47                 new_url = self.get_new_craw_url()
 48                 print("--------------歌单地址:"+new_url+":开始爬取")
 49                 songHtml = self.down_song(new_url)
 50                 # -----------解析
 51                 data = self.parser_song(songHtml)
 52                 # -----------保存
 53                 success = success + self.keep_song(data)
 54             except:
 55                 print("操作歌单出现错误出错")
 56         # 展示
 57         print("》》》%s个歌单爬取失败!"%(success-oldSize))
 58         print("》》》%s个歌单爬取成功!" % success)
 59         df = self.get_song()
 60         # 当前歌单信息对照表
 61         print("*************************************************")
 62         print("*************当前歌单id-name对照表****************")
 63         print(df[['id','name']])
 64         self.show_song(df)
 65
 66     '''
 67     ----------------------- 歌单详情页 start -----------------------------
 68     '''
 69     '''
 70     歌单信息下载
 71     '''
 72     def down_song(self,url):
 73         try:
 74             if url is None:
 75                 return None
 76             s = requests.session()
 77             header = {
 78                 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36',
 79                 "Referer": "https://music.163.com/"
 80             }
 81             self.browser.get(url)
 82             self.browser.switch_to.frame('g_iframe')
 83             html = self.browser.page_source
 84             return html
 85         except:
 86             print("----------------down song failed--------------")
 87             return None
 88
 89     '''
 90     歌单信息解析
 91     '''
 92     def parser_song(self,html):
 93         if html is None:
 94             return None
 95         soup = BeautifulSoup(html,"html.parser")
 96         # 歌单名称
 97         name = soup.select('.cntc > .hd h2')[0].string
 98         # 歌单初创时间
 99         createTime = soup.select('.cntc > div.user.f-cb > span.time.s-fc4')[0].string #标签的内容
100         createTime = self.parser_int_and_str(createTime) #转化为只有数字的字符串列表
101         createTime = self.to_data(createTime) #拼接成日期格式字符串 2019-11-11
102         # 用户名称
103         userName = soup.select('.cntc > div.user.f-cb > span.name > a')[0].string
104         # 用户链接
105         userLink = "https://music.163.com/#" + soup.select('.cntc > div.user.f-cb > span.name > a')[0]['href']
106         # 收藏数量
107         collectNum = soup.select('#content-operation > a.u-btni.u-btni-fav > i')[0].string
108         collectNum = self.parser_int_and_str(collectNum)[0] #转化为数字的字符串
109         # 分享的数量
110         shareNum = soup.select('#content-operation > a.u-btni.u-btni-share > i')[0].string
111         shareNum = self.parser_int_and_str(shareNum)[0]
112         # 评论的数量 #auto-id-WVwlyAZTzbf1caK > div.cnt > div > div.tags.f-cb > a:nth-child(2) > i
113         commentNum = soup.select('#cnt_comment_count')[0].string
114         # 歌单的标签
115         iList = soup.select('.cntc > div.tags.f-cb i')
116         lable = []
117         for l in iList:
118             lable.append(l.string)
119         lable = self.to_lable(lable) #拼接标签
120         # 播放量
121         playbackVolume = soup.select('#play-count')[0].string
122         return {'name':name, 'createTime':createTime, 'userName':userName, 'userLink':userLink, 'collectNum':collectNum, 'shareNum':shareNum, 'commentNum':commentNum, 'lable':lable, 'playbackVolume':playbackVolume}
123
124     '''
125     歌单信息保存
126     '''
127     def keep_song(self,data):
128         try:
129             self.open_mysql()
130             sql = "insert into song (name,create_time,user_name,user_link,collect_num,share_num,comment_num,lable,playback_volume) values (%s,%s,%s,%s,%s,%s,%s,%s,%s)"
131             params = (data['name'],data['createTime'],data['userName'],data['userLink'],data['collectNum'],data['shareNum'],data['commentNum'],data['lable'],data['playbackVolume'])
132             row = self.curs.execute(sql, params)
133             self.con.commit()
134             self.close_mysql()
135             return row
136         except:
137             print("插入data:%s \n失败!"%data)
138             self.con.rollback()
139             self.close_mysql()
140             return 0
141
142     '''
143     歌单信息获取
144     '''
145     def get_song(self):
146         self.open_mysql()
147         sql = sql = "select * from song order by id asc"
148         try:
149             df = pd.read_sql(sql=sql,con=self.con)
150             self.close_mysql()
151             return df
152         except:
153             print('获取数据失败!')
154             self.close_mysql()
155             return None
156
157     '''
158     歌单信息显示
159     '''
160     def show_song(self,data):
161         id = data['id']
162         collectNum = data['collect_num']
163         shareNum = data['share_num']
164         commentNum = data['comment_num']
165         playbackVolume = data['playback_volume']
166         # x轴刻度最小值
167         xmin = (id[id.idxmin()] - 1)
168         xmax = (id[id.idxmax()] + 1)
169         # 设置坐标轴刻度
170         my_x_ticks = np.arange(xmin, xmax, 1)
171         plt.figure()
172         # 显示中文标签
173         plt.rcParams['font.sans-serif'] = ['SimHei']
174         # -------------------- 歌单的收藏数 ------------------------
175         axes1 = plt.subplot(2,2,1) #子图
176         plt.scatter(id, collectNum)
177         # 设置坐标轴范围
178         plt.xlim((xmin,xmax))
179         plt.xticks(my_x_ticks)
180         plt.xlabel("歌单id")
181         plt.ylabel("收藏数量")
182         plt.grid()
183         plt.title("网易云歌单 收藏数量")
184         axes2 = plt.subplot(2, 2, 2)
185         plt.scatter(id, shareNum)
186         # 设置坐标轴范围
187         plt.xlim((xmin, xmax))
188         plt.xticks(my_x_ticks)
189         plt.xlabel("歌单id")
190         plt.ylabel("分享数量")
191         plt.grid()
192         plt.title("网易云歌单 分享数量")
193         axes3 = plt.subplot(2, 2, 3)
194         plt.scatter(id, commentNum)
195         # 设置坐标轴范围
196         plt.xlim((xmin, xmax))
197         plt.xticks(my_x_ticks)
198         plt.xlabel("歌单id")
199         plt.ylabel("评论数量")
200         plt.grid()
201         plt.title("网易云歌单 评论数量")
202         axes4 = plt.subplot(2, 2, 4)
203         plt.scatter(id, playbackVolume)
204         # 设置坐标轴范围
205         plt.xlim((xmin, xmax))
206         plt.xticks(my_x_ticks)
207         plt.xlabel("歌单id")
208         plt.ylabel("播放量")
209         plt.grid()
210         plt.title("网易云歌单 播放量")
211         plt.show()
212
213     '''
214     正则表达式匹配整数
215     '''
216     def parser_int_and_str(self,str):
217         pattern = re.compile(r'[\d]+')  # 查找正数字
218         result = pattern.findall(str)
219         return result
220
221     '''
222     拼接成日期
223     '''
224     def to_data(self,data):
225         if data is None:
226             return None
227         result = ''
228         # 列表生成式
229         b = [str(i) for i in data]
230         result = "-".join(b)
231         return result
232
233     '''
234     拼接标签
235     '''
236     def to_lable(self,data):
237         if data is None:
238             return None
239         result = ''
240         # 列表生成式
241         b = [str(i) for i in data]
242         result = "/".join(b)
243         return result
244
245     '''
246         ----------------------- 歌单详情页 end -----------------------------
247         ----------------------- 歌单列表 start -----------------------------
248     '''
249
250     '''
251     歌单列表下载
252     '''
253     def down_song_list(self, url):
254         try:
255             if url is None:
256                 return None
257             s = requests.session()
258             header = {
259                 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36',
260                 "Referer": "https://music.163.com/"
261             }
262             self.browser.get(url)
263             self.browser.switch_to.frame('g_iframe')
264             html = self.browser.page_source
265             return html
266         except:
267             print("----------------down song list failed--------------")
268             return None
269
270
271     '''
272     解析歌单列表
273     '''
274     def parser_song_list(self, cont):
275         try:
276             if cont is None:
277                 return None
278             soup = BeautifulSoup(cont, "html.parser")
279             ul = soup.select('#m-pl-container > li')
280             new_urls = set()
281             for li in ul:
282                 url = "https://music.163.com/#" + li.find('a')['href']
283                 new_urls.add(url)
284             return new_urls
285         except:
286             print("---------------------parser failed--------------------")
287
288     '''
289         ----------------------- 歌单列表 end -----------------------------
290     '''
291
292     '''
293     ----------------------- 地址管理 start ---------------------------------
294     增加一个待爬取的地址
295     '''
296     def add_new_craw_url(self, url):
297         if url is None:
298             return
299         if url not in self.new_craw_url and url not in self.old_craw_url:
300             self.new_craw_url.add(url)
301
302
303     '''
304     获取一个待爬取地址
305     '''
306     def get_new_craw_url(self):
307         if self.has_new_craw_url():
308             new_craw_url = self.new_craw_url.pop()
309             self.old_craw_url.add(new_craw_url)
310             return new_craw_url
311         else:
312             return None
313
314
315     '''
316     在新地址集合中是否有待爬取地址
317     '''
318     def has_new_craw_url(self):
319         return self.new_craw_url_size() != 0
320
321
322     '''
323     未爬取的地址的数量
324     '''
325     def new_craw_url_size(self):
326         return len(self.new_craw_url)
327
328
329     '''
330     被加密数据的长度不管为多少,经过md5加密后得到的16进制的数据,它的长度是固定为32的。
331     '''
332     def encryption_md5(self, data, password):
333         """
334         由于hash不处理unicode编码的字符串(python3默认字符串是unicode)
335         所以这里判断是否字符串,如果是则进行转码
336         初始化md5、将url进行加密、然后返回加密字串
337         """
338         # 创建md5对象
339         m = hashlib.md5()
340         b = data.encode(encoding='utf-8')
341         m.update(b)
342         return m.hexdigest()
343
344
345     '''
346     md5解密 暴力破解
347     '''
348     def decrypt_md5(self, data, password):
349         pass
350
351     '''
352         ----------------------- 地址管理 end ---------------------------------
353     '''
354
355     '''
356         ----------------------- 数据库 start ---------------------------------
357     '''
358     def open_mysql(self):
359         self.con = ps.connect(host=self.host, user=self.user, password=self.password, database=self.database)
360         self.curs = self.con.cursor()
361
362     # 数据库关闭
363     def close_mysql(self):
364         self.curs.close()
365         self.con.close()
366     '''
367         ----------------------- 数据库 end ---------------------------------
368     '''
369
370
371 '''
372 当没有其他类调用自己执行时使用
373 '''
374 if __name__ == '__main__':
375     formHotspot = FormHotspot()
376     form_url = "https://music.163.com/#/discover/playlist"
377     print('********************************************************************************** ^ ^start *******************************************************************************')
378     formHotspot.craw(form_url)
379     print('********************************************************************************** ^ ^Finished *******************************************************************************')

数据库结构

1.数据爬取与采集
歌单地址下载:
'''
    歌单列表下载
    '''
    def down_song_list(self, url):
        try:
            if url is None:
                return None
            s = requests.session()
            header = {
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36',
                "Referer": "https://music.163.com/"
            }
            self.browser.get(url)
            self.browser.switch_to.frame('g_iframe')
            html = self.browser.page_source
            return html
        except:
            print("----------------down song list failed--------------")
            return None
歌单信息下载:
'''
    歌单信息下载
    '''
    def down_song(self,url):
        try:
            if url is None:
                return None
            s = requests.session()
            header = {
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36',
                "Referer": "https://music.163.com/"
            }
            self.browser.get(url)
            self.browser.switch_to.frame('g_iframe')
            html = self.browser.page_source
            return html
        except:
            print("----------------down song failed--------------")
            return None
2.对数据进行清洗和处理
歌单列表详情地址解析:
'''
    解析歌单列表
    '''
    def parser_song_list(self, cont):
        try:
            if cont is None:
                return None
            soup = BeautifulSoup(cont, "html.parser")
            ul = soup.select('#m-pl-container > li')
            new_urls = set()
            for li in ul:
                url = "https://music.163.com/#" + li.find('a')['href']
                new_urls.add(url)
            return new_urls
        except:
            print("---------------------parser failed--------------------")
歌单信息解析:
'''
    歌单信息解析
    '''
    def parser_song(self,html):
        if html is None:
            return None
        soup = BeautifulSoup(html,"html.parser")
        # 歌单名称
        name = soup.select('.cntc > .hd h2')[0].string
        # 歌单初创时间
        createTime = soup.select('.cntc > div.user.f-cb > span.time.s-fc4')[0].string #标签的内容
        createTime = self.parser_int_and_str(createTime) #转化为只有数字的字符串列表
        createTime = self.to_data(createTime) #拼接成日期格式字符串 2019-11-11
        # 用户名称
        userName = soup.select('.cntc > div.user.f-cb > span.name > a')[0].string
        # 用户链接
        userLink = "https://music.163.com/#" + soup.select('.cntc > div.user.f-cb > span.name > a')[0]['href']
        # 收藏数量
        collectNum = soup.select('#content-operation > a.u-btni.u-btni-fav > i')[0].string
        collectNum = self.parser_int_and_str(collectNum)[0] #转化为数字的字符串
        # 分享的数量
        shareNum = soup.select('#content-operation > a.u-btni.u-btni-share > i')[0].string
        shareNum = self.parser_int_and_str(shareNum)[0]
        # 评论的数量 #auto-id-WVwlyAZTzbf1caK > div.cnt > div > div.tags.f-cb > a:nth-child(2) > i
        commentNum = soup.select('#cnt_comment_count')[0].string
        # 歌单的标签
        iList = soup.select('.cntc > div.tags.f-cb i')
        lable = []
        for l in iList:
            lable.append(l.string)
        lable = self.to_lable(lable) #拼接标签
        # 播放量
        playbackVolume = soup.select('#play-count')[0].string
        return {'name':name, 'createTime':createTime, 'userName':userName, 'userLink':userLink, 'collectNum':collectNum, 'shareNum':shareNum, 'commentNum':commentNum, 'lable':lable, 'playbackVolume':playbackVolume}
3.数据分析与可视化
(例如:数据柱形图、直方图、散点图、盒图、分布图、数据回归分析等)
'''
    歌单信息显示
    '''
    def show_song(self,data):
        id = data['id']
        collectNum = data['collect_num']
        shareNum = data['share_num']
        commentNum = data['comment_num']
        playbackVolume = data['playback_volume']
        # x轴刻度最小值
        xmin = (id[id.idxmin()] - 1)
        xmax = (id[id.idxmax()] + 1)
        # 设置坐标轴刻度
        my_x_ticks = np.arange(xmin, xmax, 1)
        plt.figure()
        # 显示中文标签
        plt.rcParams['font.sans-serif'] = ['SimHei']
        # -------------------- 歌单的收藏数 ------------------------
        axes1 = plt.subplot(2,2,1) #子图
        plt.scatter(id, collectNum)
        # 设置坐标轴范围
        plt.xlim((xmin,xmax))
        plt.xticks(my_x_ticks)
        plt.xlabel("歌单id")
        plt.ylabel("收藏数量")
        plt.grid()
        plt.title("网易云歌单 收藏数量")
        axes2 = plt.subplot(2, 2, 2)
        plt.scatter(id, shareNum)
        # 设置坐标轴范围
        plt.xlim((xmin, xmax))
        plt.xticks(my_x_ticks)
        plt.xlabel("歌单id")
        plt.ylabel("分享数量")
        plt.grid()
        plt.title("网易云歌单 分享数量")
        axes3 = plt.subplot(2, 2, 3)
        plt.scatter(id, commentNum)
        # 设置坐标轴范围
        plt.xlim((xmin, xmax))
        plt.xticks(my_x_ticks)
        plt.xlabel("歌单id")
        plt.ylabel("评论数量")
        plt.grid()
        plt.title("网易云歌单 评论数量")
        axes4 = plt.subplot(2, 2, 4)
        plt.scatter(id, playbackVolume)
        # 设置坐标轴范围
        plt.xlim((xmin, xmax))
        plt.xticks(my_x_ticks)
        plt.xlabel("歌单id")
        plt.ylabel("播放量")
        plt.grid()
        plt.title("网易云歌单 播放量")
        plt.show()

 4.数据持久化
 
    '''
    歌单信息保存
    '''
    def keep_song(self,data):
        try:
            self.open_mysql()
            sql = "insert into song (name,create_time,user_name,user_link,collect_num,share_num,comment_num,lable,playback_volume) values (%s,%s,%s,%s,%s,%s,%s,%s,%s)"
            params = (data['name'],data['createTime'],data['userName'],data['userLink'],data['collectNum'],data['shareNum'],data['commentNum'],data['lable'],data['playbackVolume'])
            row = self.curs.execute(sql, params)
            self.con.commit()
            self.close_mysql()
            return row
        except:
            print("插入data:%s \n失败!"%data)
            self.con.rollback()
            self.close_mysql()
            return 0
四、结论(10分)
1.经过对主题数据的分析与可视化,可以得到哪些结论?
2.对本次程序设计任务完成的情况做一个简单的小结。
12-24 08:49