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('