diff --git a/大作业4.0.py b/大作业4.0.py
new file mode 100644
index 0000000..bf6627a
--- /dev/null
+++ b/大作业4.0.py
@@ -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中的中,取出即可
+ 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]#取出cid,cid 是视频的编号,它用于拼接评论的 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中的元素可以是不同的数据类型,而Array(Numpy)和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 标签 中的标题信息,匹配到的结果是一个列表,
+# 列表中只有一个元素,即视频的标题。接着使用字符串方法 replace 去掉标题中的空格,最终返回视频的标题。
+
+# 视频相关信息
+video_info = re.findall('标签中的JavaScript代码中,其中包含了视频的相关信息,包括清晰度、视频链接、音频链接等等,是解析视频的重要数据来源。
+# 使用正则表达式匹配