ADD file via upload

master
p9eymkgwc 2 years ago
parent e254565567
commit 4b24a7ce5a

@ -0,0 +1,408 @@
import bilibili_api
import csv
from lxml import etree
import os # 判断文件是否存在
import time
from time import sleep # 设置等待,防止反爬
import random # 生成随机数
# 导入请求模块
import requests
# 正则模块
import re
# 导入json模块
import json
# 导入格式化输出模块
import subprocess
import pandas as pd # 用于数据分析及文件格式转换
import jieba # 用于分词统计出现的关键词
from wordcloud import WordCloud # 筛选词云图展示的词
import matplotlib.pyplot as plt # 实现绘图可视化
from imageio import imread # 图片库读取照片RGB内容转换照片格式
# python通过调用warnings模块中定义的warn()函数来发出警告。我们可以通过警告过滤器进行控制是否发出警告消息
import warnings
from pyecharts.charts import Bar
from pyecharts import options as opts
from pyecharts.faker import Faker
#######################################################################################
#以下代码爬取bilibili视频热门榜前一百的BV号
POPULAR_URL = "https://api.bilibili.com/x/web-interface/popular"#哔哩哔哩热门的官方网址
#设置请求头,解决UA反爬
HEADERS = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36'
}
def get_popular_list():
"""
获取排行榜1-100
:param pn:
:return: All bvid_list
"""
bvidList = []#存bv号
#一页20个视频所以获取前5页
for i in range(1, 6):
query = "pn=" + str(i)# 构建GET请求的参数设置当前页数
r = requests.get(POPULAR_URL, headers=HEADERS, params=query)
resultList = r.json()['data']['list']# 从响应中获取当前页的视频列表
for item in resultList:
bvidList.append(
bilibili_api.aid2bvid(
item['aid'] #得到bv号。bilibili_api.aid2bvid为aid转bvid由bilibili_api提供。
)
)
return bvidList
#params=query的作用是将我们构建的参数字符串添加到GET请求中以便于向API请求特定的数据。
# resultList = r.json()['data']['list']
# 当发送GET请求并获取到响应后响应的内容通常是一个JSON格式的字符串。
# r.json()表示将响应r的JSON字符串转换为Python中的字典对象。由于响应的JSON对象中我们需要获取的视频列表保存在"data"字段下的"list"字段中,
# 因此我们可以使用r.json()['data']['list']的方式获取到视频列表将其赋值给变量resultList。最终resultList中存储的就是当前页的视频列表。
print ("以下为bilibili视频热门榜前一百的BV号")
for i in get_popular_list():
print(i)
############################################################################################
#################################################################################################
#爬取弹幕
class BiliSpider:
def __init__(self, BV):
# 构造要爬取的视频url地址
self.BVurl = "https://m.bilibili.com/video/" + BV#指定bv号视频的网址
self.headers = {
"User-Agent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.116 Mobile Safari/537.36"
}
# 获取该视频网页的内容
def getHTML_content(self):
# 获取该视频网页的内容
# 发送GET请求获取该视频网页的内容
response = requests.get(self.BVurl, headers=self.headers)
#将HTTP响应的二进制内容解码为字符串。
html_str = response.content.decode()
return html_str
#为了后续使用XPath表达式从HTML文本中提取数据。
def get_script_list(self, str):
# 将获取到的 HTML 字符串解析为一个 Element 对象,方便后续使用 XPath 语法进行解析。
html = etree.HTML(str)
#cid视频编号在script中
script_list = html.xpath("//script/text()")
return script_list
def gen_xml(self, code):
# 组装成要请求的xml地址
xml_url = "https://comment.bilibili.com/{}.xml".format(code)
return xml_url
# 弹幕包含在xml中的<d></d>中,取出即可
def get_word_list(self, str):
html = etree.HTML(str)
word_list = html.xpath("//d/text()")#xpath语法得到
return word_list
# Xpath不能解析指明编码格式的字符串所以此处我们不解码还是二进制文本
def parse_url(self, url):
#得到内容
response = requests.get(url, headers=self.headers)
return response.content
def run(self):
# 1.根据BV号获取网页内容
html_content = self.getHTML_content()
# 2.请求并获取script数据
script_list = self.get_script_list(html_content)
# 解析script数据获取cid信息
for script in script_list:
if '[{"cid":' in script:
find_script_text = script#遍历 script_list 列表,找到包含 cid 字符串的 script 文本,
# 并将其保存在变量 find_script_text 中。
cid = find_script_text.split('[{"cid":')[1].split(',"page":')[0]#取出cidcid 是视频的编号,它用于拼接评论的 xml 文件地址。
#通过两个 split() 方法将 find_script_text 字符串分割成多个部分,最终得到一个包含 cid 值的字符串。
# 其中,第一个 split() 方法根据 [{"cid": 字符串将 find_script_text 分割成两部分,即 '[{"cid":' 和 '其他部分'
# 然后取第二个部分(即 '其他部分')。接着,第二个 split() 方法根据 ',"page":' 字符串将第一步得到的字符串分割成两部分,即 cid 值和 '其他部分'
# 然后取第一个部分(即 cid 值)。这样就成功地提取出了 cid 的值。
# 根据cid信息拼接弹幕评论的xml文件url由于哔哩哔哩的弹幕是保存在另一个网址的所以我们需要把弹幕网址找出
xml_url = self.gen_xml(cid)
print(xml_url)
# 请求xml文件地址并解析
xml_str = self.parse_url(xml_url)
word_list = self.get_word_list(xml_str)
# 3.打印
for word in word_list:
print(word)
return word_list;
print("请输入需要查看信息的视频BV号:")
BV = input()
#输出弹幕
spider = BiliSpider(BV)
danmu_list=spider.run()
#################################################################################################
# 写入弹幕
print("将弹幕数据写入danmu.csv中")
# 打开文件,使用 utf-8 编码newline='' 参数用来处理每行数据之间存在空格行的问题。
f = open("danmu.csv", mode='w', encoding='utf-8', newline='') # newline='' 用来处理每行数据之间存在空格行的问题。
# 创建 csv writer 对象
csvwriter = csv.writer(f)
# 写入每条弹幕数据
for danmu in danmu_list:
csvwriter.writerow([danmu])
f.close()
##############################################################################################################
print("下面开始绘制词云图")
#绘制词云图
warnings.filterwarnings("ignore") # 忽略警告信息
# 读取弹幕CSV文件
data = pd.read_csv('danmu.csv')
# 将弹幕数据保存为txt格式,以便进行词云图的绘制,只能用纯文本格式所以用txt
data.to_csv('danmu.txt', sep='\t', index=False)
#读取文本文件并使用lcut()方法进行分词
with open("../大作业/danmu.txt",encoding="utf-8") as f:
txt = f.read()
txt = txt.split()
data_cut = [jieba.lcut(x) for x in txt]
#首先将文本文件读入到变量txt中然后使用Python内置的字符串方法split()将文本按照空格切分成一个列表。
# 接下来,使用列表解析式[jieba.lcut(x) for x in txt]对每个字符串进行中文分词将结果保存到列表data_cut中。
#其中jieba.lcut(x)是jieba库中的一个分词函数用于对中文文本进行分词返回一个列表。x是待分词的文本。
#读取停用词(不展示的词),该文件为提前设置好的,一个词显示为一行
with open("../大作业/stoplist.txt",encoding="utf-8") as f:
stop = f.read()
stop = stop.split()
stop = [" ","","说道",""] + stop
#得到去掉停用词之后的最终词,为了让词云更好看易懂
s_data_cut = pd.Series(data_cut) # 将数据转为一维数组与Python基本的数据结构List也很相近
# 其区别是List中的元素可以是不同的数据类型而ArrayNumpy和Series中则只允许存储相同的数据类型
all_words_after = s_data_cut.apply(lambda x:[i for i in x if i not in stop])
#然后,对 s_data_cut 中每一个分词后的弹幕数据,使用 lambda 函数和列表推导式进行去停用词处理,得到去除停用词后的所有词语列表 all_words_after。
#具体来说lambda x: [i for i in x if i not in stop] 的作用是遍历 x 列表中的每一个元素(即分词后的一个弹幕数据),
#如果这个元素不在停用词列表 stop 中就将其添加到返回值列表中。最终apply() 方法会将这个函数应用到 s_data_cut 中的每一个弹幕数据中,
# 得到一个包含所有去除停用词后的词语列表的 Series 数据结构 all_words_after。
#词频统计
all_words = []#创建一个空列表all_words用来存储所有的词语。
for i in all_words_after:#遍历all_words_after中的每一个分词结果即遍历所有去除停用词后的分词结果。
all_words.extend(i)#对于每个分词结果将其中的所有词语添加到all_words列表中。
word_count = pd.Series(all_words).value_counts()
#pd.Series()将all_words列表转化为pandas的Series数据类型方便后续统计词频。
#value_counts()方法统计每个词语出现的次数生成词频统计结果word_count。
#词云图的绘制
def view():
# 1读取背景图片
back_picture = imread("../大作业/background.jpg")
# 2设置词云参数
wc = WordCloud(
font_path="C:/Windows/Fonts/simfang.ttf",
background_color="white",
max_words=2000,
mask=back_picture,
max_font_size=200,
random_state=42#生成随机颜色的种子值
)
wc2 = wc.fit_words(word_count) # fit_words() 方法来根据词频信息生成词云图
# 3绘制词云图
plt.figure(figsize=(16, 8))
plt.imshow(wc2)
plt.axis("off")#关闭坐标轴显示
plt.show()
wc.to_file("../大作业/ciyun.png") # 生成的图片存储路径
#词云图绘制完成
print("词云图绘制完成")
view()
##################################################################################################
print("下面开始爬取指定视频的评论")
# 请求头
headersp = {
# 需定期更换cookie否则location爬不到
'cookie':'buvid3=114CEDA0-85FF-4DC4-9687-7E230EC3922634763infoc; LIVE_BUVID=AUTO7516290399110767; balh_is_closed=; balh_server_inner=__custom__; i-wanna-go-back=-1; buvid_fp_plain=undefined; CURRENT_BLACKGAP=0; blackside_state=0; is-2022-channel=1; DedeUserID=341891643; DedeUserID__ckMd5=868969f705549646; b_ut=5; fingerprint3=91317b33d1486edea477476111226160; hit-dyn-v2=1; _uuid=14D102ACC-1AC2-5B43-F613-CD2103EA94C9977773infoc; go_old_video=1; i-wanna-go-feeds=-1; b_nut=100; rpdid=|(J|lmRkmuRl0JuYY)l)~)km; buvid4=29001660-0B06-78DE-F2B4-3FD7CA49439233181-022012118-dWg1L%2Bln%2Fbvj%2FYnR0RiUpA%3D%3D; fingerprint=4a27fc308659d4faf13e9361e621c75f; buvid_fp=6d7dd3797b522a2f84b750e9b8fb35ba; header_theme_version=CLOSE; hit-new-style-dyn=1; CURRENT_FNVAL=4048; nostalgia_conf=-1; CURRENT_PID=b369d710-c92e-11ed-b6f8-bf9093b777e4; CURRENT_QUALITY=80; bp_video_offset_341891643=784002501477138600; FEED_LIVE_VERSION=V_LIVE_2; share_source_origin=QQ; bsource=share_source_qqchat; bp_t_offset_341891643=787671082207281189; SESSDATA=742a1cb4%2C1697807586%2C40a4c%2A41; bili_jct=6eb7012a2383b24993a845c885caac7a; b_lsid=45C10432E_187AE8DAF27; sid=5wujmgl0; innersign=1; PVID=5',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36 Edg/106.0.1370.47'
}
def bv2av(bid):
"""把哔哩哔哩视频的bv号转成av号"""
table = 'fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF'# 哔哩哔哩bv号转av号的对照表
tr = {} # 储存表格
for i in range(58):
tr[table[i]] = i# 生成对照表
s = [11, 10, 3, 8, 4, 6]# 与对照表对应的权重
r = 0# 计算结果
for i in range(6):
r += tr[bid[s[i]]] * 58 ** i# 根据对应表将bv号转换成av号
aid = (r - 8728348608) ^ 177451812# 通过异或加密后得到av号
return aid
def get_comment(v_aid, v_bid):
"""
爬取B站评论数据
:param v_aid: B站视频的aid号
:param v_bid: B站视频的bid号
:return: None
"""
# 循环页码爬取评论
for i in range(max_page):
try:
sleep(random.uniform(0, 1)) # 随机等待,防止反爬
url = "https://api.bilibili.com/x/v2/reply/main?csrf=bf9a78c05400af2f7bdac7947b836cc8&mode=3&next={}&oid={}&plat=1&type=1".format(
i, v_aid) # 请求地址
response = requests.get(url, headers=headersp, ) # 发送请求
data_list = response.json()['data']['replies'] # 解析评论数据
#response.json()'是将API响应解析为json格式'[data]'表示选择json中'data'字段的值,
# '[replies]'表示选择'data'中的'replies'字段的值,最终得到的是评论数据的列表。
print('正在爬取B站评论[{}]: 第{}页,共{}条评论'.format(bid, i + 1, len(data_list)))
comment_list = [] # 评论内容
location_list = [] # IP属地
user_list = [] # 评论作者
like_list = [] # 点赞数
# 循环爬取每一条评论数据
for a in data_list:
# 评论内容
comment = a['content']['message']#键值对content中的message字段即评论的正文内容。
comment_list.append(comment)
# IP属地
try:
location = a['reply_control']['location']
except:
location = ""
location_list.append(location.replace("IP属地", ""))
# 评论作者
user = a['member']['uname']
user_list.append(user)
# 点赞数
like = a['like']
like_list.append(like)
# 把列表拼装为DataFrame数据
df = pd.DataFrame({
'视频链接': 'https://www.bilibili.com/video/' + v_bid,
'评论页码': (i + 1),
'评论作者': user_list,
'IP属地': location_list,
'点赞数': like_list,
'评论内容': comment_list,
})
if os.path.exists(outfile):
header = False
else:
header = True
#检查输出文件是否存在。如果存在将header设置为False这样新的数据将被追加到文件末尾而不是覆盖掉原有的数据。
# 如果输出文件不存在将header设置为True这样将创建一个新的文件并添加列名作为文件的第一行。
# 把评论数据保存到csv文件
df.to_csv(outfile, mode='a+', encoding='utf_8_sig', index=False, header=header)
except Exception as e:
print('爬评论发生异常: {},继续..'.format(str(e)))
print('爬虫开始执行!')
bid_list = [BV]
# 评论最大爬取页每页20条评论
max_page = 10
# 获取当前时间戳
now = time.strftime("%Y%m%d%H%M%S", time.localtime())
# 输出文件名
outfile = 'b站评论_{}.csv'.format(now)
# 循环爬取这几个视频的评论
for bid in bid_list:
# 转换aid
aid = bv2av(bid=bid)
# 爬取评论
get_comment(v_aid=aid, v_bid=bid)
print('爬虫正常结束!')
#################################################################################3
# 去除重复
df = pd.read_csv(outfile)
# 去重
df.drop_duplicates(subset=['评论作者','评论内容'], inplace=True, keep='first')
#drop_duplicates() 方法中的 subset 参数指定需要去重的列名列表
# 再次保存csv文件
df.to_csv('去重后_' + outfile, index=False, encoding='utf_8_sig')
print('评论数据清洗完成')
print("评论爬取成功并已存入指定csv文件")
########################################################################
#绘制柱形图
print("下面开始根据评论数据绘制柱形图")
# 读取去重后的评论数据文件
df = pd.read_csv(rf"去重后_b站评论_{now}.csv")
# 按照点赞数进行排序选择前20个评论
df1 = df.sort_values(by="点赞数",ascending=False).head(20)
# 绘制柱形图x轴为评论内容y轴为点赞数
c1 = (
Bar()#Bar()类创建了一个柱形图对象
.add_xaxis(df1["评论内容"].to_list())
.add_yaxis("点赞数", df1["点赞数"].to_list(), color=Faker.rand_color())
.set_global_opts(
title_opts=opts.TitleOpts(title="评论热度Top20"),# 设置图表标题
datazoom_opts=[opts.DataZoomOpts(), opts.DataZoomOpts(type_="inside")],# 设置数据缩放
)
.render(path='bar.html')
)
print("柱形图绘制成功保存为bar.html")
############################################################################################################
print("下面开始下载指定视频")
print("下载视频")
url = f'https://www.bilibili.com/video/{BV}/'
headers = {
"referer": "https://api.bilibili.com/",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36 SLBrowser/8.0.0.12022 SLBChan/105"
}
resp = requests.get(url=url, headers=headers)
# print(resp.text)
# 视频标题
title = re.findall('name="title" content="(.*?)_哔哩哔哩_bilibili">', resp.text)[0]
title = title.replace(" ","")
#re.findall 匹配 HTML 标签 <meta name="title" content="XXX"> 中的标题信息,匹配到的结果是一个列表,
# 列表中只有一个元素,即视频的标题。接着使用字符串方法 replace 去掉标题中的空格,最终返回视频的标题。
# 视频相关信息
video_info = re.findall('<script>window.__playinfo__=(.*?)</script', resp.text)[0]
#视频信息会嵌入在<script>标签中的JavaScript代码中其中包含了视频的相关信息包括清晰度、视频链接、音频链接等等是解析视频的重要数据来源。
# 使用正则表达式匹配<script>标签内容中的window.__playinfo__字符串所对应的值即为视频信息的json字符串。
# 数据类型转换
json_data = json.loads(video_info)
# 字典取值 键值对-->根据冒号左边的内容【键】,提取冒号右边的内容【值】
# pprint(json_data)
# 提取音频url地址
audio_url = json_data['data']['dash']['audio'][0]['baseUrl'] # 有声音没有画面
video_url = json_data['data']['dash']['video'][0]['baseUrl'] # 有画面没有声音
print(title)
print(audio_url)
print(video_url)
'''
保存数据保存本地文件夹里面
'''
# 发送请求过去二进制数据内容
audio_content = requests.get(url=audio_url, headers=headers).content # 音频内容
video_content = requests.get(url=video_url, headers=headers).content # 视频内容
with open('video\\' + title + '.mp3', mode='wb') as a:
a.write(audio_content)#音频
with open('video\\' + title + '.mp4', mode='wb') as v:
v.write(video_content)
cmd = f'ffmpeg -i video\\{title}.mp4 -i video\\{title}.mp3 -c:v copy -c:a aac -strict experimental video\\{title}output.mp4'
subprocess.run(cmd)
#调用FFmpeg软件实现将视频文件和音频文件进行合并的操作。cmd变量中的命令是使用FFmpeg软件的命令行参数
#指定了输入的视频和音频文件路径以及输出的文件名。最后通过subprocess.run()方法执行该命令
#视频下载完毕
print("视频下载成功结果保存在video文件夹里面")
print("工作已完成,请退出")
Loading…
Cancel
Save