|
|
# -*- coding: utf-8 -*-
|
|
|
import requests
|
|
|
from bs4 import BeautifulSoup
|
|
|
import time
|
|
|
import os
|
|
|
from requests.adapters import HTTPAdapter
|
|
|
from urllib3.util.retry import Retry
|
|
|
|
|
|
class BilibiliDanmakuCrawler:
|
|
|
def __init__(self, keyword, video_count, save_dir):
|
|
|
self.keyword = keyword
|
|
|
self.video_count = video_count
|
|
|
self.save_dir = save_dir
|
|
|
os.makedirs(save_dir, exist_ok=True)
|
|
|
|
|
|
# 带重试机制的会话(解决网络波动)
|
|
|
self.session = requests.Session()
|
|
|
retry = Retry(
|
|
|
total=3,
|
|
|
backoff_factor=1,
|
|
|
status_forcelist=[412, 429, 500, 502, 503, 504]
|
|
|
)
|
|
|
self.session.mount('http://', HTTPAdapter(max_retries=retry))
|
|
|
self.session.mount('https://', HTTPAdapter(max_retries=retry))
|
|
|
|
|
|
# 请求头(替换为你的Cookie)
|
|
|
self.headers = {
|
|
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
|
|
|
"Referer": "https://search.bilibili.com/",
|
|
|
"Cookie": "buvid3=E8B6A22C-2C45-4243-1A50-0B7887C84A4500588infoc; rpdid=|(u))kkYu|um0J'u~k)~mYm|u; b_nut=100; b_lsid=BD6D42CE_19A76AED18B; bsource=search_sougo; _uuid=BD2459B10-A5107-5424-57D5-D3E3C2510222859761infoc; buvid_fp=01d6b98373d0ee5ae25897960af410a8; home_feed_column=5; browser_resolution=1699-941; buvid4=FD45C162-3221-ACF9-4FB1-19AE076778C860860-025111214-xHcqiQBrPSLcJVMwvAmRJw%3D%3D; CURRENT_QUALITY=0; csrf_state=e8a98437773776f3420329dc3758a34c; bili_ticket=eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NjMxOTAwMzEsImlhdCI6MTc2MjkzMDc3MSwicGx0IjotMX0.wLjhNhZSAz-VJZCGAEltHftitM-90-C1nSRr_JtoVII; bili_ticket_expires=1763189971; SESSDATA=36fd0664%2C1778482862%2Ce1d71%2Ab1CjBUqeAMzYv1RDPYIrJSzYNj9v_TLDaM3RXdELRfLIrNqKJHA7i5yvvzwA3AiDKvw4ISVkN3Q0lpNFcwU2pLNk9ZakczZklvOWsxTWZvcm9KMzNBR1R1d05kOTcwU1BSSWY1RVdBWmtmUHY3VURDektoVHNJVTVFSF9vQlFmSlY4ZDRERkFfaF9nIIEC; bili_jct=3380c36097411cca708626cb4e7a81d6; DedeUserID=409262887; DedeUserID__ckMd5=234150aa4a4b4661; bp_t_offset_409262887=1134268504589991936; theme-tip-show=SHOWED; CURRENT_FNVAL=4048; sid=5uxkxypo"
|
|
|
}
|
|
|
|
|
|
def get_top_bv_ids(self):
|
|
|
"""获取视频BV号列表"""
|
|
|
bv_list = []
|
|
|
page = 1
|
|
|
max_pages = 20 # 最大页数
|
|
|
print(f"开始获取「{self.keyword}」相关视频BV号...")
|
|
|
|
|
|
while len(bv_list) < self.video_count and page <= max_pages:
|
|
|
api_url = (
|
|
|
f"https://api.bilibili.com/x/web-interface/search/type"
|
|
|
f"?keyword={self.keyword}&search_type=video&order=totalrank&page={page}"
|
|
|
)
|
|
|
try:
|
|
|
resp = self.session.get(api_url, headers=self.headers, timeout=15)
|
|
|
resp.raise_for_status()
|
|
|
data = resp.json()
|
|
|
|
|
|
if data.get("code") != 0:
|
|
|
print(f"第{page}页错误:{data.get('message')}")
|
|
|
page += 1
|
|
|
time.sleep(2)
|
|
|
continue
|
|
|
|
|
|
# 解析BV号并去重
|
|
|
new_count = 0
|
|
|
for item in data["data"]["result"]:
|
|
|
bv = item.get("bvid")
|
|
|
if bv and bv not in bv_list:
|
|
|
bv_list.append(bv)
|
|
|
new_count += 1
|
|
|
if len(bv_list) >= self.video_count:
|
|
|
break
|
|
|
|
|
|
print(f"第{page}页处理完成,新增{new_count}个,共{len(bv_list)}个")
|
|
|
page += 1
|
|
|
time.sleep(1.5)
|
|
|
|
|
|
except Exception as e:
|
|
|
print(f"第{page}页请求失败:{str(e)},重试...")
|
|
|
time.sleep(3)
|
|
|
|
|
|
# 保存BV号
|
|
|
with open(f"{self.save_dir}/bv_list.txt", "w", encoding="utf-8") as f:
|
|
|
f.write("\n".join(bv_list))
|
|
|
print(f"BV号获取完成,共{len(bv_list)}个")
|
|
|
return bv_list
|
|
|
|
|
|
def get_cid_by_bv(self, bv):
|
|
|
"""通过BV号获取CID"""
|
|
|
try:
|
|
|
url = f"https://api.bilibili.com/x/web-interface/view?bvid={bv}"
|
|
|
resp = self.session.get(url, headers=self.headers, timeout=15)
|
|
|
return resp.json()["data"]["cid"]
|
|
|
except Exception as e:
|
|
|
print(f"BV号「{bv}」获取CID失败:{str(e)}")
|
|
|
return None
|
|
|
|
|
|
def crawl_danmaku(self, cid):
|
|
|
"""爬取弹幕并过滤噪声"""
|
|
|
if not cid:
|
|
|
return []
|
|
|
try:
|
|
|
url = f"https://comment.bilibili.com/{cid}.xml"
|
|
|
resp = self.session.get(url, headers=self.headers, timeout=15)
|
|
|
resp.encoding = "utf-8"
|
|
|
soup = BeautifulSoup(resp.text, "xml")
|
|
|
danmu_tags = soup.find_all("d")
|
|
|
|
|
|
# 过滤规则
|
|
|
noise = {"666", "哈哈哈", "点赞", "投币", "收藏", "打卡", "来了"}
|
|
|
valid = [
|
|
|
tag.text.strip() for tag in danmu_tags
|
|
|
if tag.text.strip() and len(tag.text.strip()) > 1
|
|
|
and not any(n in tag.text.strip() for n in noise)
|
|
|
]
|
|
|
return valid
|
|
|
except Exception as e:
|
|
|
print(f"CID「{cid}」爬取失败:{str(e)}")
|
|
|
return []
|
|
|
|
|
|
def run(self):
|
|
|
"""执行爬取流程"""
|
|
|
bv_list = self.get_top_bv_ids()
|
|
|
all_danmu = []
|
|
|
|
|
|
for i, bv in enumerate(bv_list, 1):
|
|
|
print(f"处理第{i}/{len(bv_list)}个视频(BV:{bv})")
|
|
|
cid = self.get_cid_by_bv(bv)
|
|
|
all_danmu.extend(self.crawl_danmaku(cid))
|
|
|
time.sleep(1)
|
|
|
|
|
|
# 保存弹幕
|
|
|
with open(f"{self.save_dir}/all_danmu.txt", "w", encoding="utf-8") as f:
|
|
|
f.write("\n".join(all_danmu))
|
|
|
print(f"爬取完成,共{len(all_danmu)}条有效弹幕")
|
|
|
return all_danmu |