网易云评论爬虫及情感分析

网易云评论爬虫及情感分析_第1张图片
窦唯

1.1 API 分析

  网易云音乐的评论区一直为人们所津津乐道,不少人因其优质的评论被圈粉。近日看到篇通过 SnowNLP 对爬取的云音乐评论进行情感分析的文章,便乘此研究下如何爬取云音乐评论并对其进行情感分析。

  首先,通过浏览器的开发者工具观察云音乐歌曲评论的页面请求,发现评论是通过 Ajax 来传输的,其 POST 请求的 paramsenSecKey 参数是经过加密处理的,这问题已有人给出了解决办法。但在前面提到的那篇文章里,发现了云音乐未被加密的 API(=。=):

http://music.163.com/api/v1/resource/comments/R_SO_4_5279713?limit=20&offset=0

  在该 URL 中,R_SO_4_ 后的那串数字是歌曲的 id,而 limitoffset 分别是分页的每页记录数和偏移量。但有了这个 API 还不够,还需要获取歌曲列表的 API,否则得手动查找和输入歌曲 id。然后又十分愉快地,找到了搜索的 API:

http://music.163.com/api/search/get/web?csrf_token=&hlpretag=&hlposttag=&s=%E7%AA%A6%E5%94%AF&type=1&offset=0&total=true&limit=

  这条 URL,s= 后面的是搜索条件,type 则对应的是搜索结果的类型(1=单曲, 10=专辑, 100=歌手, 1000=歌单, 1006=歌词, 1014=视频, 1009=主播电台, 1002=用户)。

  有了这两个 API,就可以开始编写爬虫了。

Warning:
本文代码基于 Win10 + Py3.7 环境,由于为一次性需求,且对数据量估计不足(实际爬取近 16w 条),未过多考虑效率和异常处理问题,仅供参考。

1.2 爬虫

  按照惯例,首先导入爬虫的相关库。

import requests

import re
import urllib
import math
import time
import random

import pandas as pd
import sqlite3

  构造请求头。

my_headers = {
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
    'Accept-Encoding': 'gzip, deflate',
    'Accept-Language': 'zh-CN,zh;q=0.9',
    'Host': 'music.163.com',
    'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36'
}

  接下来构建了 6 个用于爬虫的函数:

  • getJSON(url, headers): 从目标 URL 获取 JSON
  • countPages(total, limit): 根据记录总数计算要抓取的页数
  • parseSongInfo(song_list): 解析歌曲信息
  • getSongList(key, limit=30): 获取歌曲列表
  • parseComment(comments): 解析评论
  • getSongComment(id, limit=20): 获取歌曲评论
def getJSON(url, headers):
    """ Get JSON from the destination URL
    @ param url: destination url, str 
    @ param headers: request headers, dict
    @ return json: result, json
    """
    res = requests.get(url, headers=headers) 
    res.raise_for_status()  #抛出异常
    res.encoding = 'utf-8'  
    json = res.json()
    return json
def countPages(total, limit):
    """ Count pages
    @ param total: total num of records, int
    @ param limit: limit per page, int
    @ return page: num of pages, int
    """
    page = math.ceil(total/limit) 
    return page
def parseSongInfo(song_list):
    """ Parse song info
    @ param song_list: list of songs, list
    @ return song_info_list: result, list
    """
    song_info_list = []
    
    for song in song_list:
        song_info = []
        song_info.append(song['id'])
        song_info.append(song['name'])
        artists_name = ''
        artists = song['artists']
        for artist in artists:
            artists_name += artist['name'] + ','
        song_info.append(artists_name)
        song_info.append(song['album']['name'])
        song_info.append(song['album']['id'])
        song_info.append(song['duration'])
        
        song_info_list.append(song_info)
        
    return song_info_list
def getSongList(key, limit=30):
    """ Get a list of songs
    @ param key: key word, str
    @ param limit: limit per page, int, default 30
    @ return result: result, DataFrame
    """
    total_list = []
    key = urllib.parse.quote(key) #url编码
    url = 'http://music.163.com/api/search/get/web?csrf_token=&hlpretag=&hlposttag=&s=' + key +  '&type=1&offset=0&total=true&limit='
    # 获取总页数
    first_page = getJSON(url, my_headers)
    song_count = first_page['result']['songCount']
    page_num = countPages(song_count, limit)
    # 爬取所有符合条件的记录
    for n in range(page_num):
        url = 'http://music.163.com/api/search/get/web?csrf_token=&hlpretag=&hlposttag=&s=' + key +  '&type=1&offset=' + str(n*limit) + '&total=true&limit=' + str(limit)
        tmp = getJSON(url, my_headers)
        song_list = parseSongInfo(tmp['result']['songs'])
        total_list += song_list
        print('第 {0}/{1} 页爬取完成'.format(n+1, page_num))
        time.sleep(random.randint(2, 4)) 
        
    df = pd.DataFrame(data = total_list, columns=['song_id', 'song_name', 'artists', 'album_name', 'album_id', 'duration'])
    return df
def parseComment(comments):
    """ Parse song comment
        @ param comments: list of comments, list
        @ return comments_list: result, list
    """
    comments_list = []
    
    for comment in comments:
        comment_info = []
        comment_info.append(comment['commentId'])
        comment_info.append(comment['user']['userId'])
        comment_info.append(comment['user']['nickname'])
        comment_info.append(comment['user']['avatarUrl'])
        comment_info.append(comment['content'])
        comment_info.append(comment['likedCount'])
        comments_list.append(comment_info)
        
    return comments_list
def getSongComment(id, limit=20):
    """ Get Song Comments
    @ param id: song id, int
    @ param limit: limit per page, int, default 20
    @ return result: result, DataFrame
    """
    total_comment = []
    url = 'http://music.163.com/api/v1/resource/comments/R_SO_4_' + str(id) +  '?limit=20&offset=0'
    # 获取总页数
    first_page = getJSON(url, my_headers)
    total = first_page['total']
    page_num = countPages(total, limit)
    # 爬取该首歌曲下的所有评论
    for n in range(page_num):
        url = 'http://music.163.com/api/v1/resource/comments/R_SO_4_' + str(id) +  '?limit=' + str(limit) + '&offset=' + str(n*limit)
        tmp = getJSON(url, my_headers)
        comment_list = parseComment(tmp['comments'])
        total_comment += comment_list 
        print('第 {0}/{1} 页爬取完成'.format(n+1, page_num))
        time.sleep(random.randint(2, 4)) 
        
    df = pd.DataFrame(data = total_comment, columns=['comment_id', 'user_id', 'user_nickname', 'user_avatar', 'content', 'likeCount'])
    df['song_id'] = str(id) #添加 song_id 列
    return df

  在爬取数据前,先连接上数据库。

conn = sqlite3.connect('netease_cloud_music.db') 

  设置搜索条件,并爬取符合搜索条件的记录。

artist='窦唯' #设置搜索条件
song_df = getSongList(artist, 100)
song_df = song_df[song_df['artists'].str.contains(artist)] #筛选记录
song_df.drop_duplicates(subset=['song_id'], keep='first', inplace=True) #去重
song_df.to_sql(name='song', con=conn, if_exists='append', index=False)

  从数据库中读取所有 artists 包含 窦唯 的歌曲,这将得到 song_id 数据框。

sql = '''
    SELECT song_id
    FROM song
    WHERE artists LIKE '%窦唯%'
'''
song_id = pd.read_sql(sql, con=conn)

  爬取 song_id 数据框中所有歌曲的评论,并保存到数据库。

comment_df = pd.DataFrame()
for index, id in zip(song_id.index, song_id['song_id']):
    print('开始爬取第 {0}/{1} 首, {2}'.format(index+1, len(song_id['song_id']), id))
    tmp_df = getSongComment(id, 100)
    comment_df = pd.concat([comment_df, tmp_df])
comment_df.drop_duplicates(subset=['comment_id'], keep='first', inplace=True)
comment_df.to_sql(name='comment', con=conn, if_exists='append', index=False)
print('已成功保存至数据库!')

  完成上述所有步骤后,数据库将增加近 16w 条记录。

1.3 数据概览

  从数据库中读取所有 artists 包含 窦唯 的评论,得到 comment 数据框。

sql = '''
    SELECT *
    FROM comment
    WHERE song_id IN (
        SELECT song_id
        FROM song
        WHERE artists LIKE '%窦唯%'
    )
'''
comment = pd.read_sql(sql, con=conn)

  通过 nunique() 方法可得到 comment 中各字段分别有多少个不同值。从中可以看出,一共有来自 70254 名用户的 159232 条评论。

comment.nunique()

comment_id 159232
user_id 70254
user_nickname 68798
user_avatar 80094
content 136898
likeCount 616
song_id 445
dtype: int64

  接下来分别查看评论数、评论次数、点赞数前 10 的歌曲、用户和评论

song_top10_num = comment.groupby('song_id').size().sort_values(ascending=False)[0:10]
song_top10 = song[song['song_id'].isin(song_top10_num.index)].iloc[:, 0:2]
song_top10['num'] =  song_top10_num.tolist()
print(song_top10)
index song_id song_name num
0 5279713 高级动物 11722
4 5279715 悲伤的梦 9316
5 77169 暮春秋色 7464
8 5279714 噢 乖 6477
13 526468453 送别2017 5605
28 512298988 重返魔域 4677
124 27853979 殃金咒 4493
327 26031014 雨吁 3965
377 34248413 既然我们是兄弟 3845
435 28465036 天宫图 3739
user_top10 = comment.groupby('user_id').size().sort_values(ascending=False)[0:10]
print(user_top10)
user_id comments
42830600 549
33712056 322
51625217 273
284151966 242
2159884 234
271253793 234
388206024 233
263344124 232
84030184 209
131005965 204
comment_top10 = comment.sort_values(['likeCount'], ascending=False)[0:10]
print(comment_top10[['comment_id', 'likeCount']])
index comment_id likeCount
11252 51694054 35285
10522 133265373 15409
10211 148045985 12886
146129 40249220 9234
10038 157500246 7670
38728 6107434 7393
48826 658314395 5559
31101 7875585 5248
146213 35287069 4900
37307 231408710 4801

1.4 情感分析

  导入情感分析及可视化的相关库。

import numpy as np

import matplotlib.pyplot as plt
plt.style.use('ggplot')
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei']
plt.rcParams['axes.unicode_minus'] = False 

import jieba
from snownlp import SnowNLP
from wordcloud import WordCloud

  这里使用 SnowNLP 进行情感分析,SnowNLP 是一个用于处理中文文本的自然语言处理库,可以很方便地进行中文文本的情感分析(”现在训练数据主要是买卖东西时的评价,所以对其他的一些可能效果不是很好,待解决“),试举一例:

test = '窦唯只要出来把自己的老作品演绎一遍,就能日进斗金,可人家没这么干!人家还在自己坐着地铁!什么是人民艺术家?这就是!!'
c = SnowNLP(test)
c.sentiments
# 0.9988789161400798

  得分在 [0, 1] 区间内,越接近 1 则情感越积极,反之则越消极。一般来说,得分大于 0.5 的归于正向情感,小于的归于负向。下面为 comment 增加两列,分别是评论内容的情感得分和正负向标签(1=正向,-1=负向)。

comment['semiscore'] = comment['content'].apply(lambda x: SnowNLP(x).sentiments)
comment['semilabel'] = comment['semiscore'].apply(lambda x: 1 if x > 0.5 else -1)

  基于评论内容的情感得分,得到下方的直方图,从图中不难看出,对窦唯音乐的评论多是积极正面的:

plt.hist(comment['semiscore'], bins=np.arange(0, 1.01, 0.01), label='semisocre', color='#1890FF')
plt.xlabel("semiscore")
plt.ylabel("number")
plt.title("The semi-score of comment")
plt.show()
网易云评论爬虫及情感分析_第2张图片

  再通过情感标签观察,可知持正向情感的评论数是负向情感的近两倍。

semilabel = comment['semilabel'].value_counts()
semilabel = semilabel.loc[[1, -1]]

plt.bar(semilabel.index, semilabel.values, tick_label=semilabel.index, color='#2FC25B')
plt.xlabel("semislabel")
plt.ylabel("number")
plt.title("The semi-label of comment")
plt.show()
网易云评论爬虫及情感分析_第3张图片

1.5 词云

  最后,使用 jieba 进行中文分词(关于 jieba,可参阅简明 jieba 中文分词教程),并绘制词云图:

text = ''.join(str(s) for s in comment['content'] if s not in [None]) #将所有评论合并为一个长文本
jieba.add_word('窦唯') #增加自定义词语
word_list = jieba.cut(text, cut_all=False) #分词
stopwords = [line.strip() for line in open('stopwords.txt',encoding='UTF-8').readlines()] #加载停用词列表
clean_list = [seg for seg in word_list if seg not in stopwords] #去除停用词
# 生成词云
cloud = WordCloud(  
    font_path = 'F:\fonts\FZBYSK.TTF',   
    background_color = 'white',  
    max_words = 1000,  
    max_font_size = 64       
) 
word_cloud = cloud.generate(clean_text) 
# 绘制词云
plt.figure(figsize=(16, 16))
plt.imshow(word_cloud)  
plt.axis('off')  
plt.show()
网易云评论爬虫及情感分析_第4张图片

  在生成的词云图中(混入了一个 、、、、,可能是特殊字符的问题),最显眼的是窦唯高级动物的歌词,结合高达 11722 的评论数,不难看出人们对这首歌的喜爱。其次是 喜欢, 听不懂, 好听 等词语,在一定程度上体现了人们对窦唯音乐的评价。再基于 TF-IDF 算法对评论进行关键词提取,得出前 30 的关键词:

for x, w in anls.extract_tags(clean_text, topK=30, withWeight=True):
    print('{0}: {1}'.format(x, w))

喜欢: 0.07174921826661623
摇滚: 0.06222465433996381
好听: 0.048331581166697744
仙儿: 0.04814604948274102
王菲: 0.04271112348151552
窦仙: 0.027324893954643947
听不懂: 0.01956956751188709
幸福: 0.014775956892430308
成仙: 0.01465450183828875
汪峰: 0.014175488038594907
大仙: 0.013705819518861267
高级: 0.013225888298888759
黑梦: 0.013076421076696725
前奏: 0.012872688959687885
黑豹: 0.012540924545728218
听歌: 0.012455923064269991
艳阳天: 0.012455923064269991
动物: 0.012396754282072616
听听: 0.012369319024839337
听懂: 0.01160376390830011
吉他: 0.01142745810497296
忘词: 0.011296092030755316
歌曲: 0.011181124179616048
希望: 0.01089713506654457
理解: 0.010537493766491456
厉害: 0.0104225740491279
哀伤: 0.009602942087618863
窦靖童: 0.009406198340815812
电影: 0.009266377909595709
送别: 0.008950847971089923

  排在前面的关键词有“喜欢、摇滚、好听、听不懂”等,还出现了 3 个人名,分别是窦唯的前妻、女儿以及另一位中国摇滚代表人物。一些歌名(如“高级动物”)、专辑名(如“黑梦”)也出现在这列表中,可惜的是窦唯后来的作品并没有出现(和“听不懂”多少有点关系)。而带“仙”字的关键词有 4 个,“窦唯成仙了”。最有意思的彩蛋,莫过于"忘词"这个关键词,看样子大家对窦唯在 94 年那场演唱会的忘词,还是记忆犹新。

你可能感兴趣的:(网易云评论爬虫及情感分析)