解名缰 鸟倦飞

B站1000多集的柯南,该怎么追?

2020 年 05 月 08 日 | 分类于 生活R

在当前这个人均B站大会员的时代,上B站刷弹幕已经成了很多90后中老年前浪们日常的消遣娱乐生活。前不久B站买下了《名侦探柯南》的版权,一次性更新到了1031集(实际上暂缺942到993集,所以实际上更新了979集)。现在问题来了,1000多集的《柯南》肯定没法一集一集地看,那么应该如何科学摸鱼规划时间,做到劳逸结合呢?显然,下图给出的建议肯定是不可取的。弹幕里这些一口气肝1031集的同学,你们真的是认真的吗?

Conan danmu

其实既然是在B站,那么必不可少的一项体验就是跟着弹幕一起吐槽越来越飘逸的剧情,比如为什么柯南越来越明目张胆,连麻醉枪都不用就开始推理了,比如为什么过了一千多集小兰还是命案现场第一个发出惊呼的人。既然很多剧情已经看过了,那么现在重新追剧的动力似乎就变成了弹幕主打、剧情辅助的观影模式了。于是我有了一个大胆的想法,能不能直接找出弹幕最多的那些剧集,以此作为观影指南呢?

这个目标看上去非常简单,只要点进剧集网页,然后查看弹幕数即可。但别忘了,B站一次性上传了近一千集,即使手速足够快,也要考虑到网速和页面加载的问题。假设一个人可以用10秒钟的时间查询到一集的弹幕量,那算下来记录所有剧集的弹幕数差不多也得花三个小时,有这个时间都可以看好几集正片了。

有没有更好的办法呢?当然是有的,毕竟我们号称是数据科学家(我不是),每天都在处理海量的数据(我没有),其中第一步的数据收集自然是不在话下了(别瞎说)。由于平时用得最多的编程语言依然还是 R,因此在这里就介绍一些基本的收集B站弹幕数据的方法。当然,用 R 的另一个好处是摸鱼不会被发现有许多强大的可视化工具可以使用,我会在最后提到其应用。

首先,虽然B站给大部分人的感觉依然是二次元的聚集地(最近靠《后浪》成功破圈,但争议也远超以往,此为后话),但有一说一,B站对于开发者还是非常友好的。许多基础的数据,如弹幕、评论、视频基本信息等,都有非常成熟和稳定的 API 可供使用。比如,如果我们知道了某个视频的弹幕 ID(通常称为 cid),例如 cid=183362119,那么下面的这个网址就会返回该视频的实时弹幕:

http://api.bilibili.com/x/v1/dm/list.so?oid=183362119

你可以试着在浏览器里访问这个网址,它的返回结果不是一般的网页,而是一个 XML 文件。XML 文件也是纯文本,但要从中提取出有用的信息还需要借助一些工具。下面这段 R 代码用到了 curlxml2 这两个软件包,前者用来模拟浏览器访问网址的过程,后者则是去解析得到的 XML 文件。

library(curl)
library(xml2)
api = "http://api.bilibili.com/x/v1/dm/list.so?oid=183362119"
## 获取链接所指内容
ret = curl_fetch_memory(api)
## XML 文本存储在 ret$content 中
doc = read_xml(ret$content, encoding = "UTF-8")
## 获取 XML 中的节点,其中弹幕存储在 <d></d> 标签中
nodes = xml_find_all(doc, ".//d")
danmu = xml_text(nodes)
head(danmu)
[1] "干杯吧!!!!" "这波币值得"     "天啊"           "十年了!!!!"
[5] "居然能全屏"     "哇!!!"      
length(danmu)
3000

从中,我们就提取出了3000条弹幕。为什么是3000这么整的一个数?很遗憾,这是因为B站对每一个视频的当前弹幕数都做了限制,比如这个视频其实是《柯南》的第一集,弹幕早就过万了,但新的弹幕会把旧的弹幕挤掉,因此在观看视频的时候最多只能看到3000条,而我们也无法通过公开的方法获取那些已经被挤掉的弹幕。

至此,问题已经解决了一半:只要知道了视频的 cid,我们就可以通过上面的方法获取弹幕数,甚至更进一步,可以获取弹幕的全部文本。那么第二个问题自然就来了:如何知道所有剧集的 cid 呢?其实,方法很粗暴但也很有效:点开《柯南》剧集的网页(https://www.bilibili.com/bangumi/play/ep321808),然后查看页面源代码,我们就会发现如下的一串信息(这里我经过了格式化整理):

[
    {
        "loaded": true,
        "id": 321808,
        "badge": "",
        "badgeType": 0,
        "epStatus": 2,
        "aid": 710444604,
        "bvid": "BV1PQ4y1N7V8",
        "cid": 183362119,
        "from": "bangumi",
        "cover": "\u002F\u002Fi0.hdslb.com\u002Fbfs\u002Farchive\u002F82d4523e2562748d050a8d8ec7ebc03fbe1a15a1.jpg",
        "title": "1",
        "titleFormat": "第1话",
        "vid": "",
        "longTitle": "云霄飞车杀人事件",
        "hasNext": true,
        "i": 0,
        "sectionType": 0,
        "releaseDate": "",
        "skip": {},
        "hasSkip": false
    },
    ...
]

这其实是另一种网页中常见的存储数据的格式,称为 JSON。其中方括号代表了这是一串数组,而每一个大括号包含的内容是一个元素(这里只展示了一条)。只要将这一大串内容存储到一个文件 ep_list.json 中,就可以丢到 R 中进行整理了。在 R 中,可以使用 jsonlite 包读取 JSON 文件并转换成 R 中常见的数据类型:

library(jsonlite)
library(dplyr)
dat = read_json("ep_list.json", simplifyVector = TRUE)
dat %>% as_tibble() %>% select(cid, titleFormat, longTitle)
# A tibble: 979 x 3
         cid titleFormat longTitle           
       <int> <chr>       <chr>               
 1 183362119 第1话       云霄飞车杀人事件    
 2 183362192 第2话       董事长千金绑架事件  
 3 183362280 第3话       偶像密室杀人事件    
 4 183362362 第4话       大都会暗号地图事件  
 5 183362432 第5话       新干线大爆破事件    
 6 183362527 第6话       情人节杀人事件      
 7 183362614 第7话       每月一件礼物威胁事件
 8 183362677 第8话       美术馆杀人事件      
 9 183362762 第9话       天下第一夜祭杀人事件
10 183362916 第10话      足球选手恐吓事件    
# ... with 969 more rows

没错,转换的结果是一个数!据!框!

数据科学家最喜欢的数据格式。

——沃茨基·硕德

于是我们很轻易地从 JSON 文件中获取了每一集的标题和 cid,再结合之前的 curl 访问和 XML 解析,我们便能最后整理出每一集的弹幕量了。

但!是!

凡事都有但是。需要格外小心的是,用 curl 去访问网站是要遵从一定的技术道德的,如果将 curl 包含在一个循环中,那么在循环之间应该使用 Sys.sleep(1) 等命令人为增加一些访问间隙,否则就跟对网站发起攻击没有区别。当然像B站这种大型网站都有一些防止频繁访问的机制,比如我之前有一次忘了设置访问间隙,导致本机的 IP 地址直接被B站关小黑屋了。为了避免误操作,这里我就不贴详细代码了,仅给出最后的部分结果,当然这个结果很显然会实时变化,因此只是作展示之用。

# A tibble: 979 x 2
    num_danmu title                                        
        <int> <chr>                                        
  1      3000 第1话-云霄飞车杀人事件                       
  2      3000 第2话-董事长千金绑架事件                     
  3      3000 第3话-偶像密室杀人事件                       
  4      3000 第5话-新干线大爆破事件                       
  5      3000 第6话-情人节杀人事件                         
  6      3000 第7话-每月一件礼物威胁事件                   
  7      3000 第35话-山庄绷带怪人之杀人事件(上集)        
  8      3000 第51话-图书馆杀人事件                        
  9      3000 第136话-来自黑暗组织的女子 大学教授杀人事件  
 10      3000 第908话-樱花班的回忆(兰GIRL)               
 11      3000 第1028话-目标是警视厅交通部(一)            
 12      3000 第1029话-目标是警视厅交通部(二)            
 13      3000 第1031话-目标是警视厅交通部(四)            
 14      2930 第4话-大都会暗号地图事件                     
 15      2903 第78话-柯南对怪盗基德                        
 16      2859 第11话-钢琴奏鸣曲《月光》杀人事件            
 17      2847 第8话-美术馆杀人事件                         
 18      2803 第1030话-目标是警视厅交通部(三)            
 19      2676 第138话-来自黑暗组织的女子 大学教授杀人事件  
 20      2663 第21话-鬼屋杀人事件                          
 21      2577 第10话-足球选手恐吓事件                      
 22      2465 第13话-步美被绑架了                          
 23      2460 第836话-绯色的真相                           
 24      2373 第235话-名侦探齐聚一堂!工藤新一VS怪盗基德   
 25      2366 第23话-豪华客轮连续杀人事件(上集)          
 26      2335 第44话-江户川柯南诱拐事件                    
 27      2323 第17话-古董收藏家杀人事件                    
 28      2307 第16话-消失的尸体                            
 29      2303 第22话-电视剧外景队杀人事件                  
 30      2246 第59话-福尔摩斯迷杀人事件(上集)            

你是否从中找到了自己的童年阴影呢?

又到了结尾彩蛋的时间。前面介绍了利用 R 中的相关软件包来抓取弹幕的操作,然后只需要再加上亿点点细节,就可以打造出一个实用的追剧可视化工具了:https://yixuanq.shinyapps.io/conan-danmu/

Add some details