成果展示



項目地址
完整代碼可在我的github中下載:https://github.com/XavierJiezou/python-danmu-analysis
爬取彈幕
可以看我之前寫的這篇文章:10行代碼下載B站彈幕
下載代碼
# download.py
'''依賴模塊 pip install requests '''
import re
import requests
url = input('請輸入B站視頻鏈接: ')
res = requests.get(url)
cid = re.findall(r'"cid":(.*?),', res.text)[-1]
url = f'https://comment.bilibili.com/{cid}.xml'
res = requests.get(url)
with open(f'{cid}.xml', 'wb') as f:
f.write(res.content)
樣例輸入
B站番劇《花丸幼稚園》第六集視頻播放地址:https://www.bilibili.com/bangumi/play/ep17617
樣例輸出
彈幕文件51816463.xml:https://comment.bilibili.com/51816463.xml
數據處理
下載彈幕文件51816463.xml后,我們打開看一下:
<?xml version="1.0" encoding="UTF-8"?>
<i>
<chatserver>chat.bilibili.com</chatserver>
<chatid>51816463</chatid>
<mission>0</mission>
<maxlimit>3000</maxlimit>
<state>0</state>
<real_name>0</real_name>
<source>k-v</source>
<d p="302.30800,1,25,16777215,1606932706,0,b105c2ee,41833218383544323">長頸鹿呢?還是大象呢?(</d>
<d p="608.17000,1,25,16707842,1606926336,0,af4597df,41829878640672775">我也不想的,實在是太大了呀</d>
<d p="249.95600,1,25,14811775,1606925877,0,af4597df,41829637765988357">真是深不可測啊</d>
此處省略很多字
</i>
可以看到xml文件中d標簽的text部分就是彈幕的文本,而d標簽的p屬性應該是彈幕的相關參數,共有8個,用逗號分隔。
參數略解:
- stime: 彈幕出現時間 (s)
- mode: 彈幕類型 (< 7 時為普通彈幕)
- size: 字號
- color: 文字顏色
- date: 發送時間戳
- pool: 彈幕池ID
- author: 發送者ID
- dbid: 數據庫記錄ID(單調遞增)
參數詳解:
① stime(float):彈幕出現時間,單位是秒;也就是在幾秒出現彈幕。
② mode(int):彈幕類型,有8種;小於8為普通彈幕,8是高級彈幕。
- 1~3:滾動彈幕
- 4:底端彈幕
- 6:頂端彈幕
- 7:逆向彈幕
- 8:高級彈幕
③ size(int):字號。
- 12:非常小
- 16:特小
- 18:小
- 25:中
- 36:大
- 45:很大
- 64:特別大
④ color(int):文字顏色;十進制表示的顏色。
⑤ data(int):彈幕發送時間戳。也就是從基准時間1970-1-1 08:00:00開始到發送時間的秒數。
⑥ pool(int):彈幕池ID。
- 0:普通池
- 1:字幕池
- 2:特殊池(高級彈幕專用)
⑦ author(str):發送者ID,用於"屏蔽此發送者的彈幕"的功能。
⑧ dbid(str):彈幕在數據庫中的行ID,用於"歷史彈幕"功能。
了解彈幕的參數后,我們就將彈幕信息保存為danmus.csv文件:

# processing.py
import re
with open('51816463.xml', encoding='utf-8') as f:
data = f.read()
comments = re.findall('<d p="(.*?)">(.*?)</d>', data)
# print(len(comments)) # 3000
danmus = [','.join(item) for item in comments]
headers = ['stime', 'mode', 'size', 'color', 'date', 'pool', 'author', 'dbid', 'text']
headers = ','.join(headers)
danmus.insert(0, headers)
with open('danmus.csv', 'w', encoding='utf_8_sig') as f:
f.writelines([line+'\n' for line in danmus])
數據分析
詞頻分析

# wordCloud.py
'''依賴模塊 pip install jieba, pyecharts '''
from pyecharts import options as opts
from pyecharts.charts import WordCloud
import jieba
with open('danmus.csv', encoding='utf-8') as f:
text = " ".join([line.split(',')[-1] for line in f.readlines()])
words = jieba.cut(text)
_dict = {}
for word in words:
if len(word) >= 2:
_dict[word] = _dict.get(word, 0)+1
items = list(_dict.items())
items.sort(key=lambda x: x[1], reverse=True)
c = (
WordCloud()
.add(
"",
items,
word_size_range=[20, 120],
textstyle_opts=opts.TextStyleOpts(font_family="cursive"),
)
.render("wordcloud.html")
)
情感分析

由餅狀圖可知:3000條彈幕中,積極彈幕超過一半,中立彈幕有百分之三十幾。
當然,彈幕調侃內容居中,而且有很多梗,會對情感分析造成很大的障礙,舉個栗子:
>>> from snownlp import SnowNLP >>> s = SnowNLP('阿偉死了') >>> s.sentiments 0.1373666377744408"阿偉死了"因帶有"死"字,所以被判別為消極情緒。但實際上,它反映的確是積極情緒,形容對看到可愛的事物時的激動心情。
# emotionAnalysis.py
'''依賴模塊 pip install snownlp, pyecharts '''
from snownlp import SnowNLP
from pyecharts import options as opts
from pyecharts.charts import Pie
with open('danmus.csv', encoding='utf-8') as f:
text = [line.split(',')[-1] for line in f.readlines()[1:]]
emotions = {
'positive': 0,
'negative': 0,
'neutral': 0
}
for item in text:
if SnowNLP(item).sentiments > 0.6:
emotions['positive'] += 1
elif SnowNLP(item).sentiments < 0.4:
emotions['negative'] += 1
else:
emotions['neutral'] += 1
print(emotions)
c = (
Pie()
.add("", list(emotions.items()))
.set_colors(["blue", "purple", "orange"])
.set_series_opts(label_opts=opts.LabelOpts(formatter="{b}: {c} ({d}%)"))
.render("emotionAnalysis.html")
)
精彩片段

由折線圖可知:第3分鍾,第8、第9分鍾,還有第13分鍾分別是該視頻的精彩片段。
# highlights.py
'''依賴模塊 pip install snownlp, pyecharts '''
from pyecharts.commons.utils import JsCode
from pyecharts.charts import Line
from pyecharts.charts import Line, Grid
import pyecharts.options as opts
with open('danmus.csv', encoding='utf-8') as f:
text = [float(line.split(',')[0]) for line in f.readlines()[1:]]
text = sorted([int(item) for item in text])
data = {}
for item in text:
item = int(item/60)
data[item] = data.get(item, 0)+1
x_data = list(data.keys())
y_data = list(data.values())
background_color_js = (
"new echarts.graphic.LinearGradient(0, 0, 0, 1, "
"[{offset: 0, color: '#c86589'}, {offset: 1, color: '#06a7ff'}], false)"
)
area_color_js = (
"new echarts.graphic.LinearGradient(0, 0, 0, 1, "
"[{offset: 0, color: '#eb64fb'}, {offset: 1, color: '#3fbbff0d'}], false)"
)
c = (
Line(init_opts=opts.InitOpts(bg_color=JsCode(background_color_js)))
.add_xaxis(xaxis_data=x_data)
.add_yaxis(
series_name="彈幕數量",
y_axis=y_data,
is_smooth=True,
symbol="circle",
symbol_size=6,
linestyle_opts=opts.LineStyleOpts(color="#fff"),
label_opts=opts.LabelOpts(is_show=True, position="top", color="white"),
itemstyle_opts=opts.ItemStyleOpts(
color="red", border_color="#fff", border_width=3
),
tooltip_opts=opts.TooltipOpts(is_show=True),
areastyle_opts=opts.AreaStyleOpts(
color=JsCode(area_color_js), opacity=1),
markpoint_opts=opts.MarkPointOpts(
data=[opts.MarkPointItem(type_="max")])
)
.set_global_opts(
title_opts=opts.TitleOpts(
title="",
pos_bottom="5%",
pos_left="center",
title_textstyle_opts=opts.TextStyleOpts(
color="#fff", font_size=16),
),
xaxis_opts=opts.AxisOpts(
type_="category",
boundary_gap=False,
axislabel_opts=opts.LabelOpts(margin=30, color="#ffffff63"),
axisline_opts=opts.AxisLineOpts(
linestyle_opts=opts.LineStyleOpts(width=2, color="#fff")
),
axistick_opts=opts.AxisTickOpts(
is_show=True,
length=25,
linestyle_opts=opts.LineStyleOpts(color="#ffffff1f"),
),
splitline_opts=opts.SplitLineOpts(
is_show=True, linestyle_opts=opts.LineStyleOpts(color="#ffffff1f")
)
),
yaxis_opts=opts.AxisOpts(
type_="value",
position="left",
axislabel_opts=opts.LabelOpts(margin=20, color="#ffffff63"),
axisline_opts=opts.AxisLineOpts(
linestyle_opts=opts.LineStyleOpts(width=2, color="#fff")
),
axistick_opts=opts.AxisTickOpts(
is_show=True,
length=15,
linestyle_opts=opts.LineStyleOpts(color="#ffffff1f"),
),
splitline_opts=opts.SplitLineOpts(
is_show=True, linestyle_opts=opts.LineStyleOpts(color="#ffffff1f")
),
),
legend_opts=opts.LegendOpts(is_show=False),
tooltip_opts=opts.TooltipOpts(trigger="axis", axis_pointer_type="line")
)
.render("highlights.html")
)
高能時刻
更多時候,我們可能對精彩片段不太關注,而是想知道番劇的名場面出自幾分幾秒,即高能時刻。
# highEnergyMoment.py
import re
with open('danmus.csv', encoding='utf-8') as f:
danmus = []
for line in f.readlines()[1:]:
time = int(float(line.split(',')[0]))
text = line.split(',')[-1].replace('\n', '')
danmus.append([time, text])
danmus.sort(key=lambda x: x[0])
dict1 = {}
dict2 = {}
control = True
for item in danmus:
if re.search('名場面(:|:)', item[1]):
print(f'{int(item[0]/60)}m{item[0]%60}s {item[1]}')
control = False
break
if '名場面' in item[1]:
minute = int(item[0]/60)
second = item[0] % 60
dict1[minute] = dict1.get(minute, 0)+1
dict2[minute] = dict2.get(minute, 0)+second
else:
pass
if control:
minute= max(dict1, key=dict1.get)
second = round(dict2[minute]/dict1[minute])
print(f'{minute}m{second}s 名場面')
輸出:9m29s 名場面:懷中抱妹鯊。我們去視頻中看一下,9m29s確實是名場面:

福利情節
字體顏色為黃色,也就是10進制顏色的值為16776960時,就是那種比較污的福利情節,同時為了防止異常,只有當該分鍾內出現黃色彈幕的次數≥3時,才說明該分鍾內是福利情節,並且輸出該分鍾內第一次出現黃色彈幕的秒數:
02m15s 吼吼吼
03m30s 什么玩意
06m19s 真的有那么Q彈嗎
08m17s 憋死
09m10s 前方萬惡之源
10m54s 噢噢噢噢
11m02s 這就是平常心
12m34s 這個我可以
17m19s 因為你是鋼筋混凝土直女
18m06s 假面騎士ooo是你嗎
19m00s 警察叔叔就是這個人
20m00s 金色傳說的說。。。
21m02s 嘿嘿嘿~
# textColor.py
with open('danmus.csv', encoding='utf-8') as f:
danmus = []
for line in f.readlines()[1:]:
time = int(float(line.split(',')[0]))
color = line.split(',')[3]
text = line.split(',')[-1].replace('\n', '')
danmus.append([time, color, text])
danmus.sort(key=lambda x: x[0])
dict1 = {}
dict2 = {}
for item in danmus:
if item[1] == '16776960':
minute = int(item[0]/60)
second = item[0] % 60
dict1[minute] = dict1.get(minute, 0)+1
if dict2.get(minute) == None:
dict2[minute] = f'{minute:0>2}m{second:0>2}s {item[2]}'
else:
pass
for key, value in dict1.items():
if value >= 3:
print(dict2[key])
