B站歷史彈幕爬蟲
以b站彈幕最多的天鵝臂視頻爲例,可視化效果見哪位藝人的老婆們最有毅力?b站彈幕爬蟲及可視化。
1. 最新彈幕
最新彈幕可以通過NetWork
中的list.so...
獲取,具體操作可參考歷史彈幕的爬取。
但是最新彈幕只能最多獲取3000條,如果視頻的彈幕超過3000條,則需要通過別的方式獲取。
2. 歷史彈幕
2.1 探索
觀察彈幕列表可以發現,b站提供了歷史彈幕的查看途徑。
點擊不同的日期,可以發現不斷有以history?
開頭的資源加載進來。
從預覽中可以看到,其實就是對應日期最新的3000條彈幕。觀察一下url,有關的參數是type
、oid
和date
,oid
就是視頻對應的id,date
則是日期,type
暫時不知道幹哈的。
https://api.bilibili.com/x/v2/dm/history?type=1&oid=5627945&date=2020-03-02
再把這個網址放到沒有登錄b站賬號的瀏覽器打開,顯示賬號未登錄
,也就是需要登錄才能獲取數據,可以考慮用模擬登錄解決。
2.2 數據獲取
2.2.1 模擬登錄
用的是selenium
搭配谷歌瀏覽器,需要先配置一下和瀏覽器版本匹配的驅動,可自行百度或參考這裏的第五點。
# 導入需要的包
from bs4 import BeautifulSoup as bs
import pandas as pd
import re
from selenium import webdriver
import time, datetime
# 模擬登錄
## 登錄界面鏈接
url = 'https://passport.bilibili.com/login'
## 模擬登錄啓動
driver = webdriver.Chrome()
driver.get(url)
## 接下來就是在打開的谷歌瀏覽器裏手動登錄一下自己的賬號,
## * 因爲b站登錄需要驗證,這一步自動化的代價比較大,所以手動了。
2.2.2 獲取每一天的數據
探索部分,看看即可:
# 以2019-01-01爲例
date = '2019-01-01'
url = 'https://api.bilibili.com/x/v2/dm/history?type=1&oid=5627945&date=%s'%date
# 轉入歷史彈幕的頁面
driver.get(url)
# 獲取數據部分的html
html = driver.find_element_by_tag_name('i').get_attribute('innerHTML')
# 解析
soup = bs(html, 'lxml')
# 先康康第一條
print(soup.find('d'))
# 彈幕內容
print(soup.find('d').text)
# 彈幕其他信息
print(soup.find('d').get('p'))
# 經過一番探索,以逗號分割,第一個參數是視頻時間(秒),第五個參數是時間戳,第七個參數是用戶id,其他反正暫時不重要,不探索了
# 整理成DataFrame
data = pd.DataFrame([i.get('p').split(',') + [i.text] for i in soup.findAll('d')], \
columns=['second', 'b', 'c', 'd', 'timestamp', 'f', 'user', 'h', 'text'])
data.head(2)
封裝成函數後:
def get_per_day(date):
# url
url = 'https://api.bilibili.com/x/v2/dm/history?type=1&oid=5627945&date=%s'%date
# 轉入歷史彈幕的頁面
driver.get(url)
# 獲取並解析
soup = bs(driver.find_element_by_tag_name('i').get_attribute('innerHTML'), 'lxml')
# 整理成DataFrame
data = pd.DataFrame([i.get('p').split(',') + [i.text] for i in soup.findAll('d')], \
columns=['second', 'b', 'c', 'd', 'timestamp', 'f', 'user', 'h', 'text'])
# 只取有用的列
data = data[['timestamp', 'text']]
return data
2.2.3 獲取一段時間的數據
探索部分,看看即可:
# 設定開始和結束日期
start = '2019-01-01'
end = '2019-02-01'
# 創建空DataFrame方便後面放數據
data = pd.DataFrame()
# 轉成日期形式方便加天數
start = pd.datetime.strptime(start, '%Y-%m-%d')
end = pd.datetime.strptime(end, '%Y-%m-%d')
# 循環
while start <= end:
date = pd.datetime.strftime(start, '%Y-%m-%d')
print(date,end=', ')
data = data.append(get_per_day(date))
start = start + datetime.timedelta(days=1)
封裝成函數後:
def get_period(start, end):
# 創建空DataFrame方便後面放數據
data = pd.DataFrame()
# 轉成日期形式方便加天數
start = pd.datetime.strptime(start, '%Y-%m-%d')
end = pd.datetime.strptime(end, '%Y-%m-%d')
# 循環
while start <= end:
date = pd.datetime.strftime(start, '%Y-%m-%d')
print(date,end=', ')
data = data.append(get_per_day(date))
# 去重(因爲如果當天彈幕量不到3000會獲取到前一天的彈幕,導致數據大量重複)
data.drop_duplicates(inplace=True)
start = start + datetime.timedelta(days=1)
return data
於是就可以通過設定開始和結束的時間爬取一段時間的彈幕。
data = get_period('2019-01-01', '2019-01-31')
2.2.4 代碼整理
如果不想看上面的部分,直接複製下面這段。
def get_per_day(date, driver):
'''
獲取一天的數據
date: str, %Y-%m-%d
'''
# url
url = 'https://api.bilibili.com/x/v2/dm/history?type=1&oid=5627945&date=%s'%date
# 轉入歷史彈幕的頁面
driver.get(url)
# 獲取並解析
soup = bs(driver.find_element_by_tag_name('i').get_attribute('innerHTML'), 'lxml')
# 整理成DataFrame
data = pd.DataFrame([i.get('p').split(',') + [i.text] for i in soup.findAll('d')], \
columns=['second', 'b', 'c', 'd', 'timestamp', 'f', 'user', 'h', 'text'])
# 只取有用的列
data = data[['timestamp', 'text']]
return data
def get_period(start, end):
'''
先登錄,然後獲取數據。
start: str, %Y-%m-%d
end: str, %Y-%m-%d
'''
# 登錄
url = 'https://passport.bilibili.com/login'
driver = webdriver.Chrome()
driver.get(url)
input('請在瀏覽器登錄賬號後回車')
# 創建空DataFrame方便後面放數據
data = pd.DataFrame()
# 轉成日期形式方便加天數
start = pd.datetime.strptime(start, '%Y-%m-%d')
end = pd.datetime.strptime(end, '%Y-%m-%d')
# 循環
while start <= end:
date = pd.datetime.strftime(start, '%Y-%m-%d')
# 時不時打印一下方便追蹤進度
if start.day == 1:
print(date,end=', ')
# 拼接數據
data = data.append(get_per_day(date, driver))
# 去重
data.drop_duplicates(inplace=True)
# 下一天
start = start + datetime.timedelta(days=1)
return data
data = get_period('2019-01-01', '2019-12-31')
2.3 數據處理
2.3.1 計算彈幕發送時間
把獲取的時間由時間戳專成字符串或整型。
# 轉成字符串
#data['time'] = data['timestamp'].apply(lambda x: time.strftime("%Y%m%d", time.localtime(int(x))))
# 轉成整型方便後面計算
data['time'] = data['timestamp'].apply(lambda x: int(time.strftime("%Y%m%d", time.localtime(int(x)))))
2.3.2 從彈幕中提取各種粉絲類型和明星名字
有很多種方法,規則的學習成本最低所以用規則,具體不講了,主要就是正則匹配。
def clean(text):
for i in ['前面的', '前面', \
'前邊兒的', '前邊兒', '前邊', '你的', '討厭', '感覺'\
'我是', '小姐姐', '揮着翅膀', '這位', '這個動作', \
'我以後養', '大姨媽', '來姨媽', '姨媽', '第四天', '第五天', '第三天', '第二天', \
'墜機', '哈哈', '廣場舞大媽', '做到這裏', '已經', '安利', '我的媽', '一半', \
'沒有', '要做', '撞媽', '心疼', '雞翼', '要成爲', '好多', '未來']:
text = text.replace(i, '')
pattern = r'([^,.,。 !!??()\t0-9嗎嗚了啊的🟢着呦他說我你是·]{2,})'
return text, pattern
def find_relation(text, names):
text, pattern = clean(text)
for i in names:
pa = re.compile(pattern+i)
if len(pa.findall(text)) != 0:
return pa.findall(text)[0]
return None
wife = ['的大老婆', '大老婆', \
'的小老婆', '小老婆', \
'的老婆', '老婆', \
'的真老婆', '真老婆', \
'的腦婆', '腦婆', \
'的夫人', '夫人', \
'的妻子', '妻子']
girlfriend = ['的女朋友', '女朋友', \
'的女票', '女票', \
'的對象', '腦婆', \
'的女盆友', '女盆友', \
'的女友', '女友']
mon = ['的麻麻', '麻麻', \
'的媽媽', '媽媽', \
'的媽', '媽', \
'的親媽', '親媽', \
'的親麻', '親麻', \
'的母親', '母親', \
'的馬麻', '馬麻', \
'的媽粉', '媽粉']
women = ['的女人', '女人']
girl = ['的女孩', '女孩']
daughter = ['的女兒', '女兒']
sister = ['的姐姐', '姐姐', \
'的妹妹', '妹妹']
relation_dic = {'wife': '老婆', 'girlfriend': '女朋友', 'mon': '媽媽', \
'women': '女人', 'girl': '女孩', 'daughter': '女兒', \
'sister': '姐妹'}
data['wife'] = data['text'].apply(lambda x: find_relation(x, wife))
data['girlfriend'] = data['text'].apply(lambda x: find_relation(x, girlfriend))
data['mon'] = data['text'].apply(lambda x: find_relation(x, mon))
data['women'] = data['text'].apply(lambda x: find_relation(x, women))
data['girl'] = data['text'].apply(lambda x: find_relation(x, girl))
data['daughter'] = data['text'].apply(lambda x: find_relation(x, daughter))
data['sister'] = data['text'].apply(lambda x: find_relation(x, sister))
2.3.3 彙總每一天
relation_count = pd.DataFrame()
for k,v in relation_dic.items():
df = pd.DataFrame(data.groupby(['time', k]).size())
df.reset_index(inplace=True)
df.columns = ['date', 'name', 'value']
df['type'] = v
relation_count = relation_count.append(df)
2.3.4 計算累計值
dates = data['time'].unique()
relation_sum = pd.DataFrame()
for i in dates:
df = pd.DataFrame(relation_count[relation_count['date'] <= i].groupby(['name', 'type']).sum()['value'])
df.reset_index(inplace=True)
df['date'] = '%s-%s-%s'%(str(i)[:4], str(i)[4:6], str(i)[6:])
df.sort_values('value', ascending=False, inplace=True)
relation_sum = relation_sum.append(df.iloc[:30])
relation_sum['name'] = relation_sum.apply(lambda x: str(x['name'])+'的'+x['type'], axis=1)