ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

B站视频元数据采集系统:Selenium结构化爬虫方案

2026/9/20 10:28:20 拓冰建站 浏览量
B站视频元数据采集系统:Selenium结构化爬虫方案 简介这是一套面向Python数据采集初学者与中级开发者的Bilibili视频数据精准爬取工具包解决B站公开视频元数据批量获取难、字段不全、反爬易失效等实际问题。资源包含6个核心文件主爬虫脚本scraper.py实现登录绕过与API接口调用output-sample.xlsx提供结构化示例数据idlist-sample.txt用于输入目标视频ID列表README.md详述运行逻辑与参数配置LICENSE明确开源协议output-sample.png直观展示数据字段覆盖效果。压缩包仅468KB轻量易部署适配本地快速验证与二次开发。已有1080人学习下载使用者可直接获得含标题、UP主ID、精确播放量、弹幕总数、点赞/硬币/收藏/转发数、发布时间、视频时长及简介标签等13维度的完整结构化数据同时掌握B站动态接口分析、请求头模拟、数据清洗与Excel导出全流程实践方案。1. 这不是“一键下载视频”的工具而是一套面向B站公开数据的结构化采集系统你手里的这个Bilibili视频数据爬虫.zip本质是一套针对B站PC端Web页面设计的、可复现的公开元数据采集方案——它不抓取视频文件本身也不绕过任何登录或权限校验而是精准提取每个视频页如https://www.bilibili.com/video/BV1xx411x7xx在未登录状态下即可渲染的完整字段标题、UP主昵称与UID、精确播放量非估算、历史累计弹幕数、点赞/硬币/收藏/转发四维互动指标、发布时间ISO8601格式、时长秒级整数、视频简介、UP主个人简介、以及全部标签含多级标签结构。所有字段均经实测验证可稳定提取输出为.xlsx表格与.txtID列表双格式。它适用于内容分析、UP主画像建模、平台生态研究等合规场景要求使用者具备基础Python环境管理能力且必须理解B站反爬策略的边界该脚本依赖真实浏览器渲染后的HTML结构因此需配合SeleniumChromeDriver模拟用户行为而非纯Requests静态请求。如果你的目标是批量下载MP4或提取音频流请转向FFmpeg或专用下载器但若你需要构建一个可审计、可溯源、字段完整的B站视频元数据库这套代码就是起点。2. 为什么选择Selenium而非Requests解析B站动态渲染与反爬机制的底层逻辑2.1 B站前端数据加载模式决定技术选型B站PC端视频页的核心数据尤其是播放量、弹幕数、互动数并非直接写入初始HTML而是通过JavaScript异步请求/x/web-interface/archive/stat等接口获取并由Vue组件动态注入DOM。这意味着纯requests.get()返回的HTML中span classview等节点内为空或占位符BeautifulSoup解析原始HTML无法提取有效数值即使构造合法Headers并携带Cookierequests仍会遭遇412 Precondition Failed或403 Forbidden——B站服务端会对User-Agent、Referer、Accept-Encoding组合做指纹校验且对无JavaScript执行痕迹的请求主动降权。提示scraper.py中明确禁用requests直连方案强制启用Selenium正是基于此架构事实。这不是过度设计而是绕过前端渲染屏障的必要代价。2.2 Selenium配置要点与ChromeDriver版本适配脚本依赖chromedriver驱动Chrome浏览器完成页面加载与DOM解析。关键配置位于scraper.py第32–45行options webdriver.ChromeOptions() options.add_argument(--headless) # 后台运行不弹窗 options.add_argument(--no-sandbox) options.add_argument(--disable-dev-shm-usage) options.add_argument(--disable-gpu) options.add_argument(--user-agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36) options.add_experimental_option(excludeSwitches, [enable-logging]) driver webdriver.Chrome(optionsoptions)--headless确保服务器环境可部署但需注意B站部分新版页面在headless模式下可能触发额外检测--user-agent必须与真实Chrome 120版本匹配否则document.documentElement.outerHTML中关键class名缺失--no-sandbox和--disable-dev-shm-usage是Linux服务器常见必需参数避免权限与共享内存错误。注意ChromeDriver版本必须与本地Chrome浏览器主版本号严格一致如Chrome 120.x → chromedriver 120.x。若出现session not created: This version of ChromeDriver only supports Chrome version xx错误请访问https://chromedriver.chromium.org/下载对应版本解压后替换./chromedriver文件。2.3 页面等待策略显式等待替代time.sleep()脚本采用WebDriverWait配合expected_conditions实现精准等待避免因网络抖动导致元素未加载即解析wait WebDriverWait(driver, 15) # 最长等待15秒 wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, span.view))) wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, span.dm)))presence_of_element_located检测元素是否存在于DOM而非是否可见span.view对应播放量容器span.dm对应弹幕数容器二者是B站视频页最稳定的锚点若超时未出现抛出TimeoutException并记录URL到failed_urls.txt便于后续人工核查。对比time.sleep(3)硬等待前者在元素就绪后立即执行后者无论页面快慢均阻塞固定时长既降低效率又增加被风控概率。3. 字段提取逻辑详解从HTML结构到结构化数据的映射规则3.1 核心字段XPath定位表与容错处理字段名定位方式XPath表达式容错说明标题h1 classvideo-title文本//h1[classvideo-title]/text()若页面改版备用路径//meta[propertyog:title]/contentUP主昵称a classname href//space.bilibili.com/xxx文本//a[classname]/text()需strip()去空格UP主改名时仍可捕获最新值UP主UIDa classname链接中的数字//a[classname]/href→ 正则/space\.bilibili\.com/(\d)比直接读>def parse_play_count(text): 将123.4万、5678.9亿等格式转为int text text.strip() if not text or text -: return 0 if 万 in text: return int(float(text.replace(万, ).strip()) * 10000) elif 亿 in text: return int(float(text.replace(亿, ).strip()) * 100000000) else: return int(text.replace(,, ).strip())text.replace(万, )移除单位字符float()兼容小数点如“123.4万”* 10000实现万→个的换算* 100000000实现亿→个text.replace(,, )清除千分位逗号部分页面显示“1,234,567”return 0对空值或“-”统一归零避免Excel写入报错。同理parse_duration(text)函数def parse_duration(text): 将MM:SS或H:MM:SS转为总秒数 parts text.strip().split(:) if len(parts) 2: # MM:SS return int(parts[0]) * 60 int(parts[1]) elif len(parts) 3: # H:MM:SS return int(parts[0]) * 3600 int(parts[1]) * 60 int(parts[2]) else: return 0split(:)按冒号分割len(parts)判断时间格式int(parts[0]) * 3600将小时转秒int(parts[1]) * 60将分钟转秒返回0作为兜底值防止异常格式中断流程。3.3 数据存储逻辑XLSX格式与字段顺序控制输出使用openpyxl库写入Excel而非pandas.to_excel()原因在于openpyxl支持追加写入append()避免每次写入重载整个工作簿可精确控制列宽与单元格格式如播放量列设为数字格式发布时间列设为日期格式字段顺序严格按headers [BV号, 标题, UP主, UP主UID, ...]定义确保.xlsx列序与README.md文档一致。关键代码段scraper.py第187–195行wb Workbook() ws wb.active ws.append(headers) # 写入表头 for row in data_rows: ws.append(row) # 设置播放量列为数字格式 for col in [C, D, E, F, G, H, I, J]: # C列播放量D列弹幕数... for cell in ws[col]: cell.number_format #,##0 wb.save(output-sample.xlsx)ws.append(headers)一次性写入表头ws.append(row)逐行追加数据col in [C,D,...]对应Excel列字母cell.number_format #,##0启用千分位分隔若需导出CSV只需将wb.save()替换为csv.writer循环写入但XLSX对中文兼容性更优。4. 实战部署从ID列表生成到失败重试的全流程操作指南4.1 准备ID列表idlist-sample.txt的生成与格式规范脚本默认读取idlist-sample.txt作为待爬BV号源。该文件必须为纯文本每行一个BV号无空行、无前缀、无引号BV1xx411x7xx BV1ab411c7xx BV1cd411e7xx不可写成https://www.bilibili.com/video/BV1xx411x7xxURL会被截断不可写成BV1xx411x7xx引号导致strip()后仍残留若需从B站搜索页批量提取BV号可用浏览器控制台执行以下JS仅限学习用途Array.from(document.querySelectorAll(a[href^/video/BV])).map(el el.href.match(/\/video\/(BV\w)/)[1]).join(\n)复制结果粘贴至idlist-sample.txt保存为UTF-8编码。4.2 执行爬虫命令行参数与速率控制进入项目根目录执行python scraper.py --input idlist-sample.txt --output output-sample.xlsx --delay 2.5 --timeout 15--input指定ID列表路径默认idlist-sample.txt--output指定输出Excel路径默认output-sample.xlsx--delay页面间随机延迟秒范围[delay*0.8, delay*1.2]防触发QPS限制--timeout单页面最大等待时间秒超时则跳过并记入failed_urls.txt。提示--delay 2.5是平衡效率与稳定性推荐值。低于1.5秒易被B站识别为机器流量高于5秒则单日采集量骤减。若需加速建议改用分布式部署见4.4节而非单纯降低delay。4.3 失败重试机制与日志分析脚本自动记录两类日志failed_urls.txt包含超时或解析失败的完整URL如https://www.bilibili.com/video/BV1xx411x7xx格式为每行一个URLerror_log.txt记录异常类型与Traceback如NoSuchElementException、TimeoutException。重试步骤检查failed_urls.txt中URL是否仍可正常访问浏览器打开确认若页面存在手动执行单URL调试python scraper.py --single https://www.bilibili.com/video/BV1xx411x7xx --debug--debug参数启用driver.save_screenshot(debug.png)保存当前页面截图供分析若截图显示验证码或“请稍后再试”说明IP已被临时限制需更换代理或等待通常2小时后恢复将failed_urls.txt中成功重试的URL移除剩余URL再次传入--input参数重跑。4.4 分布式扩展基于Redis队列的多进程采集框架当ID列表超过1000条时单机Selenium效率瓶颈明显。可改造为Redis多进程架构安装依赖pip install redis启动Redis服务默认localhost:6379修改scraper.py将idlist-sample.txt读取逻辑替换为Redis List弹出import redis r redis.Redis(hostlocalhost, port6379, db0) while True: bv r.lpop(bv_queue) if not bv: break crawl_single_video(bv.decode(utf-8))预加载ID队列cat idlist-sample.txt | redis-cli -x lpush bv_queue启动4个进程并发采集python scraper.py --mode distributed python scraper.py --mode distributed python scraper.py --mode distributed python scraper.py --mode distributed Redis保证ID不重复消费lpop原子性操作避免竞态每个进程独立Chrome实例CPU核心数即最大并发数输出仍为单个output-sample.xlsx需在写入前加文件锁threading.Lock。5. 进阶技巧字段补全、增量采集与数据质量验证5.1 补充UP主粉丝数与投稿数调用B站API的合规方式scraper.py当前未采集UP主粉丝数follower与总投稿数archive_count因其需调用https://api.bilibili.com/x/space/acc/info?midxxx接口且该接口有频率限制。若需补充可在get_up_info()函数中添加def get_up_follower(mid): url fhttps://api.bilibili.com/x/space/acc/info?mid{mid} headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } try: resp requests.get(url, headersheaders, timeout10) data resp.json() if data[code] 0: return data[data][follower], data[data][archive_count] except Exception as e: print(fUP info API failed for mid {mid}: {e}) return 0, 0mid即UP主UID从//a[classname]/href正则提取data[code] 0判断API成功data[data]含完整信息必须添加time.sleep(1)间隔否则连续请求触发429 Too Many Requests。5.2 增量采集基于最后修改时间的智能跳过为避免重复采集已存在的BV号可建立SQLite数据库记录采集时间戳conn sqlite3.connect(crawl_history.db) conn.execute(CREATE TABLE IF NOT EXISTS history ( bv TEXT PRIMARY KEY, last_crawl TIMESTAMP DEFAULT CURRENT_TIMESTAMP ))在crawl_single_video()开头添加cursor conn.cursor() cursor.execute(SELECT last_crawl FROM history WHERE bv ?, (bv,)) if cursor.fetchone(): print(fSkip {bv}: already crawled) return # ... 执行采集逻辑 ... cursor.execute(INSERT INTO history (bv) VALUES (?), (bv,)) conn.commit()PRIMARY KEY确保BV号唯一INSERT自动忽略重复last_crawl字段可用于统计采集频次如“近7天新增视频数”。5.3 数据质量验证三类必检指标与自动化脚本采集完成后运行validate_output.py需自行编写检查数据完整性import pandas as pd df pd.read_excel(output-sample.xlsx) # 1. 检查空值率 null_rate df.isnull().mean() print(Null rate per column:) print(null_rate[null_rate 0]) # 2. 检查播放量异常值10亿视为异常 abnormal_play df[df[播放量] 1000000000] print(fAbnormal play count: {len(abnormal_play)} rows) # 3. 检查发布时间是否全为过去时间 from datetime import datetime now datetime.now() future_dates df[pd.to_datetime(df[发布时间]) now] print(fFuture timestamps: {len(future_dates)} rows)null_rate输出各列空值比例若UP主简介列空值率30%说明页面结构变动abnormal_play捕获可能的单位解析错误如“12.3亿”误为123future_timestamps发现服务器时间不同步或页面伪造时间。提示将上述验证逻辑封装为--validate命令行参数集成进scraper.py可实现采集后自动质检。本文还有配套的精品资源点击获取