600字范文,内容丰富有趣,生活中的好帮手!
600字范文 > 网易云音乐爬虫 数据可视化分析

网易云音乐爬虫 数据可视化分析

时间:2018-07-06 01:10:19

相关推荐

网易云音乐爬虫  数据可视化分析

网易云音乐爬虫 & 数据可视化分析

1. 数据爬取1.1 评论爬取1.2 用户信息爬取 2 数据清洗 & 可视化歌评文本分析

个人博客:Archiew’s blog

源码:/Archiewyq/music_163 欢迎star(●’◡’●)

1. 数据爬取

爬虫部分主要是调用官方API,本次用到的API主要有两个:

获取评论:

/api/v1/resource/comments/R_SO_4_{歌曲ID}?limit={每页限制数量}&offset={评论数总偏移}

获取评论对应用户的信息:

/api/v1/user/detail/{用户ID}

工具:

Python3.6

sublime3

MySQL(数据存储)

scrapy(数据清洗)

pyecharts(可视化工具库)

* 关于网易云音乐官方API,后期会整理一下做个汇总放在GitHub上。

1.1 评论爬取

实际操作过程中,网易云官方对于API的请求是有限制的,有条件的可以采用更换代理IP来防反爬,本次采用的是单线程爬取,所以IP封的并不太频繁,后面会对代码进行重构,实现多线程+更换IP来加快爬取速度。

根据获取评论的API,请求URL有3个可变部分:歌曲ID、每页限制数limit和评论总偏移量offset,通过API分析得知:当offeset=0时,返回json数据中包含有评论总数量total,所以根据API可设计爬虫如下:

# -*- coding:utf8 -*-# python3.6from urllib import requestimport jsonimport pymysqlfrom datetime import datetimeimport reROOT_URL = '/api/v1/resource/comments/R_SO_4_%s?limit=%s&offset=%s'LIMIT_NUMS = 50# 每页限制爬取数DATABASE = ''# 数据库名TABLE = ''# 数据库表名# 数据表设计如下:'''id(int)commentId(varchar) content(text) likedCount(int) userId(varchar)time(datetime)'''PATTERN = pile(r'[\n\t\r\/]') # 替换掉评论中的特殊字符以防插入数据库时报错def getData(url):if not url:return None, Noneheaders = {"User-Agent": 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36',"Host": "",}print('Crawling>>> ' + url)try:req = request.Request(url, headers=headers)content = request.urlopen(req).read().decode("utf-8")js = json.loads(content)total = int(js['total'])datas = []for c in js['comments']:data = dict()data['commentId'] = c['commentId']data['content'] = PATTERN.sub('', c['content'])data['time'] = datetime.fromtimestamp(c['time']//1000)data['likedCount'] = c['likedCount']data['userId'] = c['user']['userId']datas.append(data)return total, datasexcept Exception as e:print('Down err>>> ', e)passdef saveData(data):if not data:return Noneconn = pymysql.connect(host='localhost', user='****', passwd='****', db='****', charset='utf8mb4') # 注意字符集要设为utf8mb4,以支持存储评论中的emoji表情cursor = conn.cursor()sql = 'insert into ' + TABLE + ' (id,commentId,content,likedCount,time,userId) VALUES (%s,%s,%s,%s,%s,%s)'for d in data:try:cursor.execute('SELECT max(id) FROM '+TABLE)id_ = cursor.fetchone()[0]cursor.execute(sql, (id_+1,d['commentId'], d['content'], d['likedCount'], d['time'], d['userId']))mit()except Exception as e:print('mysql err>>> ',d['commentId'],e)passcursor.close()conn.close()if __name__ == '__main__':songId = input('歌曲ID:').strip()total,data = getData(ROOT_URL%(songId, LIMIT_NUMS, 0))saveData(data)if total:for i in range(1, total//EVERY_PAGE_NUMS+1):_, data = getData(ROOT_URL%(songId, LIMIT_NUMS, i*(LIMIT_NUMS)))saveData(data)

以上代码实现了单线程爬取网易云音乐某首歌曲的评论并存储进数据库(在这里其实有个坑!!!不过不影响,后面会讲到)。实际上,API返回的不仅仅包含代码中所提到的信息,具体可自行测试,我们还想要得到评论对应的用户的具体信息,但是这个API返回的用户信息不全面,所以接下来,针对评论对应的用户信息进行抓取。

1.2 用户信息爬取

根据获取用户信息的API,请求URL有1个可变部分:用户ID,前一部分已经将每条评论对应的用户ID也存储下来,这里只需要从数据库取用户ID并抓取信息即可,所以根据API可设计爬虫如下:

# -*- coding:utf8 -*-# python3.6from urllib import requestimport jsonimport pymysqlimport reROOT_URL = '/api/v1/user/detail/'DATABASE = '****'TABLE_USERS = '****'TABLE_COMMENTS = '****'# 数据表设计如下:'''id(int)userId(varchar) gender(char) userName(varchar) age(int)level(int)city(varchar)sign(text)eventCount(int)followedCount(int)followsCount(int)recordCount(int)avatar(varchar)'''PATTERN = pile(r'[\n\t\r\/]') # 替换掉签名中的特殊字符以防插入数据库时报错def getData(url):if not url:return Noneprint('Crawling>>> ' + url)try:req = request.Request(url, headers=headers)content = request.urlopen(req).read().decode("utf-8")js = json.loads(content)data = {}if js['code'] == 200:data['userId'] = js['profile']['userId']data['userName'] = js['profile']['nickname']data['avatar'] = js['profile']['avatarUrl']data['gender'] = js['profile']['gender']if int(js['profile']['birthday'])<0:data['age'] = 0else:data['age'] =(-1970)-(int(js['profile']['birthday'])//(1000*365*24*3600))if int(data['age'])<0:data['age'] = 0data['level'] = js['level']data['sign'] = PATTERN.sub(' ', js['profile']['signature'])data['eventCount'] = js['profile']['eventCount']data['followCount'] = js['profile']['follows']data['fanCount'] = js['profile']['followeds']data['city'] = js['profile']['city']data['recordCount'] = js['listenSongs']except Exception as e:print('Down err>>> ', e)passreturn Nonedef saveData(data):if not data:return Noneconn = pymysql.connect(host='localhost', user='****', passwd='****', db=DATABASE, charset='utf8mb4') # 注意字符集要设为utf8mb4,以支持存储签名中的emoji表情cursor = conn.cursor()sql = 'insert into ' + TABLE + ' (id,userName,gender,age,level,city,sign,eventCount,followsCount,followedCount,recordCount,avatar,userId) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)'try:cursor.execute('SELECT max(id) FROM '+TABLE_USERS)id_ = cursor.fetchone()[0]cursor.execute(sql, (id_+1,data['userName'],data['gender'],data['age'],data['level'],data['city'],data['sign'],data['eventCount'],data['followsCount'],data['followedCount'],data['recordCount'],data['avatar'],data['userId']))mit()except Exception as e:print('mysql err>>> ',data['userId'],e)passfinally:cursor.close()conn.close()def getID():conn = pymysql.connect(host='localhost', user='****', passwd='****', db=DATABASE, charset='utf8mb4')cursor = conn.cursor()sql = 'SELECT userId FROM '+TABLE_COMMENTStry:cursor.execute(sql)res = cursor.fetchall()return resexcept Exception as e:print('get err>>> ', e)passfinally:cursor.close()conn.close()return Noneif __name__ == '__main__':usersID = getID()for i in usersID:data = getData(ROOT_URL+i[0].strip())saveData(data)

以上代码实现了单线程爬取网易云音乐用户信息并存储进数据库。至此,已经完成了歌曲评论和对应用户信息的抓取。接下来,对抓取到的数据进行清洗及可视化分析。

2 数据清洗 & 可视化

关于数据的清洗,实际上在上一部分抓取数据的过程中已经做了一部分,包括:后台返回的空用户信息、重复数据的去重等。除此之外,还要进行一些清洗:用户年龄错误、用户城市编码转换等。

关于数据的去重,评论部分可以以sommentId为数据库索引,利用数据库来自动去重;用户信息部分以用户ID为数据库索引实现自动去重。

API返回的用户年龄一般是时间戳的形式(以毫秒计)、有时候也会返回一个负值或者一个大于当前时间的值,暂时没有找到这两种值代表的含义,故而一律按0来处理。

API返回的用户信息中,城市分为province和city两个字段,本此分析中只保存了city字段。实际上字段值是一个城市code码,具体对照在这里下载。

在这部分,利用Python的数据处理库pandas进行数据处理,利用可视化库pyecharts进行数据可视化。处理代码如下:

# -*- coding:utf8 -*-# python3.6import pandas as pdimport pymysqlfrom pyecharts import Bar,Pie,Line,Scatter,MapTABLE_COMMENTS = '****'TABLE_USERS = '****'DATABASE = '****'conn = pymysql.connect(host='localhost', user='****', passwd='****', db=DATABASE, charset='utf8mb4')sql_users = 'SELECT id,gender,age,city FROM '+TABLE_USERSsql_comments = 'SELECT id,time FROM '+TABLE_COMMENTScomments = pd.read_sql(sql_comments, con=conn)users = pd.read_sql(sql_users, con=conn)# 评论时间(按天)分布分析comments_day = comments['time'].dt.datedata = comments_day.id.groupby(comments_day['time']).count()line = Line('评论时间(按天)分布')line.use_theme('dark')line.add('',data.index.values,data.values,is_fill=True,)line.render(r'./评论时间(按天)分布.html')# 评论时间(按小时)分布分析comments_hour = comments['time'].dt.hourdata = comments_hour.id.groupby(comments_hour['time']).count()line = Line('评论时间(按小时)分布')line.use_theme('dark')line.add('',data.index.values,data.values,is_fill=True,)line.render(r'./评论时间(按小时)分布.html')# 评论时间(按周)分布分析comments_week = comments['time'].dt.dayofweekdata = comments_week.id.groupby(comments_week['time']).count()line = Line('评论时间(按周)分布')line.use_theme('dark')line.add('',data.index.values,data.values,is_fill=True,)line.render(r'./评论时间(按周)分布.html')# 用户年龄分布分析age = users[users['age']>0]# 清洗掉年龄小于1的数据age = age.id.groupby(age['age']).count()# 以年龄值对数据分组Bar = Bar('用户年龄分布')Bar.use_theme('dark')Bar.add('',age.index.values,age.values,is_fill=True,)Bar.render(r'./用户年龄分布图.html')# 生成渲染的html文件# 用户地区分布分析# 城市code编码转换def city_group(cityCode):city_map = {'11': '北京','12': '天津','31': '上海','50': '重庆','5e': '重庆','81': '香港','82': '澳门','13': '河北','14': '山西','15': '内蒙古','21': '辽宁','22': '吉林','23': '黑龙江','32': '江苏','33': '浙江','34': '安徽','35': '福建','36': '江西','37': '山东','41': '河南','42': '湖北','43': '湖南','44': '广东','45': '广西','46': '海南','51': '四川','52': '贵州','53': '云南','54': '西藏','61': '陕西','62': '甘肃','63': '青海','64': '宁夏','65': '新疆','71': '台湾','10': '其他',}return city_map[cityCode[:2]]city = users['city'].apply(city_group)city = city.id.groupby(city['city']).count()map_ = Map('用户地区分布图')map_.add('',city.index.values,city.values,maptype='china',is_visualmap=True,visual_text_color='#000',is_label_show=True,)map_.render(r'./用户地区分布图.html')

以上,是对抓取到的数据采用可视化库pyecharts进行可视化分析,得到的结果如下:

评论时间按周分布图可以看出,评论数在一周当中前面较少,后面逐渐增多,这可以解释为往后接近周末,大家有更多时间来听听歌、刷刷歌评,而一旦周末过完,评论量马上下降(周日到周一的下降过渡),大家又回归到工作当中。

评论时间按小时分布图可以看出,评论数在一天当中有两个小高峰:11点-13点和22点-0点。这可以解释为用户在中午午饭时间和晚上下班(课)在家时间有更多的时间来听歌刷评论,符合用户的日常。至于为什么早上没有出现一个小高峰,大概是早上大家都在抢时间上班(学),没有多少时间去刷评论。

用户年龄分布图可以看出,用户大多集中在14-30岁之间,以20岁左右居多,除去虚假年龄之外,这个年龄分布也符合网易云用户的年龄段。图中可以看出28岁有个高峰,猜测可能是包含了一些异常数据,有兴趣的化可以做进一步分析。

用户地区分布图可以看出,用户涵盖了全国各大省份,因为中间数据(坑)的缺失,并没有展现出哪个省份特别突出的情况。对别的歌评(完全数据)的可视化分析,可以看出明显的地区分布差异。

** 接下来说说前面提到的!!!

细心观察评论数(按天)分布那张图,发现到间有很大一部分数据缺失,这实际上是因为在数据抓取过程中出现的问题。研究了一下发现,根据获取歌曲评论的API,实际上每首歌最多只能获得2w条左右(去重后)的评论,对于评论数超过2w的歌曲,只能获得前后(日期)各1w条评论,而且这个限制对于网易云官网也是存在的,具体表现为:对一首评论数超过2w的歌,如果一直往后浏览评论,会发现从第500页(网页端网易云每页20条评论)往后,后台返回的内容和第500页完全一样,从后往前同理。这应该是官方后台做了限制,连自家也不放过。。。

此次分析只是对某一首歌曲评论时间、用户年龄/地区分布进行的,实际上抓取到的信息不仅仅在于此,可以做进一步分析(比如利用评论内容进行文本内容分析等),这部分,未来会进一步分析。当然也可以根据自己情况对不同歌曲进行分析。

-12-17

歌评文本分析

评论的文本分析做了两部分:情感分析和词云生成。

情感分析采用Python的文本分析库snownlp,代码如下:

# -*- coding:utf8 -*-# python3.6import numpy as npimport pymysqlfrom snownlp import SnowNLPfrom pyecharts import BarTABLE_COMMENTS = '****'DATABASE = '****'SONGNAME = '****'def getText():conn = pymysql.connect(host='localhost', user='root', passwd='root', db=DATABASE, charset='utf8')sql = 'SELECT id,content FROM '+TABLE_COMMENTStext = pd.read_sql(sql%(SONGNAME), con=conn)return textdef getSemi(text):text['content'] = text['content'].apply(lambda x:round(SnowNLP(x).sentiments, 2))semiscore = text.id.groupby(text['content']).count()bar = Bar('评论情感得分')bar.use_theme('dark')bar.add('',y_axis = semiscore.values,x_axis = semiscore.index.values,is_fill=True,)bar.render(r'情感得分分析.html')text['content'] = text['content'].apply(lambda x:1 if x>0.5 else -1)semilabel = text.id.groupby(text['content']).count()bar = Bar('评论情感标签')bar.use_theme('dark')bar.add('',y_axis = semilabel.values,x_axis = semilabel.index.values,is_fill=True,)bar.render(r'情感标签分析.html')

结果:

词云生成采用jieba分词库分词,wordcloud生成词云,代码如下:

from wordcloud import WordCloudimport matplotlib.pyplot as pltplt.style.use('ggplot')plt.rcParams['axes.unicode_minus'] = Falsedef getWordcloud(text):text = ''.join(str(s) for s in text['content'] if s)word_list = jieba.cut(text, cut_all=False)stopwords = [line.strip() for line in open(r'./StopWords.txt', 'r').readlines()]# 导入停用词clean_list = [seg for seg in word_list if seg not in stopwords] #去除停用词clean_text = ''.join(clean_list)# 生成词云cloud = WordCloud(font_path = r'C:/Windows/Fonts/msyh.ttc',background_color = 'white',max_words = 800,max_font_size = 64)word_cloud = cloud.generate(clean_text)# 绘制词云plt.figure(figsize=(12, 12))plt.imshow(word_cloud)plt.axis('off')plt.show()if __name__ == '__main__':text = getText()getSemi(text)getWordcloud(text)

中文停用词下载

Todo:

对评论内容做进一步分析.12.17代码重构,增加多线程等.12.19代码开源在Github上.12.19

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。