写在前面:

前段时间偶然发现了一篇披着柯南外衣的技术文章,这不,本文是对其进行的复现,原文有错误的地方也进行了修正(本来以为很容易,没想到挺多坑…)。

1 爬取介绍

利用Chrome浏览器抓包可知,B站的弹幕文件以XML文档式进行储存,如下所示(共三千条实时弹幕):

其URL为:http://comment.bilibili.com/183362119.xml

数字183362119则代表该视频专属ID,通过改变数字即可得到相应的弹幕文件。打开第1集的视频,查看源码,如下图所示。
爬取B站柯南弹幕进行可视化_经验分享

不难看出,CID则是对应着各个视频的ID,接下来用正则表达式提取即可。
完整爬取代码如下:

import requests
import re
from bs4 import BeautifulSoup as BS
import os

path = 'D:/pycharm/Conan/柯南'
if os.path.exists(path) == False:
    os.makedirs(path)
os.chdir(path)


def gethtml(url, header):
    r = requests.get(url, headers=header)
    r.encoding = 'utf-8'
    return r.text


def crawl_comments(r_text):
    txt1 = gethtml(url, header)
    pat = '"cid":(\d+)'
    chapter_total = re.findall(pat, txt1)[1:-2]
    count = 1
    for chapter in chapter_total:
        url_base = 'http://comment.bilibili.com/{}.xml'.format(chapter)
        txt2 = gethtml(url_base, header)
        soup = BS(txt2, 'lxml')
        all_d = soup.find_all('d')
        with open('{}.txt'.format(count), 'w', encoding='utf-8') as f:
            for d in all_d:
                f.write(d.get_text() + '\n')
        print('第{}话弹幕写入完毕'.format(count))
        count += 1


if __name__ == '__main__':
    url = 'https://www.bilibili.com/bangumi/play/ep321808'
    header = {'user-agent': 'Opera/12.80 (Windows NT 5.1; U; en) Presto/2.10.289 Version/12.02'}
    r_text = gethtml(url, header)
    crawl_comments(r_text)

最终的全部弹幕文件都位于项目文件夹里的 柯南 文件夹下

爬取B站柯南弹幕进行可视化_经验分享_02

注:这里共爬取到1052个弹幕文件。(没有大会员也能爬弹幕耶~)
爬取B站柯南弹幕进行可视化_经验分享_03

2 弹幕可视化

2.1 主要人物讨论总次数分析

2.1.1 统计人数总次数

这里用到了 role.txt 文件,原文并没有给我们这个文件,我从代码中也没看出来长什么样,就自己写了个文件,然后后面的原文代码只需要进行相应的修改就行。

注:role.txt是主要人物名文件(需考虑到弹幕一般不会对人物的全名进行称呼,多数使用的是昵称,否则可能与实际情况相差较大。)

# role.txt
柯南
兰
新一
小哀
平次
小五郎
园子
琴酒
秀一
贝姐
步美
光彦
和叶
安室透
元太
朱蒂
伏特加
有希子
优作
妃英理
基德
阿笠博士
目暮警官
朗姆

然后统计上面文件中的人物在每一个弹幕文件中出现的次数。

import jieba
import os
import pandas as pd
os.chdir('D:/pycharm/Conan')
jieba.load_userdict('role.txt')
role = [i.replace('\n', '') for i in open('role.txt', 'r', encoding='utf-8').readlines()]
txt_all = os.listdir('./柯南/')
print(txt_all)
txt_all.sort(key=lambda x: int(x.split('.')[0]))  # 按集数排序
count = 1
​
df = pd.DataFrame()
for chapter in txt_all:
  names = {}
  data = []
  with open('./柯南/{}'.format(chapter), 'r', encoding='utf-8') as f:
    for line in f.readlines():
      poss = jieba.cut(line)
      for word in poss:
        if word in role:
          if names.get(word) is None:
            names[word] = 0
          names[word] += 1
    df_new = pd.DataFrame.from_dict(names, orient='index', columns=['{}'.format(count)])
    df = pd.concat([df, df_new], axis=1)
  print('第{}集人物统计完毕'.format(count))
  count += 1
df.T.to_csv('role_count.csv', encoding='gb18030')

2.1.2 可视化

import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
​
plt.rcParams['font.sans-serif'] = ['kaiti']
plt.style.use('ggplot')
df = pd.read_csv('role_count.csv', encoding='gbk')
df = df.fillna(0)  # 空缺处补0
df = df.drop([df.columns[0]], axis=1)  # 把第一列的序号删掉
plt.figure(figsize=(20, 10))
role_sum = df.sum().to_frame().sort_values(by=0, ascending=False)
g = sns.barplot(role_sum.index, role_sum[0], palette='Set3', alpha=0.8)
index = np.arange(len(role_sum))
for name, count in zip(index, role_sum[0]):
    g.text(name, count+50, int(count), ha='center', va='bottom',)
plt.title('B站名侦探柯南弹幕——主要人物讨论总次数分布')
plt.ylabel('讨论次数')
plt.savefig("B站名侦探柯南弹幕——主要人物讨论总次数分布")
plt.show()

爬取B站柯南弹幕进行可视化_经验分享_04

虽说是万年小学生,柯南还是有变回新一的时候,且剧情也并不只是"找犯人—抓犯人"。接下来从数据的角度来,扒扒一些精彩剧情集数。

2.2 柯南变回新一集数统计

考虑到部分集数中新一是在回忆中出现的,为减少偏差,将讨论的阈值设为250次,绘制如下分布图
爬取B站柯南弹幕进行可视化_经验分享_05

其讨论次数结果及剧集名如下表所示:
爬取B站柯南弹幕进行可视化_经验分享_06

有兴趣的朋友可以码一下,除235集外,均是柯南变回新一的集数。

相关代码如下:

# -*-coding:utf-8-*-
import matplotlib.pyplot as plt
import pandas as pd
​
plt.rcParams['font.sans-serif'] = ['kaiti']
plt.style.use('ggplot')
​
df = pd.read_csv('role_count.csv', encoding='gbk')
df = df.fillna(0)
xinyi = df[df['新一'] >= 250]['新一'].to_frame()
print(xinyi)  # 新一登场集数
​
plt.figure(figsize=(20, 10))
plt.plot(df.index, df['新一'], label='新一', color='blue', alpha=0.6)
plt.annotate('集数:50,讨论次数:439',
             xy=(50, 439),
             xytext=(40, 420),
             arrowprops=dict(color='red', headwidth=8, headlength=8)
            )
plt.annotate('集数:235,讨论次数:392',
             xy=(235, 392),
             xytext=(225, 370),
             arrowprops=dict(color='red', headwidth=8, headlength=8)
            )
plt.annotate('集数:572,讨论次数:504',
             xy=(572, 504),
             xytext=(563, 480),
             arrowprops=dict(color='red', headwidth=8, headlength=8)
            )
plt.annotate('集数:825,讨论次数:307',
             xy=(825, 307),
             xytext=(815, 280),
             arrowprops=dict(color='red', headwidth=8, headlength=8)
            )
plt.hlines(xmin=df.index.min(), xmax=df.index.max(), y=250, linestyles='--', colors='red')
plt.legend(loc='best', frameon=False)
plt.xlabel('集数')
plt.ylabel('讨论次数')
plt.title('工藤新一讨论次数分布图')
plt.savefig("工藤新一讨论次数分布图")
plt.show()

以讨论次数最多的572集,绘制词云图(剔除了高频词“新一”,防止遗漏其他信息)代码如下所示:

# -*-coding:utf-8-*-
import jieba
from wordcloud import WordCloud
import matplotlib.pyplot as plt
​
stopwords = [line.strip() for line in open('Library/stopwords.txt', 'r', encoding='utf-8')]
with open('柯南/572.txt', 'r', encoding='utf-8') as f:
    txt = f.read()
    words = jieba.cut(txt)
    sentences = ""
    for word in words:
        if word in stopwords or word in '新一':
            continue
        sentences += str(word)+' '
    wordcloud = WordCloud(background_color='white',
                          font_path="Library/SourceHanSerif-Heavy.ttc",
                          width=2000,
                          height=2000,).generate(sentences)
    plt.imshow(wordcloud)
    plt.axis('off')
    plt.savefig("572集新一词云")
    plt.show()

爬取B站柯南弹幕进行可视化_经验分享_07

从图中可看出,出现频率较高地词有整容、平次、小兰、奥斯卡等。(看来凶手是整成了新一的模样进行犯罪的,还有新兰的感情戏在里面,值得一看)

2.3 主线集数内容分析

主线剧情主要是围绕着组织成员(琴酒、伏特加、贝尔摩德)展开,绘制分布图如下:

import matplotlib.pyplot as plt
import pandas as pd
​
df = pd.read_csv('role_count.csv', encoding='gbk')
df = df.fillna(0)  # 空缺处补0
​
plt.rcParams['font.sans-serif'] = ['kaiti']
plt.style.use('ggplot')
plt.figure(figsize=(20, 10))
names = ['琴酒', '伏特加', '贝姐']
colors = ['#090707', '#004e66', '#EC7357']
alphas = [0.8, 0.7, 0.6]
for name, color, alpha in zip(names, colors, alphas):
    plt.plot(df.index, df[name], label=name, color=color, alpha=alpha)
plt.legend(loc='best',frameon=False)
plt.annotate('集数:{},讨论次数:{}'.
             format(df['琴酒'].idxmax() + 1, int(df['琴酒'].max())),
             xy=(df['琴酒'].idxmax(), df['琴酒'].max()),
             xytext=(df['琴酒'].idxmax()+30, df['琴酒'].max()),
             arrowprops=dict(color='red', headwidth=8, headlength=8)
            )
plt.xlabel('集数')
plt.ylabel('讨论次数')
plt.title('酒厂成员讨论次数分布图')
plt.hlines(xmin=df.index.min(), xmax=df.index.max(), y=200, linestyles='--', colors='red')
plt.savefig("酒厂成员讨论次数分布图")
plt.show()# 输出主线剧集
mainline = set(list(df[df['贝姐'] >= 200].index)+list(df[df['琴酒'] >= 200].index))  # 伏特加可忽略不计
print(mainline)

爬取B站柯南弹幕进行可视化_经验分享_08

从上图分析可知,组织成员的行动基本一致,其中琴酒(原文统计的是我们贝姐)的人气在三人中是较高的,特别是在547集(【红与黑】系列),讨论次数高达410。此外,统计其讨论次数大于200次的集数,结果如下:
爬取B站柯南弹幕进行可视化_经验分享_09

以讨论次数最多的547集,绘制词云图(剔除了高频词“琴酒”,防止遗漏其他信息)代码如下所示:

# -*-coding:utf-8-*-
import jieba
from wordcloud import WordCloud
import matplotlib.pyplot as plt
​
stopwords = [line.strip() for line in open('Library/stopwords.txt', 'r', encoding='utf-8')]
with open('柯南/547.txt', 'r', encoding='utf-8') as f:
    txt = f.read()
    words = jieba.cut(txt)
    sentences = ""
    for word in words:
        if word in stopwords or word in '琴酒':
            continue
        sentences += str(word)+' '
    wordcloud = WordCloud(background_color='white',
                          font_path="Library/SourceHanSerif-Heavy.ttc",
                          width=2000,
                          height=2000,).generate(sentences)
    plt.imshow(wordcloud)
    plt.axis('off')
    plt.savefig("547集琴酒词云")
    plt.show()

爬取B站柯南弹幕进行可视化_经验分享_10

从图中可看出,出现频率较高地词有柯导、秀一、千层饼等。这是一场秀一假死的大戏,背后的谋划者就是柯南。

3 人物形象网络分析

3.1 合并弹幕文件

为尽可能反映出弹幕观众对人物形象的描述,考虑到一集弹幕共3000条,为减少运行成本,这里仅选取特定人物讨论次数最多的20集合并后再进行分析。

import os
import pandas as pd
​
df = pd.read_csv('role_count.csv',encoding='gbk')
df = df.fillna(0)
huiyuan_ep = list(df.sort_values(by='小哀', ascending=False).index[:20])
mergefiledir = '柯南'
file = open('txt_all.txt', 'w', encoding='UTF-8')
count = 0
for filename in huiyuan_ep:
    filepath = mergefiledir+'/'+str(filename)+'.txt'
    for line in open(filepath, encoding='UTF-8'):
        file.writelines(line)
    file.write('\n')
    count += 1
    print('第{}集写入完毕'.format(count))
file.close()

3.2 人物形象可视化

借助共现矩阵的思想,即同一句话中出现两个指定的词则计数1。指定起始点Source为灰原哀,代码如下所示:(注:其中,stopwods.txt为停止词文件,role.txt为人物昵称文件)

import codecs
import csv
import jieba
linesName = []
names = {}
relationship = {}
jieba.load_userdict('role.txt')
txt = [line.strip() for line in open('Library/stopwords.txt', 'r', encoding='utf-8')]
name_list = [i.replace('\n', '') for i in open('role.txt', 'r', encoding='utf-8').readlines()]
​
​
def base(path):
    with codecs.open(path, 'r', 'UTF-8') as f:
        for line in f.readlines():
            line=line.replace('\r\n', '')
            poss = jieba.cut(line)
            linesName.append([])
            for word in poss:
                if word in txt:
                    continue
                linesName[-1].append(word)
                if names.get(word) is None:
                    names[word] = 0
                    relationship[word] = {}
                names[word] += 1
    return linesName, relationship
​
​
def relationships(linesName, relationship, name_list):
    for line in linesName:
        for name1 in line:
            if name1 in name_list:
                for name2 in line:
                    if name1 == name2:
                        continue
                    if relationship[name1].get(name2) is None:
                        relationship[name1][name2] = 1
                    else:
                        relationship[name1][name2] += 1
    return relationship
​
​
def write_csv(relationship):
    csv_writer2 = open('edges.csv', 'w', encoding='gb18030')
    writer = csv.writer(csv_writer2, delimiter=',', lineterminator='\n')
    writer.writerow(['Source', 'Target', 'Weight'])
    for name, edges in relationship.items():
        for k, v in edges.items():
            if v > 10:
                writer.writerow([name, k, v])
    csv_writer2.close()
​
​
if __name__ == '__main__':
    linesName, relationship = base('txt_all.txt')
    data = relationships(linesName, relationship, name_list)
    write_csv(data)

将生成的文件导入Gephi,得到如下人物形象图

线条越粗的线,代表该人物特征越明显。不难看出,大家对于哀酱的评价主要是美腻、可爱、心疼

END

全部代码可参见GitHub爬取B站柯南弹幕进行可视化