B站扩容越来越厉害,弹幕数量增多的同时,弹幕质量也在以肉眼可见的速度下滑。身为一个老B(,我想搞点事情,把弹幕弄得适合自己一点。
最开始是希望做一个人工智能算法,实现对弹幕的动态分析,实现“在我看到不喜欢的弹幕之前,先给我屏蔽了”。这很甲方。
最后因为
睡醒了懒,决定总之先把弹幕爬下来,有数据了再做EDA好了,就算最后只实现了“生成屏蔽关键词”的功能,对我来说就已经挺nice了。关于“弹薄”这个概念,有点形而上,如果我能顺利完成所有我觉得必要的代码,我会去知乎上发点魔法笔记。
0202年了,B站改版BV号了,原有的全弹幕装填策略也不好使了,想要大量爬取弹幕,虽然不难,但是各种资料都很散碎,我的编程能力也亟待提升,
所以在弹薄计划的第一阶段,专心解决爬取弹幕问题就好了。
使用requests库,关键方法是requests.get(url, header, cookie)。
由于我最先爬的网站是草榴静态页面,对动态页面爬取一知半解,这里只聊个人理解:
我们看到的网页利用查看网页源代码的方法,是可以看到几乎所有显示出来的信息的,从中筛选出需要的内容,就算是静态方法;
网页展示给我们的过程中,包含很多和后端服务器交互、获取数据的过程,从这些过程入手,直接和服务器进行交互,获取数据,就算是动态方法。
通过F12查看网页交互,发现B站页面获得弹幕主要依赖如下几个步骤:
如果爬取历史弹幕,需要用户保持登录状态。
headers则是用以模拟浏览器登陆,我没有注意到太多具体细节问题,直接拿现有代码做了。
爬虫模拟登陆的最方便方法应该是使用cookie,复杂一些的,包括利用各种cookie库的、利用图像识别库针对滑块的,甚至还有尝试攻破rsa的。
在requests.get方法中加入cookie=mycookie,可以让网站认为我们是登录后的用户,进而获取登录用户才可访问的内容。
F12,刷新,网络,查找header,api.vc.bilibili.com域名对应请求头中(firefox打开原始头),Cookie那一项对应的就是用户cookie。
这东西应该是要保密的
利用前面的方法得到的弹幕数据是XML方法存储的,利用lxml.etree方法进行轻量级分析。etree的xpath方法参考树结构访问。
弹幕xml中,弹幕metadata存于@p内,data用text()方法获取。
其中第一个时间可以转换为float,后续几个可以存为int。注意UID和rowID可能超过int范围,且没有整型化必要,保留即可。
弹幕内容使用utf-8编码。
使用CSV存储以上信息,使用csv库完成操作,关键包括csv.writer、csv.reader方法。注意编码。
其中csv.writer方法写入过程如果出现空行,在open操作中对newline关键字进行限定。
爬取历史弹幕主要关注以下几个内容:
日期比较方面,使用str即可,不过我是用datetime与timedelta方法进行操作,节省脑细胞。
代码包含三个部分:
import requests
from lxml import etree
import re
from datetime import datetime
import cCSVIO
class cdanmaku(cCSVIO.cCSVIO):
# 弹幕爬取功能
BaseURL = "https://www.bilibili.com/video/"
BV = 'initBV'
cidAPI = "https://www.bilibili.com/widget/getPageList?"
danmakuAPI = "https://api.bilibili.com/x/v2/dm/history?type=1&oid="
aid = ''
cid = ''
cookies = {'censored'}
Today = ''
date = []
htmldata = ''
def __init__(self):
self.showheaders()
self.Today = datetime.today().date()
self.date = [self.Today]
def setBV(self, bv):
if not bv:
self.BV = "youlike"
else:
self.BV = bv
self.showBV()
def showBV(self):
print('BV: '+self.BV)
def download_page(self):
url = self.BaseURL + self.BV
headers = {
'User-Agent': "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1"}
self.htmldata = requests.get(url, headers=headers).text
def analasis(self):
if not self.htmldata:
self.download_page()
self.aid = re.findall(r'aid=\d*', self.htmldata)[0]
cjson = requests.get(self.cidAPI + self.aid).json()
self.cid = cjson[0]['cid']
def getDailydanmaku(self, date):
djson = requests.get(self.danmakuAPI+str(self.cid)+'&date='+str(date), cookies=self.cookies).content
content = etree.HTML(djson)
return content
def formatdanmaku(self, content):
data = []
for each in content.xpath('//d'):
a = each.xpath('@p')[0].split(',')
b = str(each.xpath('text()')[0])
b.encode('utf-8')
a.append(b)
a[0] = float(a[0])
a[1:5] = [int(a) for a in a[1:5]]
data.append(a)
return data
def mainProcess(self, bv):
self.setBV(bv)
self.download_page()
self.analasis()
content = self.getDailydanmaku(self.Today)
DMdata = self.formatdanmaku(content)
self.writeCSV(DMdata)
print('over')
if __name__ == '__main__':
danmaku = cdanmaku()
danmaku.mainProcess('BV_youlike')
CSV方法本想写成接口类(毕竟公司组织学java),结果python似乎没有接口(也没有重载,因为有更直观的方法实现),独立CSV就显得有些……不过我想这样写还是提升了一点代码可读性的。
import csv
class cCSVIO():
# 基础属性
CSVfile = 'danmaku.csv'
Headers = ['videotime','mode','size','color','unixtime','pool','UID','rowID','content']
# 基础方法
def writeCSV(self,data):
with open(self.CSVfile,'w',encoding='utf-8',newline='') as f:
f_csv = csv.writer(f)
f_csv.writerow(self.Headers)
for each in data:
f_csv.writerow(each)
def readCSV(self):
with open(self.CSVfile,'r',encoding='utf-8') as f:
f_csv = csv.reader(f)
data = []
for row in f_csv:
data.append(row)
return data
def showheaders(self):
print('CSV Header:\n')
print(self.Headers)
def getheaders(self):
return self.Headers
写一个继承上面类的子类(突然发现了面向对象设计的优势)(虽然我以前会用函数化解决):
import cdanmaku
from bs4 import BeautifulSoup as BS
from datetime import datetime,timedelta
class cdanmakuHistory(cdanmaku.cdanmaku):
PostDate=''
oneday = timedelta(days=1)
maxday = 365
def __init__(self):
self.showheaders()
self.Today = datetime.today().date()
def getPostDate(self):
if not self.htmldata:
self.download_page()
bs = BS(test.htmldata, 'html.parser')
data = bs.find_all('div', class_='video-data')[0]
PostDate = data.find_all('span')[1].text.split()[0]
self.PostDate = datetime.strptime(PostDate, '%Y-%m-%d').date()
def fufillDate(self):
self.date = []
day = self.Today
if not self.PostDate:
self.getPostDate()
while day >= self.PostDate:
self.date.append(str(day))
day = day - self.oneday
def getHistorydanmaku(self):
HistoryDM=[]
daycounter = 0
if not self.date:
self.fufillDate()
for day in self.date:
if daycounter >= self.maxday:
break
else:
print(day)
content = self.getDailydanmaku(day)
data = self.formatdanmaku(content)
HistoryDM = HistoryDM + data
daycounter += 1
return HistoryDM
def mainProcess(self, bv):
self.setBV(bv)
self.analasis()
DMdata = self.getHistorydanmaku()
self.writeCSV(DMdata)
print('over')
def setMaxday(self, maxday):
self.maxday = maxday
if __name__ == '__main__':
test = cdanmakuHistory()
test.setMaxday(15)
test.mainProcess('BV_youlike')
其中daycounter方法明显粗鄙,主要是担心爬取数量过多会导致异常,以后看实际情况再看吧。
欢迎debug。
以上。