B 站直播間數據爬蟲

前言

起因

去年在 B 站發現一個後期超強的 UP 主:修仙不倒大小眼,專出 PDD 這樣知名主播的吃雞精彩集錦,漲粉超快。於是想怎麼做這樣的 UP,遇到的第一個問題便是素材,精彩時刻需要手動從直播錄播中剪輯,很低效。

用戶習慣

我經常看直播,但是很少發彈幕和送禮物,只有在主播玩出很溜的操作或講很好玩的事情時,才會發彈幕互動、送禮物支持,經常看直播的室友也是如此。

基於這個用戶習慣,不難推斷出在直播間的彈幕高峰或禮物高峰期,主播應該做了些好玩的事情,比如吃到雞了,或者全隊被殲滅之類的…這些時刻都可以作為精彩時刻的素材。能寫程序自動截取這些素材嗎?答案是肯定的。

實現效果

彈幕抓取

B 站直播間數據爬蟲

數據統計

B 站直播間數據爬蟲

根據彈幕和禮物高峰生成的精彩剪輯

B 站直播間數據爬蟲

實現思路

通過爬蟲抓取 B 站直播間數據,找出彈幕激增的時間點,使用 FFmpeg 自動剪輯時間點前後的視頻即可。

本文代碼:GitHub

> bilibili-live-crawler $ tree -L 2.
├── README.md
├── config.php # 配置文件:配置 FFmpeg 可執行文件的位置,錄像的保存路徑├── const.php # 常量文件:API 地址,定義數據庫用戶名和密碼、彈幕激增的判定參數等├── crawler.php # 連接並抓取彈幕服務器的數據├── cut_words

│ └── seg.php # 分詞腳本:將彈幕做分詞處理,可用於生成本次直播的詞圖├── db.sql # 數據存儲├── edit.php # 剪輯腳本├── functions.php # 公用函數└── visual_data.php # 直播數據可視化文件腳本

準備 API

以 B 站欠王癢局長為例,進入他的 528 直播間,打開 Chrome 的開發者工具,看 Network 容易找出這些 API:

直播間原始信息

熱門主播會有 2 個房間號:易識記的短房間號、原始長房間號,獲取主播原始直播間信息的 API:

Resquest: https://api.live.bilibili.com/room/v1/Room/room_init?id=528Response:
{ "code": 0, "data": { "room_id": 5441, // 開通直播間時的原始房間號,後邊會用到 "short_id": 528, // 短房間號
...
}
}

彈幕服務器信息

直播間在加載時,會請求彈幕服務器的地址,即是我們要去爬取數據的服務器:

Request: https://api.live.bilibili.com/api/player?id=cid:5441 // 5441 即原始房間號
Response:
...2243broadcastlv.chat.bilibili.com...

直播推流信息

直播間會有 3~4 個視頻推流地址,選用第一個主路線會更穩定:

Request: https://api.live.bilibili.com/room/v1/Room/playUrl?cid=5441Response:
{ "code": 0, "data": { "durl": [
{ "order": 1, "length": 0, "url": "https://bvc.live-play.acgvideo.com/live-bvc/671471/live_322892_3999292.flv?wsSecret=55083259fbc34c4227691ca0feb9c4b8&wsTime=1522465545" // flv 視頻格式的推流地址
},
...
}

協議分析

B 站和鬥魚一樣,為傳輸直播數據自己設計了協議頭部。需使用 Wireshark 抓包分析協議的細節,才能將爬蟲的請求偽裝成瀏覽器的請求,連接彈幕服務器去爬取直播間的數據。

找出彈幕服務器的 IP 地址:211.159.194.115

B 站直播間數據爬蟲

查看請求彈幕服務器的數據包:<code>ip.addr == 211.159.194.115/<code>

B 站直播間數據爬蟲

前邊三個包是我(10.0.1.34)與彈幕服務器(211.159.194.115)三次握手建立 TCP 連接的包。

請求的打包和解碼,我參考 2016.3 的博客:B站直播彈幕協議詳解,現在抓到的包協議頭與博客中的不一樣,B站重新修改過了,不過應該是為了兼容,這種舊協議頭還能用。

請求協議頭

下邊這個是打開進入直播間時,客戶端請求彈幕服務器的請求協議頭,響應協議頭類似:

00000000 00 00 00 35 00 10 00 01 00 00 00 07 00 00 00 01 ...5.... ........ # 數據包長度 # 意義不明 #請求類型:7進入直播間 # 包類型,1是數據包
# 2是心跳包 00000010 7b 22 72 6f 6f 6d 69 64 22 3a 31 30 31 36 2c 22 {"roomid ":1016,"
00000020 75 69 64 22 3a 31 35 35 39 37 33 36 38 35 37 32 uid":1559736857200000030 38 31 36 30 7d 8160} # 請求的數據

進入直播間,打包生成連接服務器的協議頭:

// $roomID 是直播間的長房間號// $uid 是當前登錄用戶的 uid,遊客的是隨機數function packMsg($roomID, $uid) {
$data = json_encode(['roomid' => $roomID, 'uid' => $uid]);
// 大端字節序,使用參數 N (4字節) 和 n(2字節) 打包請求
// 佔4字節的數據包長度:16字節協議頭長度 + 請求數據長度
// 佔2字節意義不明:00 10
// 佔2字節意義不明:00 01
// 佔4字節的請求類型:00 00 00 07
// 佔4字節的包類型:00 00 00 01
return pack('NnnNN', 16 + strlen($data), 16, 1, 7, 1) . $data;
}

響應數據

服務返回的 JSON 數據包的協議頭如下:

00000835 7b 22 69 6e 66 6f 22 3a 5b 5b 30 2c 31 2c 32 35 {"info": [[0,1,2500000845 2c 31 36 37 37 37 32 31 35 2c 31 34 35 37 39 35 ,1677721 5,145795...000008E5 5d 2c 22 63 6d 64 22 3a 22 44 41 4e 4d 55 5f 4d ],"cmd": "DANMU_M
000008F5 53 47 22 7d SG"}

解碼響應的數據體:

function decodeMessage($socket) { while (socket_last_error($socket)) { while ($out = socket_read($socket, 16)) {
$res = @unpack('N', $out); if ($res[1] != 16) { break;
}
}
$message = @socket_read($socket, $res[1] - 16);

$resp = json_decode($message, true); switch ($resp['cmd']) { case 'DANMU_MSG': // 彈幕消息
// info[1] 彈幕內容
// info[2][1] 發送者暱稱
echo $resp['info'][2][1] . " : " . $resp['info'][1] . PHP_EOL; break; case 'SEND_GIFT': // 直播間送禮物信息
$data = $resp['data']; // uname 發送者的暱稱
// giftName 贈送的禮物名稱
// unum 一次贈送的數量
// price 禮物的價值
echo $data['uname'] . ' 贈送' . $data['num'] . '份' . $data['giftName'] . PHP_EOL; break; case 'WELCOME': // 直播間歡迎信息
break; default: // 未知的消息類型
}
}
socket_close($socket);
}

心跳包

如果客戶端出現突然斷網等異常情況,服務端依舊會繼續推送數據,維護這種半打開的 TCP 連接將會浪費服務器的資源。客戶端可以每隔一小段時間給服務端發送心跳包來保活,如果服務端一定超時時間內沒收到某個客戶端的心跳包,就主動斷開連接。

B 站的彈幕服務器也有類似的機制,隨便打開一個未開播的直播間,抓包將看到每隔 30s 左右會給服務端發送一個心跳包,協議頭第四部分的值從 7 修改為 2 即可。如果不發送心跳包,彈幕服務器將在 1~2min 內主動斷開連接。

// 發送心跳包function sendHeartBeatPkg($socket) { // 包類型從數據包的 7 修改為心跳包的 2 

$str = pack('NnnNN', 16, 16, 1, 2, 1);
socket_write($socket, $str, strlen($str));
}

錄播並剪輯精彩時刻

錄播:直接使用 FFmpeg 保存推流地址的視頻即可

剪輯:根據 每分鐘 的彈幕數量變化情況,如果出現峰值,取峰值 前後的一分鐘 作為精彩部分。

峰值的判斷標準:

  • 對癢局長這樣的大主播:直播間人很多,玩出甩狙瞬狙這種騷操作彈幕會激增很多,比如是前一分鐘的三倍

  • 對小主播:人一般比較少,彈幕數量波動不大,出現精彩操作時也漲幅也不大

以上加粗的判定粒度、判定標準都可根據自己喜歡的主播修改,具體參考 edit.php 的實現。還可以為精彩時刻加上彈幕判定,比如分析是否有大量的 666、233、基本操作、學不來之類的詞集中出現等等。

彈幕分析

參考 結巴分詞 的算法,可用於生成直播的詞圖、分析粉絲的習慣用語等等。我參考的教程:

  • 結巴分詞1—結巴分詞系統介紹

  • 結巴分詞2--基於前綴詞典及動態規劃實現分詞

總結

開發遇到了兩個難點

  • 協議頭部:參考的博客裡邊逆向 B 站官方 C# 版客戶端代碼,分析協議組成,感謝博主 lyyyuna

  • 分詞算法:參考的是結巴分詞的前綴詞典與動態規劃算法,算法能力待提升 :(

再看看人家鬥魚,有開放使用的 《鬥魚彈幕服務器第三方接入協議》,協議也不會修改,興趣使然寫了 B 站的爬蟲,這種根據彈幕峰值剪輯視頻的想法應用在鬥魚上,估計會更有價值 :)


分享到:


相關文章: