爬取B站视频评论数据爬取与分析

如果只要单纯的脚本可以直接看项目结构里的b_comments.py

一、技术架构

1、环境配置

  • Python 3.8
  • PyCharm

2、模块配置

  • requests:用于发送HTTP请求
  • time:用于处理时间相关的操作
  • csv:用于读写CSV文件
  • json:用于处理JSON数据
  • hashlib:用于生成哈希值
  • urllib.parse:用于URL解析和编码
    • quote:URL编码
    • urlparse:解析URL
    • parse_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等认证信息具有时效性,需要定期更新

该项目完整地展示了从需求分析、技术调研、代码实现到产品打包的全流程,是一个具有实际应用价值的数据爬取解决方案。

项目运行结果演示
在这里插入图片描述

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/90728.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/90728.shtml
英文地址,请注明出处:http://en.pswp.cn/web/90728.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

OpenAI最新大模型GPT-4o体验之Code Copilot AI编程大模型

一、前言GPT-4o("o"代表"全能")具备处理各种文本、声音和图像资料的能力,能够输出多种格式的文本、声音和图像。GPT-4o 的推出标志着 AI 技术的重大突破。它不再局限于单一媒介,而是首次实现了文本、语音和图…

社交电商推客系统全栈开发指南:SpringCloud+分润算法+Flutter跨端

一、推客系统概述与市场背景推客系统(TuiKe System)是一种基于社交关系的营销推广平台,通过用户分享商品或服务链接,实现裂变式传播和精准营销。近年来,随着社交电商的蓬勃发展,推客系统已成为企业获客的重…

网安-中间件-Redis未授权访问漏洞

目录 Redis Redis持久化 动态修改配置 使用反弹连接的情况 常见监听端口的方式 常见建立反弹连接的方式 流程 Linux crontab cron文件存储路径 利用Redis实现攻击 1.webshell提权案例 2.定时任务shell反弹案例 3.SSH Key getshell案例 ​编辑Redis其他利用方式 …

【c++深入系列】:万字详解栈和队列和deque(附模拟实现的源码)

🔥 本文专栏:c 🌸作者主页:努力努力再努力wz 💪 今日博客励志语录: 石头能被水滴穿,不是因为水有多强,而是因为它从未停过。 ★★★ 本文前置知识: 模版 栈 那么栈这个…

速通python加密之RSA加密

RSA加密 RSA加密是一种非对称加密算法(与AES等对称加密不同),由罗纳德李维斯特(Ron Rivest)、阿迪萨莫尔(Adi Shamir)和伦纳德阿德曼(Leonard Adleman)于1977年提出&…

Java BeanUtils 类详解:作用、语法与示例

一、BeanUtils 的核心作用BeanUtils 是 Apache Commons 和 Spring Framework 提供的工具类,主要用于简化 JavaBean 的操作。核心功能包括:属性拷贝:对象间同名属性自动复制动态访问:通过字符串名称操作属性类型转换:自…

PyCharm高效开发全攻略

安装与基础配置下载PyCharm专业版或社区版(免费)并完成安装。首次启动时选择默认设置或自定义主题、字体大小等界面偏好。配置Python解释器路径(推荐使用虚拟环境),确保项目依赖隔离。快捷键与导航熟悉核心快捷键能大幅…

Pycharm 给 python 程序打包EXE的配置和方法

前言: Python 语言的设计变得越来越简单,它有很多可以使用的库,所以尤其在人工智能时代,Python语言被广泛应用。但是Python语言和windows系统的兼容性稍微偏弱,如何生成windows可以执行的exe文件。是要一个很复杂的配置过程,本文就会做一个介绍。 本文,通过一个Python…

【Linux | 网络】传输层(UDP和TCP)

目录一、再谈端口号1.1 端口号1.2 端口号的范围划分1.3 常见知名端口号1.4 netstat 命令1.5 进程与端口号的关系1.6 pidof 命令二、UDP协议2.1 UDP协议段格式2.2 如何理解UDP报头和UDP报文2.2.1 UDP报头2.2.2 UDP报文和UDP报文的管理2.2.3 UDP封装过程2.3 UDP的特点2.4 UDP的缓…

mybatisX的自定义模板生成

在idea中使用mybtais的自定义模板生成,可以帮我们省去很多重复的代码。 打开一个项目,我们要修改的主要就两个文件,一个是生成的mapper接口,另一个是xml文件: 相应的mapper接口模板为: package ${mapper…

miniz:一个轻量级、高性能的开源压缩库

目录 1.简介 2.核心特性 3.基本使用示例 4.与 ZLIB 的对比 5.使用场景 6.注意事项 1.简介 miniz 是一个轻量级、高性能的开源压缩库,专注于提供 ZLIB/GZIP 兼容的压缩和解压缩功能。它的核心优势在于体积小巧(单文件实现)、跨平台支持和…

Jenkins接口自动化测试(构建)平台搭建

Python接口自动化测试零基础入门到精通(2025最新版)自动化测试流程 在进行平台搭建前,我们首先要问自己:我需要搭建的平台的功能是什么,要实现什么目标? 在我的理解中,自动化构建平台的执行流…

Day 22: 复习

机器学习数据处理与降维技术复习总结 前言 经过6天的学习,我们系统地学习了从基础的Numpy数组操作到高级的降维算法,这些内容构成了机器学习数据预处理的重要知识体系。本文将对这一系列学习内容进行全面复习和总结,帮助大家建立完整的知识…

力扣 hot100 Day56

46. 全排列 给定一个不含重复数字的数组 nums &#xff0c;返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。 //抄的 class Solution { private:vector<vector<int>>result;vector<int> path; public:void backtracking(vector<int>& nu…

Android 编码规范全指南

在 Android 开发领域&#xff0c;代码不仅是功能实现的载体&#xff0c;更是团队协作与项目迭代的基础。一套完善的编码规范&#xff0c;能让代码从 “可运行” 升级为 “易维护、可扩展、低风险”。本文基于 Google、Square 等顶尖团队的实践经验&#xff0c;结合国内 Android…

[RPA] Excel中的字典处理

案例1一个Excel文件总共有2个Sheet页&#xff0c;分别为总表和对照表通过对照表sheet页&#xff0c;设置价格对照字典对照表循环总表sheet页&#xff0c;根据循环到的商品名称&#xff0c;找到对应字典中的价格&#xff0c;并计算出总价总表将总价写入到Excel表中C列&#xff0…

基于NSGAII优化算法的车间生产调度matlab仿真

目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.部分程序 4.算法理论概述 5.参考文献 6.完整程序 1.程序功能描述 车间生产调度是制造业的核心环节&#xff0c;其目标是在满足设备约束、工序优先级等条件下&#xff0c;优化多个相互冲突的生产指标&#xff08;如…

Cmake、VS2019、C++、openGLopenCV环境安装

在 CMake 和 Visual Studio 2019 环境下安装和配置 OpenGL、OpenCV 以及 CUDA 可能会有些复杂&#xff0c;因为涉及的组件多且相互依赖。以下是一个详细的指南&#xff0c;帮助您逐步完成安装和配置。 1. 前提条件 在开始之前&#xff0c;请确保您已安装以下软件&#xff1a; …

视频二维码在产品设备说明书中的应用

在当今数字化的时代&#xff0c;传统的产品设备说明书正面临着一场变革。文字和图片虽然能提供基本信息&#xff0c;但在复杂设备的安装、操作和故障排除方面&#xff0c;往往显得力不从心。而视频二维码的出现&#xff0c;为这一困境提供了完美的解决方案&#xff0c;它将冰冷…

【Pytest 使用教程】

pytest 使用 test_basic.py Pytest 完全实战手册 一、核心概念与基础 1、在pytest框架下运行测试用例&#xff0c;最基础的一共有三点。导入pytest的包写一个方法&#xff0c;或者类。后面运行的时候&#xff0c;相当于运行这个方法&#xff0c;或者类里的方法&#xff0c;无需…