1.1 API 分析
??網(wǎng)易云音樂(lè)的評(píng)論區(qū)一直為人們所津津樂(lè)道姻蚓,不少人因其優(yōu)質(zhì)的評(píng)論被圈粉浴鸿。近日看到篇通過(guò) SnowNLP 對(duì)爬取的云音樂(lè)評(píng)論進(jìn)行情感分析的文章盈匾,便乘此研究下如何爬取云音樂(lè)評(píng)論并對(duì)其進(jìn)行情感分析。
??首先虹脯,通過(guò)瀏覽器的開(kāi)發(fā)者工具觀察云音樂(lè)歌曲評(píng)論的頁(yè)面請(qǐng)求驴娃,發(fā)現(xiàn)評(píng)論是通過(guò) Ajax 來(lái)傳輸?shù)模?POST 請(qǐng)求的 params
和 enSecKey
參數(shù)是經(jīng)過(guò)加密處理的循集,這問(wèn)題已有人給出了解決辦法唇敞。但在前面提到的那篇文章里,發(fā)現(xiàn)了云音樂(lè)未被加密的 API(=咒彤。=):
http://music.163.com/api/v1/resource/comments/R_SO_4_5279713?limit=20&offset=0
??在該 URL 中疆柔,R_SO_4_
后的那串?dāng)?shù)字是歌曲的 id
,而 limit
和 offset
分別是分頁(yè)的每頁(yè)記錄數(shù)和偏移量镶柱。但有了這個(gè) API 還不夠旷档,還需要獲取歌曲列表的 API,否則得手動(dòng)查找和輸入歌曲 id
歇拆。然后又十分愉快地彬犯,找到了搜索的 API:
??這條 URL,s=
后面的是搜索條件查吊,type
則對(duì)應(yīng)的是搜索結(jié)果的類型(1=單曲, 10=專輯, 100=歌手, 1000=歌單, 1006=歌詞, 1014=視頻, 1009=主播電臺(tái), 1002=用戶)。
??有了這兩個(gè) API湖蜕,就可以開(kāi)始編寫爬蟲(chóng)了逻卖。
Warning:
本文代碼基于 Win10 + Py3.7 環(huán)境,由于為一次性需求昭抒,且對(duì)數(shù)據(jù)量估計(jì)不足(實(shí)際爬取近 16w 條)评也,未過(guò)多考慮效率和異常處理問(wèn)題,僅供參考灭返。
1.2 爬蟲(chóng)
??按照慣例盗迟,首先導(dǎo)入爬蟲(chóng)的相關(guān)庫(kù)。
import requests
import re
import urllib
import math
import time
import random
import pandas as pd
import sqlite3
??構(gòu)造請(qǐng)求頭熙含。
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'
}
??接下來(lái)構(gòu)建了 6 個(gè)用于爬蟲(chóng)的函數(shù):
-
getJSON(url, headers)
: 從目標(biāo) URL 獲取 JSON -
countPages(total, limit)
: 根據(jù)記錄總數(shù)計(jì)算要抓取的頁(yè)數(shù) -
parseSongInfo(song_list)
: 解析歌曲信息 -
getSongList(key, limit=30)
: 獲取歌曲列表 -
parseComment(comments)
: 解析評(píng)論 -
getSongComment(id, limit=20)
: 獲取歌曲評(píng)論
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='
# 獲取總頁(yè)數(shù)
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} 頁(yè)爬取完成'.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'
# 獲取總頁(yè)數(shù)
first_page = getJSON(url, my_headers)
total = first_page['total']
page_num = countPages(total, limit)
# 爬取該首歌曲下的所有評(píng)論
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} 頁(yè)爬取完成'.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
??在爬取數(shù)據(jù)前罚缕,先連接上數(shù)據(jù)庫(kù)。
conn = sqlite3.connect('netease_cloud_music.db')
??設(shè)置搜索條件怎静,并爬取符合搜索條件的記錄邮弹。
artist='竇唯' #設(shè)置搜索條件
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)
??從數(shù)據(jù)庫(kù)中讀取所有 artists
包含 竇唯
的歌曲黔衡,這將得到 song_id
數(shù)據(jù)框。
sql = '''
SELECT song_id
FROM song
WHERE artists LIKE '%竇唯%'
'''
song_id = pd.read_sql(sql, con=conn)
??爬取 song_id
數(shù)據(jù)框中所有歌曲的評(píng)論腌乡,并保存到數(shù)據(jù)庫(kù)盟劫。
comment_df = pd.DataFrame()
for index, id in zip(song_id.index, song_id['song_id']):
print('開(kāi)始爬取第 {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('已成功保存至數(shù)據(jù)庫(kù)!')
??完成上述所有步驟后与纽,數(shù)據(jù)庫(kù)將增加近 16w 條記錄侣签。
1.3 數(shù)據(jù)概覽
??從數(shù)據(jù)庫(kù)中讀取所有 artists
包含 竇唯
的評(píng)論,得到 comment
數(shù)據(jù)框急迂。
sql = '''
SELECT *
FROM comment
WHERE song_id IN (
SELECT song_id
FROM song
WHERE artists LIKE '%竇唯%'
)
'''
comment = pd.read_sql(sql, con=conn)
??通過(guò) nunique()
方法可得到 comment
中各字段分別有多少個(gè)不同值影所。從中可以看出,一共有來(lái)自 70254 名用戶的 159232 條評(píng)論袋毙。
comment.nunique()
comment_id 159232
user_id 70254
user_nickname 68798
user_avatar 80094
content 136898
likeCount 616
song_id 445
dtype: int64
??接下來(lái)分別查看評(píng)論數(shù)型檀、評(píng)論次數(shù)、點(diǎn)贊數(shù)前 10 的歌曲听盖、用戶和評(píng)論
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 | 高級(jí)動(dòng)物 | 11722 |
4 | 5279715 | 悲傷的夢(mèng) | 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 情感分析
??導(dǎo)入情感分析及可視化的相關(guān)庫(kù)胀溺。
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 進(jìn)行情感分析,SnowNLP 是一個(gè)用于處理中文文本的自然語(yǔ)言處理庫(kù)皆看,可以很方便地進(jìn)行中文文本的情感分析(”現(xiàn)在訓(xùn)練數(shù)據(jù)主要是買賣東西時(shí)的評(píng)價(jià)仓坞,所以對(duì)其他的一些可能效果不是很好,待解決“)腰吟,試舉一例:
test = '竇唯只要出來(lái)把自己的老作品演繹一遍无埃,就能日進(jìn)斗金,可人家沒(méi)這么干毛雇!人家還在自己坐著地鐵嫉称!什么是人民藝術(shù)家?這就是A榇织阅!'
c = SnowNLP(test)
c.sentiments
# 0.9988789161400798
??得分在 [0, 1]
區(qū)間內(nèi),越接近 1
則情感越積極震捣,反之則越消極荔棉。一般來(lái)說(shuō),得分大于 0.5
的歸于正向情感蒿赢,小于的歸于負(fù)向润樱。下面為 comment
增加兩列,分別是評(píng)論內(nèi)容的情感得分和正負(fù)向標(biāo)簽(1=正向羡棵,-1=負(fù)向)壹若。
comment['semiscore'] = comment['content'].apply(lambda x: SnowNLP(x).sentiments)
comment['semilabel'] = comment['semiscore'].apply(lambda x: 1 if x > 0.5 else -1)
??基于評(píng)論內(nèi)容的情感得分,得到下方的直方圖,從圖中不難看出舌稀,對(duì)竇唯音樂(lè)的評(píng)論多是積極正面的:
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()
??再通過(guò)情感標(biāo)簽觀察啊犬,可知持正向情感的評(píng)論數(shù)是負(fù)向情感的近兩倍。
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()
1.5 詞云
??最后壁查,使用 jieba 進(jìn)行中文分詞(關(guān)于 jieba觉至,可參閱簡(jiǎn)明 jieba 中文分詞教程),并繪制詞云圖:
text = ''.join(str(s) for s in comment['content'] if s not in [None]) #將所有評(píng)論合并為一個(gè)長(zhǎng)文本
jieba.add_word('竇唯') #增加自定義詞語(yǔ)
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()
??在生成的詞云圖中(混入了一個(gè) 睡腿、语御、、席怪、
应闯,可能是特殊字符的問(wèn)題),最顯眼的是竇唯高級(jí)動(dòng)物的歌詞挂捻,結(jié)合高達(dá) 11722 的評(píng)論數(shù)碉纺,不難看出人們對(duì)這首歌的喜愛(ài)。其次是 喜歡, 聽(tīng)不懂, 好聽(tīng)
等詞語(yǔ)刻撒,在一定程度上體現(xiàn)了人們對(duì)竇唯音樂(lè)的評(píng)價(jià)骨田。再基于 TF-IDF 算法對(duì)評(píng)論進(jìn)行關(guān)鍵詞提取,得出前 30 的關(guān)鍵詞:
for x, w in anls.extract_tags(clean_text, topK=30, withWeight=True):
print('{0}: {1}'.format(x, w))
喜歡: 0.07174921826661623
搖滾: 0.06222465433996381
好聽(tīng): 0.048331581166697744
仙兒: 0.04814604948274102
王菲: 0.04271112348151552
竇仙: 0.027324893954643947
聽(tīng)不懂: 0.01956956751188709
幸福: 0.014775956892430308
成仙: 0.01465450183828875
汪峰: 0.014175488038594907
大仙: 0.013705819518861267
高級(jí): 0.013225888298888759
黑夢(mèng): 0.013076421076696725
前奏: 0.012872688959687885
黑豹: 0.012540924545728218
聽(tīng)歌: 0.012455923064269991
艷陽(yáng)天: 0.012455923064269991
動(dòng)物: 0.012396754282072616
聽(tīng)聽(tīng): 0.012369319024839337
聽(tīng)懂: 0.01160376390830011
吉他: 0.01142745810497296
忘詞: 0.011296092030755316
歌曲: 0.011181124179616048
希望: 0.01089713506654457
理解: 0.010537493766491456
厲害: 0.0104225740491279
哀傷: 0.009602942087618863
竇靖童: 0.009406198340815812
電影: 0.009266377909595709
送別: 0.008950847971089923
??排在前面的關(guān)鍵詞有“喜歡声怔、搖滾态贤、好聽(tīng)、聽(tīng)不懂”等醋火,還出現(xiàn)了 3 個(gè)人名悠汽,分別是竇唯的前妻、女兒以及另一位中國(guó)搖滾代表人物芥驳。一些歌名(如“高級(jí)動(dòng)物”)柿冲、專輯名(如“黑夢(mèng)”)也出現(xiàn)在這列表中,可惜的是竇唯后來(lái)的作品并沒(méi)有出現(xiàn)(和“聽(tīng)不懂”多少有點(diǎn)關(guān)系)兆旬。而帶“仙”字的關(guān)鍵詞有 4 個(gè)姻采,“竇唯成仙了”。最有意思的彩蛋爵憎,莫過(guò)于"忘詞"這個(gè)關(guān)鍵詞,看樣子大家對(duì)竇唯在 94 年那場(chǎng)演唱會(huì)的忘詞婚瓜,還是記憶猶新宝鼓。