前言
今天有一個好兄弟說,他開學了,在學到資料採集的課程的時候,老師佈置了一個作業,爬取b站影片的彈幕。對此,他感覺到了非常困擾。
他說,“爬取小說我能夠理解,小說是文章,能夠直接看到,我知道該如何爬取,但是彈幕都在影片裡面,這要怎麼爬呢?是下載影片嗎?我不明白啊!”
可以看出,好兄弟確實是遇到了一些困惑,他可能不太瞭解影片網站彈幕的實現。其實,當然,彈幕也是可以爬取的,因為,彈幕也是由影片網站伺服器所提供的,自然也是可以爬取到的。
彈幕原理
如同我們經常用到的那樣,彈幕就是在影片/直播中,顯示一條文字。因此,只要將文字繪製出來,覆蓋在影片上方即可。一個常見的實現是,建立一個Canvas的彈幕系統,繪製彈幕,並且透過動畫效果,控制彈幕的移動。當然,簡單的覆蓋效果不理想,也要透過一些演算法,來調整顯示的方式,比如說必須考慮到彈幕之間不應該互相覆蓋,或者如果彈幕太多,螢幕無法顯示(對於解析度低,螢幕小的裝置,這尤其重要),需要限制彈幕池的大小。而一些高階的影片網站中,甚至可能檢測影片中的關鍵人物,避免彈幕遮擋到人物。
彈幕通常需要包括幾條重要的資訊,比如說使用者id,傳送時間,彈幕出現的位置,彈幕的內容等。在實際使用中,彈幕不僅僅要考慮到如何繪製和顯示,也要考慮到如何傳送,接受和儲存。並且在儲存的過程中需要考慮到安全性,例如,需要對使用者的輸入進行轉義,防止xss的注入。
對於彈幕爬取,在實際使用中,往往會有一個專門的彈幕介面,透過這個介面,就可以得到所有的彈幕資訊。如果是實時彈幕,或者彈幕數量特別多,往往會使用輪詢請求的方式,例如每隔幾秒鐘,就請求一次新的彈幕資訊。也就是說,爬取彈幕其實不需要下載影片,只需要請求這個彈幕介面就可以了。
彈幕實現
如果我們需要製作一個彈幕影片,一個非常快速的方式,就是使用danmaku的js庫,這是一個專門的彈幕庫,透過這個庫可以輕鬆的傳送彈幕。
首先,我們需要載入這個danmaku的js庫
<script src="https://cdn.jsdelivr.net/npm/danmaku/dist/danmaku.min.js"></script>
然後建立一個danmaku,這通常需要指定一個容器
var danmaku = new Danmaku({ container: document.getElementById('my-video-container'), media: document.getElementById('my-video'), comments: [] });
然後,就可以透過emit向螢幕傳送彈幕,設定時間,大小,顏色等
danmaku.emit({ text: '大佬6666666', style: { fontSize: '24px', color: '#fff', border: 'none', backgroundColor: 'rgba(0, 0, 0, 0.5)' }, time: 10 });
完整的一個html頁面可能像這樣:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>影片彈幕網站</title> <script src="https://cdn.jsdelivr.net/npm/danmaku/dist/danmaku.min.js"></script> </head> <body> <div id="my-video-container" style="width:640px;height:360px;position:relative;"> <video id="my-video" src="你的影片.mp4" style="position:absolute;" controls></video> </div> <script> var danmaku = new Danmaku({ container: document.getElementById('my-video-container'), media: document.getElementById('my-video'), comments: [] }); danmaku.emit({ text: '大佬66666', style: { fontSize: '24px', color: '#FFFFFF', border: 'none', backgroundColor: 'rgba(0, 0, 0, 0.5)' }, time: 10 }); </script> </body> </html>
這是關於影片彈幕的一種方法,如果是直播彈幕的話,還會有所不同,關於danmaku的更多瞭解,可以訪問該庫的github地址:github.com/weizhenye/D…
爬取彈幕
回到我們剛剛的問題上來,如果爬取b站的彈幕,同樣需要使用b站彈幕的介面,然後透過這個介面進行請求,得到結果即可,這個並不難做到,下面就是一個爬取b站彈幕的實現。
import sys import aiohttp import asyncio import time from parsel import Selector class BiliDanmaku: # 適用與windows環境的事件迴圈邏輯,非windows環境使用者可自行註釋掉 if sys.platform == 'win32': asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) @staticmethod def parse_p(data): fields = data.split(',') appear_time = float(fields[0]) send_time_epoch = int(fields[4]) send_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(send_time_epoch)) return { "影片內彈幕出現時間": appear_time, "彈幕傳送時間": send_time, } def __init__(self, bvid, agent="bilibili client"): self.bvid = bvid self.headers = {"user-agent": agent} self.base_url = "https://api.bilibili.com" async def initialize(self): url = "https://api.bilibili.com/x/web-interface/view" params = {"bvid": self.bvid} async with aiohttp.ClientSession() as session: async with session.get(url, params=params, headers=self.headers) as response: data = await response.json() self.cid = data.get("data", {}).get("cid") async def get_danmaku(self): await self.initialize() async with aiohttp.ClientSession() as session: async with session.get(f"{self.base_url}/x/v1/dm/list.so", headers=self.headers, params={"oid": self.cid}) as response: r = await response.text() s = Selector(r) d = s.xpath("//d") danmaku_list = [] for i in d: p = i.xpath("./@p").get() text = i.xpath("./text()").get() info = self.parse_p(p) info["彈幕內容"] = text danmaku_list.append(info) return danmaku_list async def __crawl_danmaku(bvid): b = BiliDanmaku(bvid) danmaku = await b.get_danmaku() return danmaku def crawl_danmaku(bvid): danmaku = asyncio.run(__crawl_danmaku(bvid)) with open("danmaku.txt", "w", encoding="utf-8") as file: for item in danmaku: file.write(str(item) + "\n") return danmaku if __name__ == "__main__": crawl_danmaku("BV1EcHgezEyF") # 輸入影片BV號
執行後會在同目錄下生成danmuku.txt
,該影片的所有彈幕都儲存在裡面了!