您的位置:首页 > 编程语言 > Python开发

《用python 玩转数据》项目——B站弹幕数据分析

2017-07-13 17:03 1141 查看
1. 背景
在视频网站上,一边看视频一边发弹幕已经是网友的习惯。在B站上有很多种类的视频,也聚集了各种爱好的网友。本项目,就是对B站弹幕数据进行分析。选取分析的对象是B站上点播量过1.4亿的一部剧《Re:从零开始的异世界生活》。2. 算法
分两部分: 第一部分:2.1 在《Re:从零开始的异世界生活》的首页面,找到共25集的所有对应播放链接和剧名的格式,获取每一集的播放链接,并保存。
2.2 从每一集的播放页面中,通过正则re获取它的cid号,获得cid号后,就可用于获取弹幕文件。由于是动态页面,所以使用了selenium库的PhantomJS()方法。
2.3 因为b站需要通过格式为:https://comment.bilibili.com/dmroll,{time},{cid}的链接获取弹幕历史文件,所以要获取30天内的弹幕历史,就要计算出最近30天的时间戳。最后将组装好的url 保存到history_danmu_url.txt 文件。
2.4 当弹幕文件的url准备完成后,就可以通过静态页面的获取方法requests.get(url) 获取弹幕页面。所有的弹幕历史格式:<d p="533.67199707031,1,25,41194,1498943949,0,7edeebe9,3511616609">刀还是没有枪快</d>
2.5 p这个字段里面的内容:(资料来自百度搜索)
0,1,25,16777215,1312863760,0,eff85771,42759017中几个逗号分割的数据第一个参数是弹幕出现的时间以秒数为单位。第二个参数是弹幕的模式1..3 滚动弹幕 4底端弹幕 5顶端弹幕 6.逆向弹幕 7精准定位 8高级弹幕第三个参数是字号, 12非常小,16特小,18小,25中,36大,45很大,64特别大第四个参数是字体的颜色以HTML颜色的十进制为准第五个参数是Unix格式的时间戳。基准时间为 1970-1-1 08:00:00第六个参数是弹幕池 0普通池 1字幕池 2特殊池【目前特殊池为高级弹幕专用】第七个参数是发送者的ID,用于“屏蔽此弹幕的发送者”功能第八个参数是弹幕在弹幕数据库中rowID 用于“历史弹幕”功能。2.6 逐个弹幕历史文件爬取,将内容一集的30天的弹幕历史,以['dtTime', 'danmu_model', 'font', 'rgb', 'stamp','danmu_chi', 'userID', 'rowID', 'message', 'episode'] 的格式保存到d1.csv,d2.csv,d3.csv,……的文件中
2.7 除了将30天的所有历史弹幕保存在一起外,爬虫程序,还将每一集视频,在B站的最新弹幕历史文件的内容单独保存在文件名格式为now1.csv,now2.csv,……的文件中。获取最新弹幕历史文件的链接格式是:https://comment.bilibili.com/{cid}.xml,它们已经被保存在”comment.txt”文件。

第二部分:2.8 保存了所有的数据后,对数据进行处理:先读取.csv文件,然后进行可视化分析:
2.8.1 每集弹幕总量的变化图
逐个读入弹幕历史文件:d1.csv,d2.csv,……经过去重后,统计出每集的弹幕总量: data = pd.read_csv(item.strip(),encoding='gbk')统计每一集,近30天,弹幕总量,保存在字典:{1:323233, 2:212121, .......}episode_comment_dic[data.loc[1,'episode']] = every_episode_comment(data)2.8.2 发弹幕总量top5用户
a.统计每一集,30天内的弹幕数量,依据弹幕数量,把用户排序,每一集排序后的结果是一个DataFrame: 结果的大致结构:user_sort_dic = {1: DataFrame1, 2:DataFrame2, ......,25: DataFrame25}。user_sort_dic[data.loc[1, 'episode']] =every_episode_usersort(data) b.把经过排序统计处理后的所有DataFrame 进行concat。然后就可以统计所有用户在30天内,对25集视频发送弹幕的数量。最后对用户排序。最终结果:d4_alldanmu_sort 是一个DataFrame 变量,将用户按弹幕数,降序排列。d3_all_user = (pd.concat([item for k, itemin user_sort_dic.items()]))d3_all_user['userID'] = d3_all_user.indexaSer =d3_all_user.groupby('userID').episode.sum()d4_alldanmu_sort =pd.DataFrame(aSer).sort_values(by='episode', ascending=False)2.8.3 用户发弹幕长度分布
统计发送弹幕的字符串长度。 danmu_length_dic[data.loc[1,'episode']] = static_danmu_length(data)2.8.4 用户发弹幕数量分布
统计一集,用户发送弹幕数量的百分比分布图。d_tmp = user_sort_dic[i] #统计用户排名时已经有数据了 every_episode_danmu_pie(d_tmp, i)2.8.5 每集弹幕密度变化图
弹幕都有一个时间参数,代表了在视频的什么时间发了弹幕。可以统计出在相同时间参数发出的弹幕量,然后再画出“时间--弹幕”折线图,就可以看到弹幕量的变化了。2.8.6 每集热词画出词云
对每一集的弹幕文本进行分析。先用jieba词库进行分词,然后逐行对弹幕进行词频统计,最后用WordCloud 画出词云图。
3. 安装
使用的是anaconda的环境。所以直接在anaconda里进行安装:pip install -ihttps://pypi.tuna.tsinghua.edu.cn/simple jiebaconda –c https://conda.anaconda.org/conda-forge wordecloud #词云只支持到python3.4

4. 统计结果
4. 1 每集弹幕总量变化



4.2 发送弹幕总量top5 用户



4.3 用户发送弹幕长度分布



4.4 用户发送弹幕数量分布




4.5 每集弹幕密度分布图



4.6 每集的词云




5. 结果分析
通过以上的图表数据可以看出用户的弹幕的一些行为。 5.1 25集剧里,第一集和最后一集,弹幕数量是最多的,这和其他剧的开头和结尾两部的一样。中间的18集也获得了很多的弹幕数,原来是在那集里剧情出现了很大的变化,吸引了大家的讨论。 5.2 统计了25部剧,用户ID为356ed98的用户共发送了580多条弹幕,算是超级粉丝了。 5.3 通过弹幕数量和长度的分布,可以看出: 绝大部分用户只会发送2条以内的弹幕。 参与弹幕讨论的用户以10个字符以内的短语为主。5.4 统计每一集的弹幕密度,可以通过用户观看时的讨论,大致确定视频的精彩点。5.5 通过词云图,可以大致看出用户的分布。例如第一集里“重温”作为热词显示里。说明现在有很多用户是会多次观看这部剧的。

6. 参考代码
6.1 爬虫部分:

# -*- coding: utf-8 -*-
"""
Created on Mon Jul 10 16:34:27 2017

@author: ahchpr

filename: re_zero_bili.py
"""

import requests, csv, re, time
from bs4 import BeautifulSoup as BS
from selenium import webdriver
import datetime
from multiprocessing import Pool
import sys

# Re:从零开始的异世界生活 的总剧情首页
first_url = 'https://bangumi.bilibili.com/anime/3461'
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko)'}
one_url = 'https://bangumi.bilibili.com/anime/3461/play#86298'
#history_danmu_url = 'https://comment.bilibili.com/dmroll,time,cid'
#now_danmu_url = 'https://comment.bilibili.com/{}.xml'.format(danmu_id)

def get_danmu_id(url):
MyDriver = webdriver.PhantomJS()
MyDriver.get(url)
time.sleep(3)
danmu_id = re.findall(r'cid=(\d+)&', MyDriver.page_source)[0]
return (danmu_id)

def sele_get_first(url):
MyDriver = webdriver.PhantomJS()
MyDriver.get(url)
time.sleep(5)
response = MyDriver.page_source.encode('utf-8')
page = response.decode('utf-8')
return (page)

def sele_get_re_list(page):
pattern = re.compile('<a.*?href="(.*?)" title="(.*?)" target.*? class="v1-complete-text"><div class=')
abstract = re.findall(pattern, page)
return (abstract)

def request_get_comment(url):
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko)'}
episode = url.split(" ")[0]
url = url.split(" ")[1].strip()
response = requests.get(url=url, headers=headers)

soup = BS(response.text, 'lxml')
result = soup.find_all('d')
if  len(result) == 0:
return (result)

all_list = []
for item in result:
#        danmu_list.append(item.get('p').split(",").append(item.string))
danmu_list = item.get('p').split(",")
danmu_list.append(item.string)
#        danmu_list[0] = sec_to_str(danmu_list[0])
#        danmu_list[4] = time.ctime(eval(danmu_list[4]))
danmu_list.append(episode)
#        print(danmu_list)
all_list.append(danmu_list)
return (all_list)

"""将秒转换成固定格式 "hh:mm:ss"
"""
def sec_to_str(seconds):
seconds = eval(seconds)
m, s = divmod(seconds, 60)
h, m = divmod(m, 60)
dtEventTime = "%02d:%02d:%02d" % (h, m, s)
return (dtEventTime)

"""计算最近30天的每天的时间戳,并返回,用于获取历史弹幕
"""
def time_to_stamp():
today = datetime.date.today()
end_day = datetime.datetime(today.year, today.month, today.day)
start_day = end_day - datetime.timedelta(30)
gap_day_sum = 30
stamp_list = []
for i in range(1, gap_day_sum):
tmp = start_day + datetime.timedelta(i)
stamp_list.append(int(time.mktime(tmp.timetuple())))

return (stamp_list)

def csv_write(tablelist, num):
tableheader = ['dtTime', 'danmu_model', 'font', 'rgb', 'stamp', 'danmu_chi', 'userID', 'rowID', 'message', 'episode']
file_name = "now{}.csv".format(num)
print(file_name)
with open(file_name, 'w', newline='', errors='ignore') as fd:
writer = csv.writer(fd)
writer.writerow(tableheader)
for row in tablelist:
writer.writerow(row)

if __name__ == "__main__":
sys.setrecursionlimit(1000000)
"""爬取首页,获取共25话 《re:从零开始的异世界生活》 的播放连接
"""
page = sele_get_first(first_url)
re_list = sele_get_re_list(page)
#    print(len(re_list))

"""以字典的形式保存例如:
{'1': ['初始的终结与结束的开始', 'https://bangumi.bilibili.com/anime/3461/play#85754'],...}
"""
re_dict = {}
for item in re_list:
re_dict[item[1].split(" ")[0]] = [item[1].split(" ")[1], item[0]]
#    print(re_dict)

"""获取每一话的播放连接,保存成列表
"""
re_url_list = []
for i in range(1, len(re_dict)+1):
re_url_list.append( re_dict[str(i)][1] )

"""利用进程池,获取每一话的弹幕文件连接,
"""
re_danmu_id_list = []
pool = Pool(14)
re_danmu_id_list = pool.map(get_danmu_id, re_url_list)
pool.close()
pool.join()
re_danmu_id_dict = {}
for n, p in enumerate(re_danmu_id_list):
re_danmu_id_dict[str(n+1)] = p

"""将25话剧集的各自的最新的弹幕文件连接保存到一个文档 comment.txt,按照剧集的顺序保存
"""
with open('comment.txt', 'w') as fd:
for i in range(len(re_danmu_id_list)):
fd.write('{} https://comment.bilibili.com/{}.xml\n'.format(i+1, re_danmu_id_list[i]))

history_danmu_url_list = []
stamp_list = time_to_stamp()
for i in range(1, len(re_danmu_id_list)+1):
for stamp in stamp_list :
history_danmu_url = '{} https://comment.bilibili.com/dmroll,{},{}'.format(i, stamp, re_danmu_id_dict[str(i)])
history_danmu_url_list.append(history_danmu_url)
history_danmu_url_list.append('{} https://comment.bilibili.com/{}.xml'.format(i, re_danmu_id_dict[str(i)]))

with open('history_danmu_url.txt', 'w') as fd:
for line in history_danmu_url_list:
fd.write("{}\n".format(line))

all_list = []

'''把每一集的弹幕文件链接,按照集数,整理到一个字典,
'''
url_dict ={}
with open ("history_danmu_url.txt", 'r') as fd:
url_whole = fd.readlines()
print(len(url_whole))
for i in range(1, len(re_danmu_id_list)+1):
url_dict[str(i)] = [line for line in url_whole if int(line.split(" ")[0])==i]
print (len(url_dict))

'''按照集数,取出弹幕链接,进行爬虫,获取弹幕记录,并保存到.csv 文件
'''
for i in range(1, len(url_dict)+1):
n = 0
tmp_to_get_url = url_dict[(str(i))]
file_name = "d{}.csv".format(i)
tableheader = ['dtTime', 'danmu_model', 'font', 'rgb', 'stamp', 'danmu_chi', 'userID', 'rowID', 'message', 'episode']
with open(file_name, 'a',  newline='', errors='ignore') as fd:
writer = csv.writer(fd)
writer.writerow(tableheader)
for url in tmp_to_get_url :
all_list = request_get_comment(url)
if all_list:
for row in all_list:
writer.writerow(row)
print("\n\n\n\n\n")
n = n+1
print(n)
del (all_list)
del tmp_to_get_url

"""获取保存最新历史弹幕文件
"""
now_danmu_all = {}
with open ('comment.txt', 'r') as fd:
for url in fd:
now_danmu_all[int(url.strip().split(" ")[0])] = request_get_comment(url)

for num, data in now_danmu_all.items():
csv_write(data, num)


6.2 数据统计可视化分析部分:

# -*- coding: utf-8 -*-
"""
Created on Wed Jul 12 08:43:04 2017

@author: ahchpr

filename: static_danmu_comment.py

"""

import pandas as pd
import matplotlib.pyplot as plt
import os
from scipy.misc import imread
from wordcloud import WordCloud
import jieba
import jieba.posseg as pseg
import math

def danmu_compress_plot(data, num):
plt.cla()
plt.xlabel(u"视频时间", fontproperties='SimHei')
plt.ylabel(u"弹幕量", fontproperties='SimHei')
plt.title(u'第{}集_时间轴弹幕变化'.format(num), fontproperties='SimHei')

keys = [item for item in data.index ]

values = [item for item in data.dtTime]

"""弹幕密度折线图
"""
plt.plot(keys, values)
plt.show()

def danmu_compress(data):
df = data.drop_duplicates()
dd = df.copy()
#    round_dic = {}
#    向下取“整秒数”
#    round_dic['dtTime_new'] = [math.floor(item) for item in dd.dtTime]
dd['dtTime_new'] = [math.floor(item) for item in dd.dtTime]
#    dd.sort_values(by='dtTime_new', inplace=True)
#    print (dd.iloc[1:200, [6, 0, 10]])
dc = dd.groupby('dtTime_new').count()
result = dc.sort_index()
return (result)

'''result  大概的结构:只写出了一列参考,其中dtTime就是在0秒开始时的弹幕数量
dtTime
dtTime_new
0               75
1               37
2               34
'''

def extract_words(data, num):

df = data.drop_duplicates()
dd = df.copy()
message_list = [str(item) for item in dd.message]

stop_words = set(line.strip() for line in open('stopwords.txt', encoding='utf-8'))

newslist = []
for subject in message_list:
if subject.isspace():
continue
# segment words line by line
word_list = pseg.cut(subject)
for word, flag in word_list:
if not word in stop_words and flag == 'n':
newslist.append(word)

d = os.path.dirname(__file__)
mask_image = imread(os.path.join(d, "qiaodan.png"))
content = ' '.join(newslist)
wordcloud = WordCloud(font_path='simhei.ttf', background_color="grey", mask=mask_image, max_words=40).generate(content)

# Display the generated image:
file_name = u"第{}集_热词云.jpg".format(num)
plt.imshow(wordcloud)
plt.axis("off")
plt.title(file_name, fontproperties='SimHei')
wordcloud.to_file(file_name)
plt.show()
plt.cla()
plt.close()

def static_danmu_length(data):
df = data.drop_duplicates()
dd = df.copy()
dd['message_len'] = [len(str(item)) for item in df.message] #统计每条弹幕的长度
d1 = dd.loc[:, ['userID', 'message', 'message_len', 'episode']]
dr = d1.copy()
return (dr)

def every_episode_usersort(data):
df = data.drop_duplicates()
dd = df.groupby("userID").count()
user_sort = dd.sort_values(by='episode', ascending=False).loc[:,['episode']]
return (user_sort)

def every_episode_user(data):
'''30天内共有多少用户发弹幕
'''
df = data.drop_duplicates()
dd = df.groupby("userID").count()
user_sum = len(dd)
return (user_sum)

def every_episode_comment(data):
'''30天内有多少弹幕发出
'''
df = data.drop_duplicates()
danmu_sum = len(df)
return (danmu_sum)

def top_user_danmu(data):
plt.cla()
ing = range(5)
x = data.head(5).episode.index
y = data.head(5).episode.values
plt.xticks(ing, x, rotation=30)
plt.xlabel(u"用户ID", fontproperties='SimHei')
plt.ylabel(u"发弹幕数量", fontproperties='SimHei')
plt.title(u"发弹幕数top5用户",fontproperties='SimHei')
plt.bar(ing, y)
plt.show()

def every_episode_comment_change(episode_comment_dic):
plt.cla()
plt.xlabel(u"剧集", fontproperties='SimHei')
plt.ylabel(u"弹幕量", fontproperties='SimHei')
plt.title(u'每集弹幕总量变化', fontproperties='SimHei')
keys = range(1, len(episode_comment_dic)+1)
values = []
for i in keys:
values.append(episode_comment_dic[i])

"""每一集弹幕总量的折线变化图
"""
plt.plot(keys, values)
plt.show()

def every_episode_danmu_pie(d_tmp, num):

a1 = float(len(d_tmp[(d_tmp.episode>=1) & (d_tmp.episode<=2)]))
a2 = len(d_tmp[(d_tmp.episode>=3) & (d_tmp.episode<=8)])
a3 = len(d_tmp[(d_tmp.episode>=9) & (d_tmp.episode<=20)])
a4 = len(d_tmp[(d_tmp.episode>=21) ])

s =  a1 + a2 + a3 + a4
s = float(s)
li = [a1 , a2 , a3 , a4  ]
xp = []
for i in li:
i = float(i)
if i<=0:
t = 0
xp.append(t)
else:
t = (i/s*100)
xp.append(t)

plt.rcParams['font.sans-serif']=['SimHei'] #用来正常显示中文标签
plt.rcParams['axes.unicode_minus']=False #用来正常显示负号

plt.figure(figsize=(6,9))
labels = [u'1-2条', u'2-8条', u'9-20条', u'21条以上' ]
sizes = xp
colors = ['red', 'yellow', 'gray', 'lightskyblue']
explodes = [0 , 0, 0, 0.6]

plt.axis('equal')
plt.title(u'第{}集_用户发送弹幕数量百分比分布图'.format(num))
plt.pie(sizes,  labels=labels,explode=explodes, colors=colors, labeldistance=0.5,
autopct = '%2.2f%%', startangle = 90, pctdistance = 0.8)

plt.show()
plt.close()

def danmu_length_pie(d_tmp, num):
a1 = float(len(d_tmp[(d_tmp.message_len>=1) & (d_tmp.message_len<=4)]))
a2 = float(len(d_tmp[(d_tmp.message_len>=5) & (d_tmp.message_len<=10)]))
a3 = float(len(d_tmp[(d_tmp.message_len>=11) & (d_tmp.message_len<=15)]))
a4 = float(len(d_tmp[(d_tmp.message_len>=16) ]))
s =  a1 + a2 + a3 + a4
s = float(s)
li = [a1 , a2 , a3 , a4  ]
xp = []
for i in li:
i = float(i)
if i<=0:
t = 0
xp.append(t)
else:
t = (i/s*100)
xp.append(t)

plt.rcParams['font.sans-serif']=['SimHei'] #用来正常显示中文标签
plt.rcParams['axes.unicode_minus']=False #用来正常显示负号

plt.figure(figsize=(6,9))
labels = [u'1-4个', u'5-10个', u'11-15个', u'16个以上' ]
sizes = xp
colors = ['red', 'yellow', 'gray', 'lightskyblue']
explodes = [0 , 0, 0, 0.09]
plt.axis('equal')
plt.title(u'第{}集_用户发送弹幕长度百分比分布图'.format(num))
plt.pie(sizes,  labels=labels, colors=colors, explode=explodes, labeldistance=0.5,
autopct = '%2.2f%%', startangle = 90, pctdistance = 0.8)

plt.show()
plt.close()

#秒转换成时间
def sec_to_str(seconds):
seconds = eval(seconds)
m, s = divmod(seconds, 60)
h, m = divmod(m, 60)
length_time = "%02d:%02d:%02d" % (h, m, s)
return (length_time)

if __name__ == "__main__":

path = os.getcwd()
path_list = []
for i in range(1, 26):
path_list.append(path + "\\d{}.csv".format(i))

episode_comment_dic = {}
user_sum_dic = {}
user_sort_dic = {}
danmu_length_dic = {}
reyun_data_dic = {}
for item in path_list:
'''读取csv数据源文件,每一集的近30天弹幕都保存在一个csv文件
'''
data = pd.read_csv(item.strip(), encoding='gbk')

'''统计每一集,近30天,弹幕总量,保存在字典:{1:323233, 2:212121, .......}
'''
episode_comment_dic[data.loc[1, 'episode']] = every_episode_comment(data)

'''统计每一集,近30天,共有多少用户发了弹幕,保存在字典:{1:3737, 2:34234,......}
'''
user_sum_dic[data.loc[1, 'episode']] = every_episode_user(data)

'''统计每一集,30天内的弹幕数量,依据弹幕数量,把用户排序,每一集排序后的结果是一个DataFrame,
user_sort_dic = {1: DataFrame1, 2:DataFrame2, ......, 25: DataFrame25}
'''
user_sort_dic[data.loc[1, 'episode']] = every_episode_usersort(data)

'''统计发送弹幕的字符串长度
'''
danmu_length_dic[data.loc[1, 'episode']] = static_danmu_length(data)

'''统计每一集的分词,热词,词云
'''
reyun_data_dic[data.loc[1, 'episode']] = data.copy()

'''统计每一集,近30天的弹幕量,视频里的每一秒的弹幕数量,弹幕密度
'''
#        danmu_compress_dic[data.loc[1, 'episode']] =  danmu_compress(data)

del data

print(episode_comment_dic)
print(user_sum_dic)

'''把经过排序统计处理后的所有DataFrame 进行concat。然后就可以统计所有用户在30天内,对25集视频
发送弹幕的数量。最后对用户排序。最终结果:
d4_alldanmu_sort 是一个DataFrame 变量,将用户按弹幕数,降序排列。
'''
d3_all_user = (pd.concat([item for k, item in user_sort_dic.items()]))
d3_all_user['userID'] = d3_all_user.index
aSer = d3_all_user.groupby('userID').episode.sum()
d4_alldanmu_sort = pd.DataFrame(aSer).sort_values(by='episode', ascending=False)

'''绘制折线图:25集视频,近30天每集弹幕总数
'''
every_episode_comment_change(episode_comment_dic)

'''柱状图:25集视频,近30天,所有用户中,发弹幕数量最多的5个用户
'''
top_user_danmu(d4_alldanmu_sort)

'''统计一集,用户发送弹幕数量的百分比分布图
'''
for i in range(1, len(user_sort_dic)+1):
d_tmp = user_sort_dic[i]
every_episode_danmu_pie(d_tmp, i)
del d_tmp

'''统计用户发送弹幕的长度分布百分比
'''
for i in range(1, len(user_sort_dic)+1):
d_tmp = danmu_length_dic[i]
danmu_length_pie(d_tmp, i)
del d_tmp

'''分析弹幕密度,使用当前最新的历史弹幕文件分析
'''
now_danmu_list = []
for i in range(1, 26):
now_danmu_list.append(path + "\\now{}.csv".format(i))

danmu_compress_dic = {}
for item in now_danmu_list:
data = pd.read_csv(item.strip(), encoding='gbk')
danmu_compress_dic[data.loc[1, 'episode']] =  danmu_compress(data)

for num, data in danmu_compress_dic.items():
danmu_compress_plot(data, num)

"""绘制热词云图,热词的运行时间太久了,先关闭
"""
for num, data in reyun_data_dic.items():
extract_words(data, num)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息