一、主题式网络爬虫设计方案(15分)
1.主题式网络爬虫名称
爬取网易云歌单信息
2.主题式网络爬虫爬取的内容与数据特征分析
2.主题式网络爬虫爬取的内容与数据特征分析
内容:
网易云歌单信息
数据特征:
歌单名称、歌单初创时间、用户名称、用户链接、收藏数量、分享的数量、评论的数量、歌单的标签、播放量
3.主题式网络爬虫设计方案概述(包括实现思路与技术难点)
3.主题式网络爬虫设计方案概述(包括实现思路与技术难点)
在获取歌单列表时还需要进一步进入歌单详情进行爬取歌单信息
二、主题页面的结构特征分析(15分)
1.主题页面的结构特征
1.主题页面的结构特征
列表页-详情页
2.Htmls页面解析
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
歌单列表详情地址解析:
''' 解析歌单列表 ''' 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}
(例如:数据柱形图、直方图、散点图、盒图、分布图、数据回归分析等)
''' 歌单信息显示 ''' 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.对本次程序设计任务完成的情况做一个简单的小结。
1.经过对主题数据的分析与可视化,可以得到哪些结论?
2.对本次程序设计任务完成的情况做一个简单的小结。