|
|
|
|
import sys
|
|
|
|
|
import asyncio
|
|
|
|
|
|
|
|
|
|
# 如果是在 Windows 平台上运行,则设置事件循环策略为 SelectorEventLoopPolicy
|
|
|
|
|
# 这是为了避免在 Windows 上运行 asyncio 时可能出现的问题
|
|
|
|
|
if sys.platform.startswith('win'):
|
|
|
|
|
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
|
|
|
|
|
|
|
|
import collections # 用于词频统计
|
|
|
|
|
import json # 用于处理 JSON 数据
|
|
|
|
|
import aiohttp # 用于异步 HTTP 请求
|
|
|
|
|
import asyncio # 用于异步操作
|
|
|
|
|
import re # 正则表达式模块,用于解析弹幕
|
|
|
|
|
import openpyxl # 用于处理 Excel 文件
|
|
|
|
|
import pandas as pd # 用于数据处理
|
|
|
|
|
import cProfile # 用于性能分析
|
|
|
|
|
|
|
|
|
|
# 创建性能分析器实例,并开始性能分析
|
|
|
|
|
profile = cProfile.Profile()
|
|
|
|
|
profile.enable()
|
|
|
|
|
|
|
|
|
|
# 定义开始和结束日期,用于生成日期范围(虽然在代码中未使用此变量)
|
|
|
|
|
startdate = '2023-01-10'
|
|
|
|
|
enddate = '2024-09-15'
|
|
|
|
|
# 生成日期列表,格式为 'YYYY-MM-DD'
|
|
|
|
|
date = [x for x in pd.date_range(startdate, enddate).strftime('%Y-%m-%d')]
|
|
|
|
|
|
|
|
|
|
# 定义 Excel 文件名,用于保存弹幕数据
|
|
|
|
|
file_xlsx = '我的全部弹幕.xlsx'
|
|
|
|
|
|
|
|
|
|
# 创建一个新的 Excel 工作簿和工作表,并添加标题行 '弹幕'
|
|
|
|
|
total_workbook = openpyxl.Workbook()
|
|
|
|
|
total_sheet = total_workbook.active
|
|
|
|
|
total_sheet.append(['弹幕'])
|
|
|
|
|
|
|
|
|
|
# 定义 B 站弹幕 API 的基础 URL,其中 {number} 是占位符,用于填充视频的 cid 号
|
|
|
|
|
tempApi = 'https://api.bilibili.com/x/v1/dm/list.so?oid={number}'
|
|
|
|
|
|
|
|
|
|
# 定义请求头,包含 cookie 和 user-agent,用于伪装请求
|
|
|
|
|
headers = {
|
|
|
|
|
'cookie': "buvid3=D65868DE-AFD5-34A4-1714-A1C0F783C5DC27124infoc; b_nut=1725930527; _uuid=FF569C27-D2C6-10814-36A8-48AA8141364924857infoc; CURRENT_FNVAL=4048; buvid_fp=2ba89565eab107e1e14c7982fc1ef9ea; buvid4=FAB9A58B-B8F5-8DAF-2AC4-4E874D3D1F0E28371-024091001-a%2FA7nVxQVETBwJOeuHlVsQ%3D%3D; rpdid=|(u))kkYu|lu0J'u~klmJ|lkm; DedeUserID=1917958039; DedeUserID__ckMd5=eaa26b970b7e3104; header_theme_version=CLOSE; enable_web_push=DISABLE; home_feed_column=5; browser_resolution=1536-730; bp_t_offset_1917958039=976131738646347776; SESSDATA=388559ba%2C1742109747%2Cdc4ae%2A91CjByrR8jH29CX_2ZktYkcWo9nu9b6csyqZX7Z52SZr2CCIHMMi3VsnapfsZ3vuXyPv4SVlhpOC1KYWpoNVExc0RPSFFCVG0zTGxwUDZNdWs5NDU4ZktBYmkzRUlFSHlDbVF2cVk4RHhDWG5BTFRxVl9oM25JdUJxWDhTWnE2dENQQ3FERUNqSER3IIEC; bili_jct=1e53039d135ff42e0131bfed9b577c34; sid=69pzvo9r; bili_ticket=eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MjY4MTY5NTMsImlhdCI6MTcyNjU1NzY5MywicGx0IjotMX0.-gfoOKe3UugjUrDKiGu2ggTujyv2qI_7XZ_usWbEMvI; bili_ticket_expires=1726816893; b_lsid=AEC784B8_19203376015",
|
|
|
|
|
'user-agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# 全局缓存,用于存储 bvid 和 cid,避免重复请求
|
|
|
|
|
bvid_cache = {}
|
|
|
|
|
cid_cache = {}
|
|
|
|
|
|
|
|
|
|
# 异步函数:获取 bvid,带缓存功能
|
|
|
|
|
async def get_bvid(session, page, index):
|
|
|
|
|
# 如果已经在缓存中,则直接返回缓存的 bvid
|
|
|
|
|
if (page, index) in bvid_cache:
|
|
|
|
|
return bvid_cache[(page, index)]
|
|
|
|
|
|
|
|
|
|
# 构造 API 请求的 URL,查询指定页码和关键字的视频
|
|
|
|
|
url = f'https://api.bilibili.com/x/web-interface/search/type?page={page}&page_size=50&keyword=2028%E5%B9%B4%E6%B4%9B%E6%9D%89%E7%9F%B6%E5%A5%A5%E8%BF%90%E4%BC%9A&search_type=video'
|
|
|
|
|
|
|
|
|
|
# 发送异步 GET 请求
|
|
|
|
|
async with session.get(url) as response:
|
|
|
|
|
try:
|
|
|
|
|
# 尝试将响应内容解析为 JSON 格式
|
|
|
|
|
json_data = await response.json()
|
|
|
|
|
# 提取第 index 个视频的 bvid
|
|
|
|
|
bvid = json_data['data']['result'][index]['bvid']
|
|
|
|
|
# 将 bvid 存入缓存
|
|
|
|
|
bvid_cache[(page, index)] = bvid
|
|
|
|
|
return bvid
|
|
|
|
|
except (KeyError, IndexError, json.JSONDecodeError) as e:
|
|
|
|
|
# 如果出现异常,打印错误信息和响应内容,返回 None
|
|
|
|
|
print(f"获取 bvid 时出错: {e}")
|
|
|
|
|
print(f"响应状态码: {response.status}")
|
|
|
|
|
text = await response.text()
|
|
|
|
|
print(f"响应内容: {text}")
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
# 异步函数:获取 cid,带缓存功能
|
|
|
|
|
async def get_cid(session, bvid):
|
|
|
|
|
# 如果 bvid 已经在缓存中,则直接返回缓存的 cid
|
|
|
|
|
if bvid in cid_cache:
|
|
|
|
|
return cid_cache[bvid]
|
|
|
|
|
|
|
|
|
|
# 构造 API 请求的 URL,查询指定 bvid 的视频信息
|
|
|
|
|
url = f'https://api.bilibili.com/x/player/pagelist?bvid={bvid}&jsonp=jsonp'
|
|
|
|
|
|
|
|
|
|
# 发送异步 GET 请求
|
|
|
|
|
async with session.get(url) as response:
|
|
|
|
|
try:
|
|
|
|
|
# 尝试将响应内容解析为 JSON 格式
|
|
|
|
|
json_dict = await response.json()
|
|
|
|
|
# 提取第一个视频的 cid
|
|
|
|
|
cid = json_dict['data'][0]['cid']
|
|
|
|
|
# 将 cid 存入缓存
|
|
|
|
|
cid_cache[bvid] = cid
|
|
|
|
|
return cid
|
|
|
|
|
except (KeyError, IndexError, json.JSONDecodeError):
|
|
|
|
|
# 如果出现异常,返回 None
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
# 异步函数:获取并保存某个视频的弹幕
|
|
|
|
|
async def fetch_and_save_bulletchat(session, cid):
|
|
|
|
|
# 使用 cid 构造弹幕 API 的 URL
|
|
|
|
|
url = tempApi.replace("{number}", str(cid))
|
|
|
|
|
try:
|
|
|
|
|
# 发送异步 GET 请求
|
|
|
|
|
async with session.get(url) as response:
|
|
|
|
|
# 获取响应的文本内容(XML 格式)
|
|
|
|
|
response_text = await response.text()
|
|
|
|
|
# 使用正则表达式提取所有弹幕内容
|
|
|
|
|
data = re.findall('<d p=".*?">(.*?)</d>', response_text)
|
|
|
|
|
# 如果有弹幕数据,返回列表,否则返回空列表
|
|
|
|
|
return data if data else []
|
|
|
|
|
except:
|
|
|
|
|
# 如果出现异常,返回空列表
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
# 异步函数:处理并发任务,收集所有弹幕数据
|
|
|
|
|
async def fetch_all_bulletchats(session):
|
|
|
|
|
all_bulletchats = [] # 用于存储所有的弹幕数据
|
|
|
|
|
tasks = [] # 用于存储所有的异步任务
|
|
|
|
|
|
|
|
|
|
total_requests = 6 * 50 # 总共请求 6 页,每页 50 个视频,共 300 个视频
|
|
|
|
|
for i in range(total_requests):
|
|
|
|
|
page_number = i // 50 + 1 # 计算当前请求的页码
|
|
|
|
|
index = i % 50 # 计算当前页内的索引
|
|
|
|
|
# 创建异步任务,获取每个视频的弹幕数据
|
|
|
|
|
tasks.append(asyncio.ensure_future(fetch_bulletchat_data(session, page_number, index)))
|
|
|
|
|
|
|
|
|
|
# 使用 asyncio.as_completed 来迭代已完成的任务
|
|
|
|
|
for task in asyncio.as_completed(tasks):
|
|
|
|
|
bulletchat_data = await task
|
|
|
|
|
if bulletchat_data:
|
|
|
|
|
# 将获取的弹幕数据添加到总列表中
|
|
|
|
|
all_bulletchats.extend(bulletchat_data)
|
|
|
|
|
|
|
|
|
|
return all_bulletchats # 返回所有的弹幕数据
|
|
|
|
|
|
|
|
|
|
# 异步函数:获取单个视频的弹幕数据
|
|
|
|
|
async def fetch_bulletchat_data(session, page_number, index):
|
|
|
|
|
# 获取视频的 bvid
|
|
|
|
|
bvid = await get_bvid(session, page_number, index)
|
|
|
|
|
if bvid:
|
|
|
|
|
# 获取视频的 cid
|
|
|
|
|
cid = await get_cid(session, bvid)
|
|
|
|
|
if cid:
|
|
|
|
|
# 获取并返回视频的弹幕数据
|
|
|
|
|
return await fetch_and_save_bulletchat(session, cid)
|
|
|
|
|
return [] # 如果获取失败,返回空列表
|
|
|
|
|
|
|
|
|
|
# 函数:保存弹幕数据到文本文件和 Excel 文件
|
|
|
|
|
def save_to_file(bulletchats):
|
|
|
|
|
# 以追加模式打开文本文件,编码为 utf-8
|
|
|
|
|
with open('我的全部弹幕.txt', 'a', encoding='utf-8') as file_txt:
|
|
|
|
|
for index in bulletchats:
|
|
|
|
|
# 将每条弹幕写入文本文件,并换行
|
|
|
|
|
file_txt.write(index + '\n')
|
|
|
|
|
# 将弹幕写入 Excel 表格
|
|
|
|
|
total_sheet.append([index])
|
|
|
|
|
# 保存 Excel 文件
|
|
|
|
|
total_workbook.save(file_xlsx)
|
|
|
|
|
|
|
|
|
|
# 函数:计算弹幕频次,并保存到 Excel 文件
|
|
|
|
|
def calculate_frequency():
|
|
|
|
|
try:
|
|
|
|
|
# 读取 Excel 文件中的弹幕数据
|
|
|
|
|
fd = pd.read_excel(file_xlsx)
|
|
|
|
|
lines = fd['弹幕']
|
|
|
|
|
# 将所有弹幕拼接成一个字符串
|
|
|
|
|
text = ' '.join(lines.astype(str))
|
|
|
|
|
# 将字符串按照空格分割为单词列表
|
|
|
|
|
words = text.split()
|
|
|
|
|
# 使用 collections.Counter 统计词频
|
|
|
|
|
word_counts = collections.Counter(words)
|
|
|
|
|
# 将词频按照出现次数从高到低排序
|
|
|
|
|
sorted_word_counts = sorted(word_counts.items(), key=lambda x: x[1], reverse=True)
|
|
|
|
|
|
|
|
|
|
# 创建一个新的 Excel 工作簿和工作表,并添加标题行
|
|
|
|
|
workbook = openpyxl.Workbook()
|
|
|
|
|
sheet = workbook.active
|
|
|
|
|
sheet.append(['弹幕', '频次'])
|
|
|
|
|
|
|
|
|
|
# 将排序后的词频数据写入 Excel 表格
|
|
|
|
|
for word, count in sorted_word_counts:
|
|
|
|
|
sheet.append([word, count])
|
|
|
|
|
|
|
|
|
|
# 保存统计结果到新的 Excel 文件
|
|
|
|
|
workbook.save('我的统计弹幕出现次数.xlsx')
|
|
|
|
|
except Exception as e:
|
|
|
|
|
# 如果出现异常,打印错误信息
|
|
|
|
|
print(f"计算频次时出错: {e}")
|
|
|
|
|
|
|
|
|
|
# 异步主函数,负责执行整个流程
|
|
|
|
|
async def main():
|
|
|
|
|
# 创建一个异步的 HTTP 会话,使用指定的请求头
|
|
|
|
|
async with aiohttp.ClientSession(headers=headers) as session:
|
|
|
|
|
# 异步获取所有弹幕数据
|
|
|
|
|
bulletchats = await fetch_all_bulletchats(session)
|
|
|
|
|
# 保存弹幕数据到文件
|
|
|
|
|
save_to_file(bulletchats)
|
|
|
|
|
# 计算弹幕频次并保存结果
|
|
|
|
|
calculate_frequency()
|
|
|
|
|
# 输出流程结束信息
|
|
|
|
|
print("Finished")
|
|
|
|
|
|
|
|
|
|
# 启动异步任务
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
|
asyncio.run(main())
|
|
|
|
|
|
|
|
|
|
# 停止性能分析
|
|
|
|
|
profile.disable()
|
|
|
|
|
# 将性能分析数据保存到文件中
|
|
|
|
|
profile.dump_stats('./youhua.prof')
|