爬取B站视频评论数据爬取与分析
如果只要单纯的脚本可以直接看项目结构里的b_comments.py
一、技术架构
1、环境配置
- Python 3.8
- PyCharm
2、模块配置
- requests:用于发送HTTP请求
- time:用于处理时间相关的操作
- csv:用于读写CSV文件
- json:用于处理JSON数据
- hashlib:用于生成哈希值
- urllib.parse:用于URL解析和编码
quote
:URL编码urlparse
:解析URLparse_qs
:解析查询字符串
- selenium:用于自动化Web浏览器操作
webdriver
:控制浏览器驱动By
:定位页面元素的方式WebDriverWait
:等待条件满足expected_conditions
(别名EC
):定义期望条件Options
:配置浏览器选项TimeoutException
:超时异常NoSuchElementException
:找不到元素异常
- scrapy:用于构建爬虫框架
使用指令下载:pip install 模块名
-
使用阿里云镜像
pip install 模块名 -i https://mirrors.aliyun.com/pypi/simple/ -
使用清华镜像
pip install 模块名 -i https://pypi.tuna.tsinghua.edu.cn/simple
3、数据来源
- 指定视频地址,比如"火柴人 VS 玩家 第零集 - 村庄保卫战":https://www.bilibili.com/video/BV1uDMXzBELa/?vd_source=2cedb2069146c8936939b253694aab4f
4、抓包分析获取数据包地址
b站的视频评论需要抓包获取
- 打开开发者工具F12按键,找到Network(网络)
- 搜索评论区的一些关键字,找到评论区数据所在地址
-
找到数据包,复制地址
-
该视频评论数据包地址:https://api.bilibili.com/x/v2/reply/wbi/main?oid=114840966336346&type=1&mode=3&pagination_str=%7B%22offset%22:%22%22%7D&plat=1&seek_rpid=&web_location=1315875&w_rid=d4269d68b7c818cabfd749dd8da6663d&wts=1752742803
二、实现步骤(发送,获取,解析,保存)
1. 发送请求
设置请求头,防止403错误(拒绝访问)
- 在请求标头里找到 Cookie,User-Agent,Referer,把值复制过来
headers = {"Cookie": "你的b站cookie","User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0","Referer": "https://www.bilibili.com/video/BV1uDMXzBELa/?vd_source=2cedb2069146c8936939b253694aab4f"
}
response = reponse_get(url, headers=headers)
2、获取数据
请求网址: https://api.bilibili.com/x/v2/reply/wbi/main (不需要完整数据包地址,只需要?号前的链接,后面是查询参数)
参数:
获取响应json数据
link = 'https://api.bilibili.com/x/v2/reply/wbi/main'
params = {'oid': '114840966336346','type': '1','mode': '3','pagination_str': '{"offset":""}','plat': '1','seek_rpid': '','web_location': '1315875','w_rid': 'd4269d68b7c818cabfd749dd8da6663d','wts': '1752742803'
}
JsonData = requests.get(link, params=params).json()
print(JsonData)
3、解析数据
找到数据位置
- 可以看到数据在replies列表里,所以可以遍历replies列表,提取列表里的元素,再提取具体的数据
具体数据
for index in JsonData['data']['replies']:dit = {'uid': index['member']['mid'],'昵称': index['member']['uname'],'性别': index['member']['sex'],'地区': index['reply_control']['location'].replace('IP属地:',''), # 去除IP属地字段'签名': index['member']['sign'],'等级': index['member']['level_info']['current_level'],'评论内容': index['content']['message'],'评论时间': index['ctime']'点赞数': index['like']}
评论时间戳解析
import time
time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(index['ctime']))
4、保存数据
import csv
with open('B站视频评论数据.csv', 'w', newline='', encoding='utf-8') as f:writer = csv.writer(f)writer.writerow(['uid', '昵称', '性别', '地区', '签名', '等级', '评论内容', '评论时间', '点赞数'])for index in data:writer.writerow([index['mid'], index['uname'], index['sex'], index['location'], index['sign'], index['level'], index['content'], time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(index['ctime'])), index['like']])
三、翻页爬取(数据量大,需要翻页的视频)
分析请求链接/参数的变化规律
加载新的评论页参看参数变化
-
不断往下翻滚页面,请求的参数会改变
-
查看各页码的请求链接、参数
-
可以看到链接不变,参数有pagination_str、w_rid、wts改变,然后是只有第一页有seek_rpid参数
找出变化规律
- wts:时间戳,可以用time模块获取当前时间戳
- pagination_str:翻页参数,每次翻页都会改变
- w_rid:评论id,每次翻页都会改变
我们可以发现wts和pagination_str的变化比较明显,wts与当前时间有关,
pagination_str除了第一页,后面都是一样的参数
找出w_rid的变化规律
- 按键:Ctrl+shift+f搜索w_rid
发现有四个匹配项
- 匹配项1:web.min.js
- 匹配项2:bili-headerumd.js
- 匹配项3:video.871f5df8af85b2dfea40b0804ba2a6f4c883dc90.js
- 匹配项4:core.deb01adc.js
通过断点观察,发现w_rid的参数值是在web.min.js文件中的,最后得到固定密钥和MD5加密
- 密钥: ea1db124af3c7062474693fa704f4ff8
- 计算过程:
得出w_rid计算方法
def get_w_rid(params, wts):"""动态生成w_rid参数"""keys_order = ['mode', 'oid', 'pagination_str', 'plat', 'seek_rpid', 'type', 'web_location']items = []for key in keys_order:value = params[key]if key == 'pagination_str':value = quote(value)items.append(f"{key}={value}")items.append(f"wts={wts}")s = '&'.join(items) + 'ea1db124af3c7062474693fa704f4ff8'md5 = hashlib.md5()md5.update(s.encode('utf-8'))return md5.hexdigest()
通过上面的方法,我们可以得到w_rid参数,然后就可以进行翻页爬取了
四、指定视频爬取
分析不同视频的参数
-发现参数中有区别的是oid,w_rid,wts
我们在前面就得到了wts和w_rid的计算方法,现在只需要获取oid即可,而oid就是b站视频的av/bv号。
而有时候b站视频的链接除了主要链接部分,其余部分是参数,所以需要提取出av/bv号
def extract_bvid(url):"""从 B 站视频链接中提取 BV 号或 AV 号支持各种参数、短链接、移动端链接等"""parsed = urlparse(url)# 尝试从路径中提取if parsed.netloc in ["www.bilibili.com", "m.bilibili.com"]:path_parts = parsed.path.strip('/').split('/')for part in path_parts:if part.startswith("BV") or part.startswith("AV"):return part# 尝试从查询参数中提取query = parse_qs(parsed.query)if 'BV' in query:return query['BV'][0]elif 'AV' in query or 'av' in query:return query.get('AV', query.get('av', [''])[0])raise ValueError("无法从链接中提取有效的 BV 号或 AV 号")
五、功能优化
在运行爬取之后我们发现爬取到的数据只是一级评论,没有回复一级评论的二级评论,所以我们需要爬取二级评论以获取完整数据
1.参看二级评论数据所在位置
和查找一级评论数据一样,直接搜索二级评论
-
请求URL:https://api.bilibili.com/x/v2/reply/reply
-
然后参看二级评论的参数以及参数变化
-
发现只有root和pn有变化,然后是在同一个root下,pn代表页码,现在我们要找到root从哪里获取
-
搜索root
-
发现二级评论的root值就是一级评论的rpid,而一级评论也有root,不过值为0
2.函数编写
所以我们可以编写一个函数用来爬取二级评论
def fetch_sub_comments(oid, type, root, ps=10):"""获取二级评论数据"""sub_comments = []pn = 1while True:params = {'oid': oid,'type': type,'root': root,'ps': ps,'pn': pn,'web_location': '333.788'}try:print(f" 正在获取二级评论,root: {root}, 页码: {pn}")response = requests.get(reply_url, headers=headers, params=params)if response.status_code != 200:print(f" 获取二级评论失败,状态码: {response.status_code}")breakjson_data = response.json()# 检查是否有数据if not json_data.get("data") or not json_data["data"].get("replies"):print(f" 第{pn}页没有二级评论数据")break# 解析二级评论for reply in json_data['data']['replies']:comment_time = reply.get('ctime')like_count = reply.get('like', 0)comment_content = reply.get('content', {})reply_control = reply.get('reply_control', {})member_info = reply.get('member', {})# 格式化时间format_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(comment_time)) if comment_time else "未知时间"comment = {'uid': member_info.get('mid', ''),'昵称': member_info.get('uname', ''),'性别': member_info.get('sex', ''),'地区': reply_control.get('location', '').replace('IP属地:', '').strip(),'签名': member_info.get('sign', ''),'等级': member_info.get('level_info', {}).get('current_level', ''),'评论内容': comment_content.get('message', ''),'评论时间': format_time,'点赞数': like_count,'评论等级': '二级评论','父评论ID': root}sub_comments.append(comment)# 检查是否还有下一页if json_data['data'].get('page'):acount = json_data['data']['page'].get('acount', 0)size = json_data['data']['page'].get('size', ps)count = json_data['data']['page'].get('count', 0)# 如果当前页是最后一页,则停止if pn * size >= acount:breakelse:breakpn += 1time.sleep(0.5) # 避免请求过快except Exception as e:print(f" 获取二级评论时发生错误: {e}")breakprint(f" 共获取到 {len(sub_comments)} 条二级评论")return sub_comments
对一级评论爬取函数修改
def parse_comments(json_data, oid, type_val):"""解析评论数据,安全访问字段,并获取二级评论"""comments = []if not json_data.get("data") or not json_data["data"].get("replies"):if json_data.get("data", {}).get("cursor", {}).get("is_end", True):return comments, None # 没有更多数据raise Exception("未找到评论数据,请检查参数或 Cookie 是否有效")for index in json_data['data']['replies']:comment_time = index.get('ctime')like_count = index.get('like', 0)comment_content = index.get('content', {})reply_control = index.get('reply_control', {})member_info = index.get('member', {})rpid = index.get('rpid', 0) # 一级评论ID,用于获取二级评论root_val = index.get('root', 0)# 格式化时间format_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(comment_time)) if comment_time else "未知时间"# 一级评论comment = {'uid': member_info.get('mid', ''),'昵称': member_info.get('uname', ''),'性别': member_info.get('sex', ''),'地区': reply_control.get('location', '').replace('IP属地:', '').strip(),'签名': member_info.get('sign', ''),'等级': member_info.get('level_info', {}).get('current_level', ''),'评论内容': comment_content.get('message', ''),'评论时间': format_time,'点赞数': like_count,'评论等级': '一级评论' if root_val == 0 else '二级评论','父评论ID': root_val if root_val != 0 else ''}comments.append(comment)# 如果有一级评论有回复,获取其二级评论if index.get('replies'):for sub_reply in index['replies']:sub_comment_time = sub_reply.get('ctime')sub_like_count = sub_reply.get('like', 0)sub_comment_content = sub_reply.get('content', {})sub_reply_control = sub_reply.get('reply_control', {})sub_member_info = sub_reply.get('member', {})sub_root_val = sub_reply.get('root', 0)sub_format_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(sub_comment_time)) if sub_comment_time else "未知时间"sub_comment = {'uid': sub_member_info.get('mid', ''),'昵称': sub_member_info.get('uname', ''),'性别': sub_member_info.get('sex', ''),'地区': sub_reply_control.get('location', '').replace('IP属地:', '').strip(),'签名': sub_member_info.get('sign', ''),'等级': sub_member_info.get('level_info', {}).get('current_level', ''),'评论内容': sub_comment_content.get('message', ''),'评论时间': sub_format_time,'点赞数': sub_like_count,'评论等级': '二级评论','父评论ID': sub_root_val}comments.append(sub_comment)elif index.get('count', 0) > 0: # 如果有更多二级评论需要单独请求# 获取完整的二级评论sub_comments = fetch_sub_comments(oid, type_val, rpid)comments.extend(sub_comments)# 获取下一页的offsetnext_offset = json_data['data']['cursor']['pagination_reply'].get('next_offset', None)return comments, next_offset
3.cookie自动获取
对于一个爬虫脚本,有时候对cookie的获取可以自动化或者手动输入
- 这里我们选择用selenium模拟浏览器获取cookie
自动登录获取cookie的函数
def login_bilibili():"""使用Selenium登录B站获取Cookie"""print("正在启动浏览器进行B站登录...")# 设置Chrome选项chrome_options = Options()# 注释掉下面这行可以查看登录过程# chrome_options.add_argument('--headless') # 无头模式chrome_options.add_argument('--no-sandbox')chrome_options.add_argument('--disable-dev-shm-usage')chrome_options.add_argument('--disable-blink-features=AutomationControlled')chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])chrome_options.add_experimental_option('useAutomationExtension', False)driver = Nonetry:# 启动浏览器driver = webdriver.Chrome(options=chrome_options)driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {"source": "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"})# 访问B站登录页面driver.get("https://passport.bilibili.com/login")print("请在浏览器中完成登录操作...")print("登录成功后,程序会自动继续执行...")# 等待登录成功(通过检查是否跳转到首页)WebDriverWait(driver, 300).until(EC.url_contains("https://www.bilibili.com/"))# 等待页面完全加载time.sleep(3)# 获取Cookiecookies = driver.get_cookies()cookie_str = "; ".join([f"{cookie['name']}={cookie['value']}" for cookie in cookies])print("登录成功,Cookie获取完成!")return cookie_strexcept TimeoutException:print("登录超时,请重试")return Noneexcept Exception as e:print(f"登录过程中发生错误: {e}")return Nonefinally:if driver:driver.quit()
六、项目打包
1. 项目结构说明
项目采用模块化结构设计,便于维护和扩展(dist目录是在运行时生成,用于存放打包后的文件,不用手动创建):
B站评论爬虫/
│
├── src/ # 源代码目录
│ ├── b_comments.py # 主爬虫脚本
│ ├── build.py # 打包脚本
│ └── dist/ # 打包输出目录
│ └── B站评论爬虫.exe # 生成的可执行文件
├── resources/ # 资源文件目录
│ ├── chromedriver.exe # Chrome浏览器驱动
│ └── b_crawler_config.json # 配置文件
└── README.md # 项目说明文档
2. src/b_comments.py
import requests
import time
import csv
import json
import hashlib
import os
import sys
from urllib.parse import quote, urlparse, parse_qs
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.chrome.options import Options
from selenium.common.exceptions import TimeoutException, NoSuchElementException
from selenium.webdriver.chrome.service import Service as ChromeServiceclass BilibiliCommentCrawler:def __init__(self):self.config_file = 'b_crawler_config.json'self.cookie = Noneself.headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0","Referer": "https://www.bilibili.com"}# 评论接口基础 URLself.base_url = "https://api.bilibili.com/x/v2/reply/wbi/main"# 二级评论接口 URLself.reply_url = "https://api.bilibili.com/x/v2/reply/reply"# 加载配置self.load_config()def resource_path(self, relative_path):"""获取资源的绝对路径。用于PyInstaller打包后定位资源文件。"""base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))return os.path.join(base_path, relative_path)def load_config(self):"""加载配置文件"""if os.path.exists(self.config_file):try:with open(self.config_file, 'r', encoding='utf-8') as f:config = json.load(f)self.cookie = config.get('cookie')print("已加载保存的Cookie")except:print("配置文件损坏,将创建新的配置文件")self.cookie = Noneelse:print("未找到配置文件,将创建新的配置文件")def save_config(self):"""保存配置到文件"""config = {'cookie': self.cookie}with open(self.config_file, 'w', encoding='utf-8') as f:json.dump(config, f, ensure_ascii=False, indent=2)print("配置已保存")def login_bilibili(self):"""使用Selenium登录B站获取Cookie"""print("正在启动浏览器进行B站登录...")# 设置Chrome选项chrome_options = Options()# 注释掉下面这行可以查看登录过程# chrome_options.add_argument('--headless') # 无头模式chrome_options.add_argument('--no-sandbox')chrome_options.add_argument('--disable-dev-shm-usage')chrome_options.add_argument('--disable-blink-features=AutomationControlled')chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])chrome_options.add_experimental_option('useAutomationExtension', False)driver = Nonetry:# 获取chromedriver路径chromedriver_path = self.resource_path('chromedriver.exe')print(f"使用chromedriver路径: {chromedriver_path}")# 启动浏览器service = ChromeService(executable_path=chromedriver_path)driver = webdriver.Chrome(service=service, options=chrome_options)driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {"source": "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"})# 访问B站登录页面driver.get("https://passport.bilibili.com/login")print("请在浏览器中完成登录操作...")print("登录成功后,程序会自动继续执行...")# 等待登录成功(通过检查是否跳转到首页)WebDriverWait(driver, 300).until(EC.url_contains("https://www.bilibili.com/"))# 等待页面完全加载time.sleep(3)# 获取Cookiecookies = driver.get_cookies()cookie_str = "; ".join([f"{cookie['name']}={cookie['value']}" for cookie in cookies])print("登录成功,Cookie获取完成!")return cookie_strexcept TimeoutException:print("登录超时,请重试")return Noneexcept Exception as e:print(f"登录过程中发生错误: {e}")return Nonefinally:if driver:driver.quit()def auto_get_cookie(self):"""使用Selenium自动获取B站Cookie(非登录方式)"""print("正在自动获取B站Cookie...")# 设置Chrome选项chrome_options = Options()chrome_options.add_argument('--headless') # 无头模式chrome_options.add_argument('--no-sandbox')chrome_options.add_argument('--disable-dev-shm-usage')chrome_options.add_argument('--disable-blink-features=AutomationControlled')chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])chrome_options.add_experimental_option('useAutomationExtension', False)driver = Nonetry:# 获取chromedriver路径chromedriver_path = self.resource_path('chromedriver.exe')print(f"使用chromedriver路径: {chromedriver_path}")# 启动浏览器service = ChromeService(executable_path=chromedriver_path)driver = webdriver.Chrome(service=service, options=chrome_options)driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {"source": "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"})driver.get("https://www.bilibili.com")# 等待页面加载WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.TAG_NAME, "body")))# 获取Cookiecookies = driver.get_cookies()cookie_str = "; ".join([f"{cookie['name']}={cookie['value']}" for cookie in cookies])print("Cookie获取成功!")return cookie_strexcept Exception as e:print(f"自动获取Cookie失败: {e}")return Nonefinally:if driver:driver.quit()def manual_input_cookie(self):"""手动输入Cookie"""print("\n请手动输入Cookie:")print("1. 打开浏览器访问 https://www.bilibili.com")print("2. 按F12打开开发者工具")print("3. 切换到Network标签页")print("4. 刷新页面,在任意请求中找到Request Headers中的Cookie")print("5. 复制完整的Cookie值并粘贴到这里\n")cookie = input("请输入Cookie: ").strip()return cookiedef get_cookie(self):"""获取Cookie的主函数"""# 如果已有cookie,询问是否使用现有cookieif self.cookie:print("\n检测到已保存的Cookie")choice = input("是否使用现有Cookie? (y/n): ").strip().lower()if choice == 'y' or choice == '':# 验证现有cookieif self.validate_cookie(self.cookie):return self.cookieelse:print("现有Cookie已失效,请重新获取")while True:print("\n请选择获取Cookie的方式:")print("1. 自动获取Cookie (无需登录,可能只能获取部分评论)")print("2. 手动输入Cookie")print("3. 登录获取Cookie (推荐,可获取完整评论)")print("4. 退出程序")choice = input("请输入选择 (1/2/3/4): ").strip()cookie = Noneif choice == "1":cookie = self.auto_get_cookie()elif choice == "2":cookie = self.manual_input_cookie()elif choice == "3":cookie = self.login_bilibili()elif choice == "4":print("程序已退出")exit()else:print("无效选择,请重新输入")continueif cookie:# 验证Cookie是否有效if self.validate_cookie(cookie):# 保存新cookieself.cookie = cookieself.save_config()return cookieelse:print("Cookie无效,请重新获取")else:print("获取Cookie失败,请重新尝试")def validate_cookie(self, cookie):"""验证Cookie是否有效"""try:headers = {"Cookie": cookie,"User-Agent": self.headers["User-Agent"],"Referer": self.headers["Referer"]}# 尝试访问一个简单的API来验证Cookietest_url = "https://api.bilibili.com/x/web-interface/nav"response = requests.get(test_url, headers=headers, timeout=5)if response.status_code == 200:data = response.json()if data.get("code") == 0:print("Cookie验证成功!")return Trueprint("Cookie验证失败!")return Falseexcept Exception as e:print(f"Cookie验证出错: {e}")return Falsedef get_w_rid(self, params, wts):"""动态生成w_rid参数"""keys_order = ['mode', 'oid', 'pagination_str', 'plat', 'seek_rpid', 'type', 'web_location']items = []for key in keys_order:value = params[key]if key == 'pagination_str':value = quote(value)items.append(f"{key}={value}")items.append(f"wts={wts}")s = '&'.join(items) + 'ea1db124af3c7062474693fa704f4ff8'md5 = hashlib.md5()md5.update(s.encode('utf-8'))return md5.hexdigest()def fetch_comments(self, url, headers, params):"""发送请求,获取评论数据"""response = requests.get(url, headers=headers, params=params)if response.status_code != 200:raise Exception(f"请求失败,状态码: {response.status_code}")return response.json()def fetch_sub_comments(self, oid, type_val, root, ps=20):"""获取二级评论数据,支持分页获取所有二级评论"""sub_comments = []pn = 1while True:params = {'oid': oid,'type': type_val,'root': root,'ps': ps,'pn': pn,'web_location': '333.788'}try:print(f" 正在获取二级评论,一级评论ID: {root}, 页码: {pn}")response = requests.get(self.reply_url, headers=self.headers, params=params)if response.status_code != 200:print(f" 获取二级评论失败,状态码: {response.status_code}")breakjson_data = response.json()# 检查是否有数据if not json_data.get("data") or not json_data["data"].get("replies"):print(f" 第{pn}页没有二级评论数据")break# 解析二级评论for reply in json_data['data']['replies']:comment_time = reply.get('ctime')like_count = reply.get('like', 0)comment_content = reply.get('content', {})reply_control = reply.get('reply_control', {})member_info = reply.get('member', {})# 格式化时间format_time = time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(comment_time)) if comment_time else "未知时间"comment = {'uid': member_info.get('mid', ''),'昵称': member_info.get('uname', ''),'性别': member_info.get('sex', ''),'地区': reply_control.get('location', '').replace('IP属地:', '').strip(),'签名': member_info.get('sign', ''),'等级': member_info.get('level_info', {}).get('current_level', ''),'评论内容': comment_content.get('message', ''),'评论时间': format_time,'点赞数': like_count,'评论等级': '二级评论','父评论ID': root}sub_comments.append(comment)# 检查是否还有下一页if json_data['data'].get('page'):# 获取总评论数和当前页信息count = json_data['data']['page'].get('count', 0)page_size = json_data['data']['page'].get('size', ps)page_count = (count + page_size - 1) // page_size # 向上取整计算总页数# 如果当前页是最后一页,则停止if pn >= page_count:breakelse:breakpn += 1time.sleep(0.5) # 避免请求过快except Exception as e:print(f" 获取二级评论时发生错误: {e}")breakprint(f" 一级评论 {root} 下共获取到 {len(sub_comments)} 条二级评论")return sub_commentsdef parse_comments(self, json_data, oid, type_val):"""解析评论数据,安全访问字段,并获取二级评论"""comments = []if not json_data.get("data") or not json_data["data"].get("replies"):if json_data.get("data", {}).get("cursor", {}).get("is_end", True):return comments, None # 没有更多数据raise Exception("未找到评论数据,请检查参数或 Cookie 是否有效")for index in json_data['data']['replies']:comment_time = index.get('ctime')like_count = index.get('like', 0)comment_content = index.get('content', {})reply_control = index.get('reply_control', {})member_info = index.get('member', {})rpid = index.get('rpid', 0) # 一级评论ID,用于获取二级评论root_val = index.get('root', 0)reply_count = index.get('count', 0) # 二级评论总数# 格式化时间format_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(comment_time)) if comment_time else "未知时间"# 一级评论comment = {'uid': member_info.get('mid', ''),'昵称': member_info.get('uname', ''),'性别': member_info.get('sex', ''),'地区': reply_control.get('location', '').replace('IP属地:', '').strip(),'签名': member_info.get('sign', ''),'等级': member_info.get('level_info', {}).get('current_level', ''),'评论内容': comment_content.get('message', ''),'评论时间': format_time,'点赞数': like_count,'评论等级': '一级评论','父评论ID': ''}comments.append(comment)print(f"获取到一级评论: {comment_content.get('message', '')[:30]}...")# 如果该一级评论有二级评论,则获取所有二级评论if reply_count > 0:print(f"检测到一级评论 {rpid} 有 {reply_count} 条二级评论,正在获取...")sub_comments = self.fetch_sub_comments(oid, type_val, rpid, ps=20)comments.extend(sub_comments)else:print(f"一级评论 {rpid} 没有二级评论")# 获取下一页的offsetnext_offset = json_data['data']['cursor']['pagination_reply'].get('next_offset', None)return comments, next_offsetdef save_to_csv(self, comments, filename='B站视频评论数据.csv', mode='a'):"""保存评论数据到 CSV 文件"""with open(filename, mode=mode, newline='', encoding='utf-8-sig') as f:writer = csv.DictWriter(f, fieldnames=['uid', '昵称', '性别', '地区', '签名', '等级', '评论内容', '评论时间', '点赞数', '评论等级', '父评论ID'])if mode == 'w' or (not os.path.exists(filename) and mode == 'a'):writer.writeheader()writer.writerows(comments)print(f"成功保存 {len(comments)} 条评论到文件:{filename}")def extract_bvid(self, url):"""从 B 站视频链接中提取 BV 号或 AV 号支持各种参数、短链接、移动端链接等"""parsed = urlparse(url)# 从路径中提取if parsed.netloc in ["www.bilibili.com", "m.bilibili.com"]:path_parts = parsed.path.strip('/').split('/')for part in path_parts:if part.startswith("BV") or part.startswith("av"):return part# 从查询参数中提取query = parse_qs(parsed.query)if 'bvid' in query:return query['bvid'][0]elif 'BV' in query:return query['BV'][0]elif 'av' in query:return query.get('av', [''])[0]# 处理短链接if "b23.tv" in url:try:response = requests.head(url, allow_redirects=True)final_url = response.urlreturn self.extract_bvid(final_url)except:passraise ValueError("无法从链接中提取有效的 BV 号或 AV 号")def get_page_limit(self):"""获取用户想要爬取的页数限制"""while True:print("\n请选择爬取页数:")print("1. 爬取全部评论")print("2. 指定爬取页数")print("3. 退出程序")choice = input("请输入选择 (1/2/3): ").strip()if choice == "1":return None # 无限制elif choice == "2":try:pages = int(input("请输入要爬取的最大页数: ").strip())if pages > 0:return pageselse:print("页数必须大于0,请重新输入")except ValueError:print("请输入有效的数字")elif choice == "3":print("程序已退出")exit()else:print("无效选择,请重新输入")def run(self):try:# 获取Cookieself.headers["Cookie"] = self.get_cookie()# 输入视频链接video_url = input("请输入视频链接: ")# 获取页数限制max_pages = self.get_page_limit()# 提取 BV 号bvid = self.extract_bvid(video_url)print(f"成功提取视频 ID: {bvid}")# 构造标准视频链接(可选)standard_url = f"https://www.bilibili.com/video/{bvid}"print(f"标准视频链接: {standard_url}")# 基础参数配置base_params = {'oid': bvid,'type': '1','mode': '3','plat': '1','seek_rpid': '','web_location': '1315875'}# 初始化分页参数next_offset = ""page = 1total_comments = 0all_comments = []output_file = 'B站视频评论数据.csv'# 如果是第一页,创建新文件(覆盖旧文件)if page == 1:if os.path.exists(output_file):os.remove(output_file)while True:# 检查是否达到页数限制if max_pages and page > max_pages:print(f"已达到指定的最大页数 {max_pages},停止爬取")break# 构建分页参数pagination_str = json.dumps({"offset": next_offset}, separators=(',', ':'))params = base_params.copy()params['pagination_str'] = pagination_str# 生成动态参数wts = int(time.time())w_rid = self.get_w_rid(params, wts)params['w_rid'] = w_ridparams['wts'] = wtsprint(f"正在请求第 {page} 页评论数据...")json_data = self.fetch_comments(self.base_url, self.headers, params)print(f"正在解析第 {page} 页评论数据...")comments, next_offset = self.parse_comments(json_data, bvid, '1')if not comments:print(f"第 {page} 页没有评论数据,停止爬取")break# 收集所有评论all_comments.extend(comments)total_comments += len(comments)print(f"第 {page} 页爬取成功,获取 {len(comments)} 条评论")# 保存当前页数据(追加模式)self.save_to_csv(comments, output_file, mode='a')# 检查是否还有下一页if next_offset is None:print("已到达最后一页,停止爬取")breakpage += 1time.sleep(1)print(f"爬取完成!共获取 {total_comments} 条评论")except Exception as e:print(f"发生错误:{e}")import tracebacktraceback.print_exc()if __name__ == "__main__":crawler = BilibiliCommentCrawler()crawler.run()
3. src/build.py
import PyInstaller.__main__
import os
import json# 获取当前脚本目录
script_dir = os.path.dirname(os.path.abspath(__file__))# 资源文件路径
config_path = os.path.join(script_dir, "..", "resources", "b_crawler_config.json")
chromedriver_path = os.path.join(script_dir, "..", "resources", "chromedriver.exe")# 确保配置文件存在
if not os.path.exists(config_path):with open(config_path, 'w') as f:json.dump({}, f)# 打包命令
PyInstaller.__main__.run(['b_comments.py','--onefile','--console','--name=B站评论爬虫','--add-data', f'{config_path};resources','--add-data', f'{chromedriver_path};resources','--clean'
])
4.resources/chromedriver.exe
下载Good Chrome驱动查询
-https://googlechromelabs.github.io/chrome-for-testing/
找到win32版本下载,解压后把chromedriver.exe复制到当前目录下
5.README.md
# B站评论爬虫项目这是一个用于爬取B站视频评论的工具,支持:
- 自动获取Cookie- 爬取一级和二级评论- 保存为CSV文件- 打包为可执行程序## 使用说明### 首次运行
1. 双击运行 `B站评论爬虫.exe`2. 程序将引导您获取B站Cookie3. 输入视频链接开始爬取### 后续运行
1. 程序会检测已保存的Cookie2. 可选择使用现有Cookie或更新Cookie3. 输入视频链接开始爬取### 打包说明
1. 安装依赖:`pip install pyinstaller selenium requests`2. 将chromedriver.exe放在resources目录3. 运行打包脚本:`python build.py`4. 生成的可执行文件在dist目录## 注意事项
1. 确保安装了与chromedriver匹配的Chrome浏览器2. 首次运行需要登录获取Cookie3. 爬取大量数据时可能需要较长时间
6. 运行build.py
进入build.py所在文件夹,在索引栏输入cmd,进入终端,命令行运行
python build.py
七、总结
1. 项目概述
- 实现了对B站视频评论数据的自动化爬取与分析
- 支持一级评论和二级评论的完整数据获取
- 采用模块化设计,具备良好的可维护性和扩展性
2. 核心技术要点
网络请求与反爬虫处理
- 使用
requests
库发送HTTP请求获取数据 - 通过设置合理的请求头(Cookie、User-Agent、Referer)绕过基础反爬虫机制
- 实现了WBI签名参数(w_rid)的动态计算,这是B站反爬虫的关键点
数据解析与处理
- 对JSON格式响应数据进行深度解析
- 提取用户信息、评论内容、时间、点赞数等关键字段
- 实现时间戳到可读时间的转换
翻页与深度爬取
- 分析并实现分页参数的构造逻辑
- 支持无限滚动加载的评论数据获取
- 特别处理了二级评论的独立请求机制
自动化与用户体验
- 利用
selenium
实现Cookie的自动获取,提升用户使用体验 - 设计了完整的项目打包方案,生成独立的可执行文件
- 提供友好的命令行交互界面
3. 项目亮点
技术难点攻克
- 成功逆向分析B站WBI签名算法
- 实现了完整的评论层级结构爬取(一级+二级评论)
- 解决了动态参数构造问题
工程化实践
- 采用模块化项目结构设计
- 实现了完整的打包和部署方案
- 编写了详细的使用说明文档
4. 应用价值
- 可用于舆情分析、用户行为研究等场景
- 为社交媒体数据挖掘提供实践案例
- 展示了完整的网络爬虫开发流程和技术栈应用
5. 注意事项
- 需要遵守网站的robots协议和使用条款
- 应控制请求频率,避免对服务器造成过大压力
- Cookie等认证信息具有时效性,需要定期更新
该项目完整地展示了从需求分析、技术调研、代码实现到产品打包的全流程,是一个具有实际应用价值的数据爬取解决方案。
项目运行结果演示
n build.py`
4. 生成的可执行文件在dist目录
## 注意事项
1. 确保安装了与chromedriver匹配的Chrome浏览器2. 首次运行需要登录获取Cookie3. 爬取大量数据时可能需要较长时间
6. 运行build.py
进入build.py所在文件夹,在索引栏输入cmd,进入终端,命令行运行
python build.py
七、总结
1. 项目概述
- 实现了对B站视频评论数据的自动化爬取与分析
- 支持一级评论和二级评论的完整数据获取
- 采用模块化设计,具备良好的可维护性和扩展性
2. 核心技术要点
网络请求与反爬虫处理
- 使用
requests
库发送HTTP请求获取数据 - 通过设置合理的请求头(Cookie、User-Agent、Referer)绕过基础反爬虫机制
- 实现了WBI签名参数(w_rid)的动态计算,这是B站反爬虫的关键点
数据解析与处理
- 对JSON格式响应数据进行深度解析
- 提取用户信息、评论内容、时间、点赞数等关键字段
- 实现时间戳到可读时间的转换
翻页与深度爬取
- 分析并实现分页参数的构造逻辑
- 支持无限滚动加载的评论数据获取
- 特别处理了二级评论的独立请求机制
自动化与用户体验
- 利用
selenium
实现Cookie的自动获取,提升用户使用体验 - 设计了完整的项目打包方案,生成独立的可执行文件
- 提供友好的命令行交互界面
3. 项目亮点
技术难点攻克
- 成功逆向分析B站WBI签名算法
- 实现了完整的评论层级结构爬取(一级+二级评论)
- 解决了动态参数构造问题
工程化实践
- 采用模块化项目结构设计
- 实现了完整的打包和部署方案
- 编写了详细的使用说明文档
4. 应用价值
- 可用于舆情分析、用户行为研究等场景
- 为社交媒体数据挖掘提供实践案例
- 展示了完整的网络爬虫开发流程和技术栈应用
5. 注意事项
- 需要遵守网站的robots协议和使用条款
- 应控制请求频率,避免对服务器造成过大压力
- Cookie等认证信息具有时效性,需要定期更新
该项目完整地展示了从需求分析、技术调研、代码实现到产品打包的全流程,是一个具有实际应用价值的数据爬取解决方案。
项目运行结果演示