666hll1212123

作业要求来自于https://edu.cnblogs.com/campus/gzcc/GZCC-16SE2/homework/3075

爬虫综合大作业

  1. 选择一个热点或者你感兴趣的主题。
  2. 选择爬取的对象与范围。
  3. 了解爬取对象的限制与约束。
  4. 爬取相应内容。
  5. 做数据分析与文本分析。
  6. 形成一篇文章,有说明、技术要点、有数据、有数据分析图形化展示与说明、文本分析图形化展示与说明。
  7. 文章公开发布。

1. 数据爬取

       我们本次爬取的对象是一首名为《five hours》的经典电音流行歌曲,Five Hours是Erick Orrosquieta于2014年4月发行的单曲,当年这首单曲就出现在奥地利,比利时,法国,荷兰,挪威,瑞典和瑞士的榜单中。

       作者Erick Orrosquieta,一般为人熟知的是他的艺名Deorro,2014年世界百大DJ排名十九位。1991年8月29日生于美国洛杉矶,墨西哥血统的音乐制作人,签约Ultra Records,2014年他创立了唱片公司Panda Funk。Deorro以前用过TON!C这个名字。Deorro的音乐倾向Melbourne Bounce风格,融合Dutch、 moombah, progressive、 house、贝斯等多种元素和风格,几分钟就可以点炸现场气氛。他的这首单曲“Five Hours”登顶Most Played on Dance Radio和Most Played on Top 40 radio Mixshows。

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

①获取评论:
http://music.163.com/api/v1/resource/comments/R_SO_4_{歌曲ID}?limit={每页限制数量}&offset={评论数总偏移}

②获取评论对应用户的信息:
https://music.163.com/api/v1/user/detail/{用户ID}

完成后的项目文件图如下:

1.1 评论爬取

具体代码如下:

 1 from urllib import request
 2 import json
 3 import pymysql
 4 from datetime import datetime
 5 import re
 6 
 7 ROOT_URL = \'http://music.163.com/api/v1/resource/comments/R_SO_4_%s?limit=%s&offset=%s\'
 8 LIMIT_NUMS = 50    # 每页限制爬取数
 9 DATABASE = \'emp\'    # 数据库名
10 TABLE = \'temp1\'    # 数据库表名
11 # 数据表设计如下:
12 \'\'\'
13 commentId(varchar) 
14 content(text)         likedCount(int) 
15 userId(varchar)    time(datetime)
16 \'\'\'
17 PATTERN = re.compile(r\'[\n\t\r\/]\') # 替换掉评论中的特殊字符以防插入数据库时报错
18 
19 def getData(url):
20     if not url:
21         return None, None
22     headers = {
23             "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\',
24             "Host": "music.163.com",
25 }
26     print(\'Crawling>>> \' + url)
27     try:
28         req = request.Request(url, headers=headers)
29         content = request.urlopen(req).read().decode("utf-8")
30         js = json.loads(content)
31         total = int(js[\'total\'])
32         datas = []
33         for c in js[\'comments\']:
34             data = dict()
35             data[\'commentId\'] = c[\'commentId\']
36             data[\'content\'] = PATTERN.sub(\'\', c[\'content\'])
37             data[\'time\'] = datetime.fromtimestamp(c[\'time\']//1000)
38             data[\'likedCount\'] = c[\'likedCount\']
39             data[\'userId\'] = c[\'user\'][\'userId\']
40             datas.append(data)
41         return total, datas
42     except Exception as e:
43         print(\'Down err>>> \', e)
44         pass
45     
46 def saveData(data):
47     if not data:
48         return None
49     conn = pymysql.connect(host=\'localhost\', user=\'root\', passwd=\'123456\', db=\'emp\', charset=\'utf8mb4\') # 注意字符集要设为utf8mb4,以支持存储评论中的emoji表情
50     cursor = conn.cursor()
51     sql = \'insert into \' + TABLE + \' (commentId,content,likedCount,time,userId) VALUES (%s,%s,%s,%s,%s)\'
52     
53     for d in data:
54         
55         try:
56             #cursor.execute(\'SELECT max(c) FROM \'+TABLE)
57             #id_ = cursor.fetchone()[0]
58             
59             cursor.execute(sql, (d[\'commentId\'], d[\'content\'], d[\'likedCount\'], d[\'time\'], d[\'userId\']))
60             conn.commit()
61         except Exception as e:
62             print(\'mysql err>>> \',d[\'commentId\'],e)
63             pass
64 
65     cursor.close()
66     conn.close()    
67     
68 if __name__ == \'__main__\':
69     songId = input(\'歌曲ID:\').strip()
70     total,data = getData(ROOT_URL%(songId, LIMIT_NUMS, 0))
71     saveData(data)
72     if total:
73         for i in range(1, total//50+1):
74                 _, data = getData(ROOT_URL%(songId, LIMIT_NUMS, i*(LIMIT_NUMS)))
75                 saveData(data)

实际操作过程中,网易云官方对于API的请求是有限制的,有条件的可以采用更换代理IP来防反爬,而这一次作业在爬取数据的时候由于前期操作过度,导致被BAN IP,数据无法获取,之后是通过挂载虚拟IP才实现数据爬取的。

本次采用的是单线程爬取,所以IP封的并不太频繁,后面会对代码进行重构,实现多线程+更换IP来加快爬取速度。

根据获取评论的API,请求URL有3个可变部分:每页限制数limit和评论总偏移量offset,通过API分析得知:当offeset=0时,返回json数据中包含有评论总数量total。

本次共爬取5394条数据(避免盲目多爬被封ID)

 

1.2 用户信息爬取

具体代码如下:

 1 from urllib import request
 2 import json
 3 import pymysql
 4 import re
 5 
 6 ROOT_URL = \'https://music.163.com/api/v1/user/detail/\'
 7 DATABASE = \'emp\'
 8 TABLE_USERS = \'temp2\'
 9 TABLE_COMMENTS = \'temp1\'
10 # 数据表设计如下:
11 \'\'\'
12 id(int)                userId(varchar) 
13 gender(char)         userName(varchar) 
14 age(int)            level(int)            
15 city(varchar)        sign(text)            
16 eventCount(int)    followedCount(int)    
17 followsCount(int)    recordCount(int)    
18 avatar(varchar)
19 \'\'\'
20 PATTERN = re.compile(r\'[\n\t\r\/]\') # 替换掉签名中的特殊字符以防插入数据库时报错
21 headers = {
22             "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\',
23             "Host": "music.163.com",
24 }
25 def getData(url):
26     if not url:
27         return None
28     print(\'Crawling>>> \' + url)
29     try:
30         req = request.Request(url, headers=headers)
31         content = request.urlopen(req).read().decode("utf-8")
32         js = json.loads(content)
33         data = {}
34         if js[\'code\'] == 200:
35             data[\'userId\'] = js[\'profile\'][\'userId\']
36             data[\'userName\'] = js[\'profile\'][\'nickname\']
37             data[\'avatar\'] = js[\'profile\'][\'avatarUrl\']
38             data[\'gender\'] = js[\'profile\'][\'gender\']
39             if int(js[\'profile\'][\'birthday\'])<0:
40                 data[\'age\'] = 0
41             else:
42                 data[\'age\'] =(2018-1970)-(int(js[\'profile\'][\'birthday\'])//(1000*365*24*3600))
43             if int(data[\'age\'])<0:
44                 data[\'age\'] = 0
45             data[\'level\'] = js[\'level\']
46             data[\'sign\'] = PATTERN.sub(\' \', js[\'profile\'][\'signature\'])
47             data[\'eventCount\'] = js[\'profile\'][\'eventCount\']
48             data[\'followsCount\'] = js[\'profile\'][\'follows\']
49             data[\'followedCount\'] = js[\'profile\'][\'followeds\']
50             data[\'city\'] = js[\'profile\'][\'city\']
51             data[\'recordCount\'] = js[\'listenSongs\']
52             
53             saveData(data)
54     except Exception as e:
55         print(\'Down err>>> \', e)
56         pass
57     return None
58 
59 def saveData(data):
60     if not data:
61         return None
62     conn = pymysql.connect(host=\'localhost\', user=\'root\', passwd=\'123456\', db=\'emp\', charset=\'utf8mb4\') # 注意字符集要设为utf8mb4,以支持存储签名中的emoji表情
63     cursor = conn.cursor()
64     sql = \'insert into \' + TABLE_USERS + \' (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)\'
65     try:
66         
67         
68         cursor.execute(sql, (data[\'userName\'],data[\'gender\'],data[\'age\'],data[\'level\'],data[\'city\'],data[\'sign\'],data[\'eventCount\'],data[\'followsCount\'],data[\'followedCount\'],data[\'recordCount\'],data[\'avatar\'],data[\'userId\']))
69         conn.commit()
70     except Exception as e:
71         print(\'mysql err>>> \',data[\'userId\'],e)
72         pass
73     finally:
74         cursor.close()
75         conn.close()    
76 
77 def getID():
78     conn = pymysql.connect(host=\'localhost\', user=\'root\', passwd=\'123456\', db=\'emp\', charset=\'utf8mb4\')
79     cursor = conn.cursor()
80     sql = \'SELECT userId FROM \'+TABLE_COMMENTS
81     try:
82         cursor.execute(sql)
83         res = cursor.fetchall()
84         return res
85     except Exception as e:
86         print(\'get err>>> \', e)
87         pass
88     finally:
89         cursor.close()
90         conn.close()
91     return None
92 
93 if __name__ == \'__main__\':
94     usersID = getID()
95     for i in usersID:
96         getData(ROOT_URL+i[0].strip())
97         

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

 

至此,已经完成了歌曲评论和对应用户信息的抓取。接下来,对抓取到的数据进行清洗及可视化分析。

 

2 数据清洗 & 可视化

处理代码如下:

  1 import pandas as pd
  2 import pymysql
  3 from pyecharts import Bar,Pie,Line,Scatter,Map
  4 
  5 TABLE_COMMENTS = \'temp1\'
  6 TABLE_USERS = \'temp2\'
  7 DATABASE = \'emp\'
  8 
  9 conn = pymysql.connect(host=\'localhost\', user=\'root\', passwd=\'123456\', db=\'emp\', charset=\'utf8mb4\')
 10 sql_users = \'SELECT id,gender,age,city FROM \'+TABLE_USERS
 11 sql_comments = \'SELECT id,time FROM \'+TABLE_COMMENTS
 12 comments = pd.read_sql(sql_comments, con=conn)
 13 users = pd.read_sql(sql_users, con=conn)
 14 
 15 # 评论时间(按天)分布分析
 16 comments_day = comments[\'time\'].dt.date
 17 data = comments_day.id.groupby(comments_day[\'time\']).count()
 18 line = Line(\'评论时间(按天)分布\')
 19 line.use_theme(\'dark\')
 20 line.add(
 21     \'\',
 22     data.index.values,
 23     data.values,
 24     is_fill=True,
 25 )
 26 line.render(r\'./评论时间(按天)分布.html\')
 27 # 评论时间(按小时)分布分析
 28 comments_hour = comments[\'time\'].dt.hour
 29 data = comments_hour.id.groupby(comments_hour[\'time\']).count()
 30 line = Line(\'评论时间(按小时)分布\')
 31 line.use_theme(\'dark\')
 32 line.add(
 33     \'\',
 34     data.index.values,
 35     data.values,
 36     is_fill=True,
 37 )
 38 line.render(r\'./评论时间(按小时)分布.html\')
 39 # 评论时间(按周)分布分析
 40 comments_week = comments[\'time\'].dt.dayofweek
 41 data = comments_week.id.groupby(comments_week[\'time\']).count()
 42 line = Line(\'评论时间(按周)分布\')
 43 line.use_theme(\'dark\')
 44 line.add(
 45     \'\',
 46     data.index.values,
 47     data.values,
 48     is_fill=True,
 49 )
 50 line.render(r\'./评论时间(按周)分布.html\')
 51 
 52 # 用户年龄分布分析
 53 age = users[users[\'age\']>0]    # 清洗掉年龄小于1的数据
 54 age = age.id.groupby(age[\'age\']).count()    # 以年龄值对数据分组
 55 Bar = Bar(\'用户年龄分布\')
 56 Bar.use_theme(\'dark\')
 57 Bar.add(
 58     \'\',
 59     age.index.values,
 60     age.values,
 61     is_fill=True,
 62 )
 63 Bar.render(r\'./用户年龄分布图.html\')    # 生成渲染的html文件
 64 
 65 # 用户地区分布分析
 66 # 城市code编码转换
 67 def city_group(cityCode):
 68     city_map = {
 69         \'11\': \'北京\',
 70         \'12\': \'天津\',
 71         \'31\': \'上海\',
 72         \'50\': \'重庆\',
 73         \'5e\': \'重庆\',
 74         \'81\': \'香港\',
 75         \'82\': \'澳门\',
 76         \'13\': \'河北\',
 77         \'14\': \'山西\',
 78         \'15\': \'内蒙古\',
 79         \'21\': \'辽宁\',
 80         \'22\': \'吉林\',
 81         \'23\': \'黑龙江\',
 82         \'32\': \'江苏\',
 83         \'33\': \'浙江\',
 84         \'34\': \'安徽\',
 85         \'35\': \'福建\',
 86         \'36\': \'江西\',
 87         \'37\': \'山东\',
 88         \'41\': \'河南\',
 89         \'42\': \'湖北\',
 90         \'43\': \'湖南\',
 91         \'44\': \'广东\',
 92         \'45\': \'广西\',
 93         \'46\': \'海南\',
 94         \'51\': \'四川\',
 95         \'52\': \'贵州\',
 96         \'53\': \'云南\',
 97         \'54\': \'西藏\',
 98         \'61\': \'陕西\',
 99         \'62\': \'甘肃\',
100         \'63\': \'青海\',
101         \'64\': \'宁夏\',
102         \'65\': \'新疆\',
103         \'71\': \'台湾\',
104         \'10\': \'其他\',
105     }
106     return city_map[cityCode[:2]]
107     
108 city = users[\'city\'].apply(city_group)
109 city = city.id.groupby(city[\'city\']).count()
110 map_ = Map(\'用户地区分布图\')
111 map_.add(
112     \'\',
113     city.index.values,
114     city.values,
115     maptype=\'china\',
116     is_visualmap=True,
117     visual_text_color=\'#000\',
118     is_label_show=True,
119 )
120 map_.render(r\'./用户地区分布图.html\')

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

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

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

②API返回的用户信息中,城市分为province和city两个字段,本此分析中只保存了city字段。实际上字段值是一个城市code码

③在这部分,利用Python的数据处理库pandas进行数据处理,利用可视化库pyecharts进行数据可视化。

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

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

 

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

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

 

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

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

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

3.歌评文本分析

评论的文本分析做了两部分:情感分析和词云生成。
情感分析采用Python的文本分析库snownlp。具体代码如下:

 1 import numpy as np
 2 import pymysql
 3 from snownlp import SnowNLP
 4 from pyecharts import Bar
 5 
 6 TABLE_COMMENTS = \'temp1\'
 7 DATABASE = \'emp\'
 8 SONGNAME = \'five hours\'
 9 
10 def getText():
11     conn = pymysql.connect(host=\'localhost\', user=\'root\', passwd=\'123456\', db=DATABASE, charset=\'utf8\')
12     sql = \'SELECT id,content FROM \'+TABLE_COMMENTS
13     text = pd.read_sql(sql%(SONGNAME), con=conn)
14     return text
15 
16 def getSemi(text):
17     text[\'content\'] = text[\'content\'].apply(lambda x:round(SnowNLP(x).sentiments, 2))
18     semiscore = text.id.groupby(text[\'content\']).count()
19     bar = Bar(\'评论情感得分\')
20     bar.use_theme(\'dark\')
21     bar.add(
22         \'\',
23         y_axis = semiscore.values,
24         x_axis = semiscore.index.values,
25         is_fill=True,
26     )
27     bar.render(r\'情感得分分析.html\')
28 
29     text[\'content\'] = text[\'content\'].apply(lambda x:1 if x>0.5 else -1)
30     semilabel = text.id.groupby(text[\'content\']).count()
31     bar = Bar(\'评论情感标签\')
32     bar.use_theme(\'dark\')
33     bar.add(
34         \'\',
35         y_axis = semilabel.values,
36         x_axis = semilabel.index.values,
37         is_fill=True,
38     )
39     bar.render(r\'情感标签分析.html\')

结果:

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

 1 from wordcloud import WordCloud
 2 import matplotlib.pyplot as plt
 3 plt.style.use(\'ggplot\')
 4 plt.rcParams[\'axes.unicode_minus\'] = False
 5 
 6 def getWordcloud(text):
 7     text = \'\'.join(str(s) for s in text[\'content\'] if s)
 8     word_list = jieba.cut(text, cut_all=False)
 9     stopwords = [line.strip() for line in open(r\'./StopWords.txt\', \'r\').readlines()]    # 导入停用词
10     clean_list = [seg for seg in word_list if seg not in stopwords] #去除停用词
11     clean_text = \'\'.join(clean_list)
12     # 生成词云
13     cloud = WordCloud(
14         font_path = r\'C:/Windows/Fonts/msyh.ttc\',
15         background_color = \'white\',
16         max_words = 800,
17         max_font_size = 64
18     )
19     word_cloud = cloud.generate(clean_text)
20     # 绘制词云
21     plt.figure(figsize=(12, 12))
22     plt.imshow(word_cloud)
23     plt.axis(\'off\')
24     plt.show()
25     
26 if __name__ == \'__main__\':
27     text = getText()
28     getSemi(text)
29     getWordcloud(text)

词云:

 

分类:

技术点:

相关文章: