B站视频信息爬取教程
一、简介
之前秋招季,某家互联网公司的商业分析岗给我出了道课题,让我爬取B站原神的视频信息,分析原神在B站的创作热点以及成因。当时因为也是期末季,赶上身上有4个project。因此直到现在才有时间来整理一下这个课题的过程和结果。趁此机会也来分享一下我的B站爬虫结构。希望能对有需要的朋友带来一些帮助。
二、爬虫逻辑
从课题的角度出发,我们需要爬取的数据主要是以下这些:
1) 视频的基本信息:BV号,up主,视频标题,视频标签,发布时间,视频播放量,点赞量,硬币量,收藏量,分享量以及评论量。
2) 视频的弹幕数据
3) 视频的评论数据
因此之后将根据爬取的具体数据,分模块进行讲解。在开始正式内容之前,需要先了解一下几个重要的概念。
首先是B站api,通过向B站的api发送请求,可以轻松获取到部分的视频的基本信息,但需要注意,发送的请求中需要包括视频的识别编号。众所周知,B站的视频可以通过BV号进行检索,但在B站的后台事实上并不使用BV号,而是沿用了AV号时代的aid和cid来进行视频相关信息的识别。Aid主要与视频的基本信息以及评论信息相关,而提取弹幕信息则需要使用cid。此外,在本项目中并没有涉及提取up主的相关信息,但通过解析网页可以看出,up主的信息是通过pid来进行识别提取的。
其次,对于无法或者无需使用B站api提取的信息,简单地对html文件进行解析就可以获得相关数据了。
那么现在我们进入正题,如何爬取我们需要的B站数据,那么主要是分为以下几个步骤:
Step1:通过b站搜索引擎获得相关词为‘原神’(这里可以任意指定)的所有视频,通过 http:// search.bilibili.com 返回的信息可以得到视频的BV号以及up主相关信息。
Step2:通过BV号获得aid和cid这两个识别码。
Step3:使用aid和cid,向对应的B站api发送请求获取视频弹幕信息,评论信息,以及视频基础信息
Step4:对还需要的信息进行补全,因此需要进入视频网页进行解析,提取发布时间、标签、以及视频标题
Step5:保存信息,本次采用excel的方式进行保存,方便后续进行数据分析
三、具体代码及解析
0、使用的Package
import json,requests,time
import pandas as pd
import re
from bs4 import BeautifulSoup
import chardet
import datetime
这几个Package中需要解释一下chardet。chardet是python的一个第三方编码检测模块,可以检测文件,XML等字符编码的类型。这里使用chardet是为了自动识别字符编码的类型,方便之后encoding。因为在弹幕及评论中可能会有多种编码类型。chardet可以通过pip install chardet安装使用。
1、通过关键字搜索获得需要爬取信息的视频列表
def search_video(search_name,pages):
search_name: str; 输入搜索关键词
pages: int; 输入需要爬取的页数
return:
bvid_lst: list; 返回BV号列表
up_lst: list; 返回up主名字列表;与BV号一一对应
bvid_lst = []
up_lst = []
for page in range(1,pages):
url = ('http://search.bilibili.com/all?keyword='+search_name+
'&single_column=0&&order=dm&page='+str(page))
req = requests.get(url)
content = req.text
pattern = re.compile('<a href="//www.bilibili.com/video/(.*?)\?from=search" title=')
pattern_up = re.compile('<a href="//space.bilibili.com/.*?class="up-name">(.*?)</a></span>')
lst_add = pattern.findall(content)
up_lst_add = pattern_up.findall(content)
while len(lst_add)==0:
url = ('http://search.bilibili.com/all?keyword='+search_name+
'&single_column=0&&order=dm&page='+str(page))
req = requests.get(url)
content = req.text
pattern = re.compile('<a href="//www.bilibili.com/video/(.*?)\?from=search" title=')
pattern_up = re.compile('<a href="//space.bilibili.com/.*?class="up-name">(.*?)</a></span>')
lst_add = pattern.findall(content)
up_lst_add = pattern_up.findall(content)
time.sleep(1)
print('第{}页'.format(page),lst_add)
up_lst.extend(up_lst_add)
bvid_lst.extend(lst_add)
return bvid_lst,up_lst
这一模块的作用是通过输入搜索的关键词,比如本项目中的”原神“。通过B站自有的搜索引擎,获取所有相关的视频。最大搜索量为20*100,共100页,每页20个视频。本模块返回的是BV号列表,以及视频对应的UP主名字列表。
2、通过BV号获得cid和aid
前面已经介绍过了,需要使用B站api提取相关数据的时候需要提供视频的唯一识别编号,提取弹幕使用cid,其他的信息使用aid。而cid和aid则是都可以通过BV号来获得。
首先是获得aid,aid很有趣,他实际上就是原本的av号。可以通过一些算法,将BV号直接转化为aid,这里主要是借鉴了网上某大神的算法。知乎上应该能够搜到:
#这一部分用于转化BV号->AV号
table = 'fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF'
tr = {}
for i in range(58):
tr[table[i]] = i
s = [11, 10, 3, 8, 4, 6]
xor = 177451812
add = 8728348608
def bv2av(x):
r = 0
for i in range(6):
r += tr[x[s[i]]] * 58 ** i
return (r - add) ^ xor
def get_aid(bvid):
if 'BV' in bvid:
return bv2av(bvid)
else:
return bvid
然后是获取cid的方法,主要是通过B站的api获得cid信息:
def get_cid(bvid):
url = f'https://api.bilibili.com/x/player/pagelist?bvid={bvid}&jsonp=jsonp'
req = requests.get(url)
content = req.text
info = json.loads(content)
return info["data"][0]["cid"]
3、获得视频基本信息
在获得了aid之后就可以通过api获得一些基本的视频信息了,包括视频播放量,投币量,收藏量,点赞量,转发量等等。
def get_base_info(aid):
base_info_url = f'https://api.bilibili.com/x/web-interface/archive/stat?aid={aid}'
base_info = requests.get(base_info_url,headers=dic_header).json()['data']
#print('播放量:{}\n弹幕数量:{}\n收藏数量:{}\n硬币数量:{}\n分享数量:{}\n点赞数量:{}\n-----\n评论数量:{}'.format(
# base_info['view'],base_info['danmaku'],base_info['favorite'],base_info['coin'],base_info['share'],
# base_info['like'],base_info['reply']))
except:
print('Error')
return base_info
4、获得视频的补充信息(视频标题,发布时间,视频标签)
通过BV号可以从html文件中直接获得视频的标题,发布时间以及最重要的tag标签。
def get_title_time_tag(bvid):
tag_lst = []
url = f'https://www.bilibili.com/video/{bvid}'
content = requests.get(url).content
soup = BeautifulSoup(content, 'html.parser')
tags = soup.find_all('a',class_='tag-link')
title_lst = soup.find_all('title')
title = title_lst[0].text.split('_')[0]
time_lst = soup.find_all(itemprop='uploadDate')
time = time_lst[0]['content'].split(' ')[0]
for tag in tags:
tag_lst.append(tag.text.strip())
return title,time,set(tag_lst)
5、视频弹幕信息
通过之前获得的cid就可以轻松通过api得到指定视频的所有弹幕了,但这里只提取了弹幕的内容,事实上也可以提取每一条弹幕的发送时间。这里有需要的同学可以自己研究一下。
def get_danmaku_datas(cid):
#弹幕使用的cid进行检索
url = f'https://api.bilibili.com/x/v1/dm/list.so?oid={cid}'
req = requests.get(url)
req.encoding = chardet.detect(req.content)['encoding']
content = req.text
pattern = re.compile('<d.*?>(.*?)</d>')
danmaku_data_lst = pattern.findall(content)
return danmaku_data_lst
6、视频评论信息
这里只爬取了一级评论,也就是对视频的评论,不包括对评论的评论以及回复。如果有需要的朋友需要自己研究一下了。
def get_comment_datas(aid):
comment_url = 'https://api.bilibili.com/x/v2/reply'
comment_page = 1
comment_data_lst = []
while True:
param = {'jsonp':'jsonp',
'pn':comment_page,
'type':1,
'oid':aid,
'sort':'2'}
html = requests.get(url=comment_url,headers=dic_header,params=param)
comment_data = json.loads(html.text)['data']['replies']
for data in comment_data:
dic_coment={}
dic_coment['member'] = data['member']['uname']
dic_coment['like'] = data['like']
dic_coment['comment'] = data['content']['message']
dic_coment['time'] = datetime.datetime.fromtimestamp(data['ctime'])
dic_coment['pid'] = data['rpid_str']
comment_data_lst.append(dic_coment)
time.sleep(1)
comment_page+=1
except Exception as Comment_Page_Error:
break
return comment_data_lst
7、整合
整合部分没有运行弹幕和评论的提取,因为这两部分涉及的数据量过于庞大,一个视频可能还行,多个视频的话耗时很长。主要是评论数据的提取,当然,上面的提取评论数据的模块亲测可以使用。
if __name__=='__main__':
dic_header = {'User-Agent':'Mozilla/5.0'}
search_name = '原神'
bvid_lst,up_lst = search_video(search_name,pages=2)
#df_result = pd.DataFrame(columns=['aid','cid','title','time','tag_lst','base_info','comment_datas','danmaku'])
df_result = pd.DataFrame()
for idx,bvid in enumerate(bvid_lst):
print('正在提取第{}个视频的信息,BV号为:{}'.format(idx+1,bvid))
aid = get_aid(bvid)
cid = get_cid(bvid)
title,ttime,tag_lst = get_title_time_tag(bvid)
up_name = up_lst[idx]
base_info = get_base_info(aid)
#comment_datas = get_comment_datas(aid)
#danmaku = get_danmaku_datas(cid)