1. 项目概述与核心价值
最近在折腾一个挺有意思的小项目,起因是我关注了上百个B站UP主,但发现一个挺头疼的问题:我经常错过一些我真正想看的UP主的新视频。B站APP的推荐流信息太杂,而手动去每个UP主主页翻看又太费时间。就在我琢磨着有没有什么自动化工具能解决这个痛点时,我在GitHub上发现了Artistkisa/bilibili-up-update-tracker这个项目。简单来说,这是一个专门用来追踪B站UP主更新动态的工具,它能够定期检查你指定的UP主是否发布了新视频,并通过你设定的方式(比如邮件、Telegram机器人等)及时通知你。
这个工具的核心价值,在于它解决了信息过载时代下的“精准订阅”问题。对于像我这样的深度B站用户,或者内容创作者、市场分析人员来说,及时获取特定领域UP主的更新动态至关重要。无论是为了学习、竞品分析还是纯粹的娱乐追更,手动追踪的效率都太低下了。这个项目用技术手段实现了自动化监控,把我们从重复的“刷新-查看”劳动中解放出来,让我们能把精力集中在内容消费本身。它就像一个为你定制的、永不疲倦的“更新雷达”,只关注你关心的频道,过滤掉所有无关的噪音。
2. 项目架构与核心组件解析
2.1 整体设计思路
bilibili-up-update-tracker的设计思路非常清晰,遵循了经典的“数据获取-处理-通知”流水线模式。整个项目可以看作一个轻量级的、专为B站场景设计的爬虫与消息推送系统的结合体。它的目标不是做一个功能庞杂的B站客户端,而是聚焦于“更新追踪”这一个核心功能,并将其做到极致。这种单一职责的设计,使得项目结构清晰,易于理解和二次开发。
项目的运行逻辑可以拆解为以下几个核心环节:
- 目标管理:维护一个需要追踪的UP主列表(通常以B站UID形式存储)。
- 数据抓取:定期(例如每10分钟、每小时)访问B站API或网页,获取列表中每个UP主的最新视频信息。
- 状态比对:将本次抓取到的最新视频信息(如视频BVID、发布时间)与本地存储的上一次记录进行比对。
- 更新判断:如果发现新的视频记录(即本次抓取的“最新视频”与本地记录的“最新视频”不一致),则判定为有更新。
- 消息生成与推送:根据更新内容,生成包含视频标题、链接、封面等信息的通知消息,并通过配置好的渠道(如邮件、Webhook)发送出去。
- 状态更新:将本地存储的UP主最新视频信息更新为刚发现的新视频,为下一次比对做准备。
这个闭环流程确保了追踪的持续性和准确性。整个系统的健壮性依赖于B站数据接口的稳定性、比对逻辑的准确性以及通知渠道的可靠性。
2.2 核心组件与技术栈
要实现上述流程,项目依赖几个关键的技术组件:
网络请求与数据解析:这是项目的基石。通常使用像
requests、aiohttp这样的HTTP库来模拟请求,获取B站的数据。数据源可能是公开的API(如space.bilibili.com/ajax/member/getSubmitVideos)或通过解析UP主主页HTML。解析环节则会用到json模块处理API返回的数据,或者用BeautifulSoup、lxml等库解析HTML。这里有一个关键点:需要处理好B站的反爬机制,比如请求头(User-Agent, Referer)的模拟、Cookies的管理(对于某些需要登录态的信息),以及请求频率的控制,避免对B站服务器造成压力或被封禁IP。数据存储:需要一个轻量级的方式来持久化存储每个UP主“上一次检查到的最新视频”状态。简单的实现可以用文件(如JSON、SQLite数据库)来存储。例如,一个
up_status.json文件,记录着{“UID_123456”: {“latest_bvid”: “BV1xx”, “latest_pubdate”: 1625097600}}。更复杂的版本可能会用到SQLite甚至Redis,以支持更快的查询和更复杂的状态管理(比如追踪历史更新记录)。定时任务调度:追踪需要定期执行。最直接的方式是使用操作系统的定时任务(如Linux的cron,Windows的任务计划程序)来周期性地运行脚本。在Python项目中,更优雅的方式是使用
schedule或apscheduler这样的库,在脚本内部实现定时循环,使得整个项目可以作为一个常驻服务运行。消息通知模块:这是与用户交互的出口。项目的实用性很大程度上取决于通知的及时性和便利性。常见的实现包括:
- 邮件通知 (SMTP):通过
smtplib库,配置发件邮箱(如QQ邮箱、163邮箱或企业邮箱)、SMTP服务器和授权码,将更新信息以邮件形式发出。这是最通用、门槛较低的方式。 - Telegram Bot:利用Telegram Bot API,用户与Bot互动添加订阅,Bot主动推送更新。这种方式即时性强,交互性好,适合移动端用户。
- Server酱、PushPlus等第三方推送服务:调用这些服务提供的API,可以将消息推送到微信、钉钉等平台。
- Webhook:将更新信息以结构化数据(JSON)的形式POST到一个预设的URL。这种方式非常灵活,可以对接自己的服务器、自动化工具(如Zapier、IFTTT)或即时通讯软件的自建机器人。
- 邮件通知 (SMTP):通过
配置管理:一个良好的项目应该将所有可配置项(如UP主UID列表、检查间隔、通知渠道的密钥/Token、发件邮箱信息等)外置,通常通过一个配置文件(如
config.yaml或config.ini)或环境变量来管理。这样无需修改代码即可调整项目行为,也便于在不同环境部署。
3. 从零开始实现一个简易追踪器
理解了核心思路后,我们可以动手实现一个基础版本。这个版本将使用Python,实现通过B站API获取数据、用JSON文件存储状态、并通过邮件发送通知的功能。
3.1 环境准备与依赖安装
首先,确保你的系统安装了Python 3.7或以上版本。然后创建一个新的项目目录,并初始化虚拟环境(推荐,以隔离依赖):
mkdir bilibili-tracker && cd bilibili-tracker python -m venv venv # Windows: venv\Scripts\activate # Linux/Mac: source venv/bin/activate安装必要的Python库:
pip install requests schedule这里我们选择requests用于网络请求,schedule用于实现简单的定时任务。对于生产环境,apscheduler是更强大的选择。
3.2 核心代码实现
接下来,我们创建几个核心文件。
1. 配置文件 (config.yaml)
# 需要追踪的UP主列表,值为B站UID up_list: - 546195 # 老番茄 - 777536 # 某幻君 - 1580990 # 罗翔说刑法 # 检查间隔(秒) check_interval: 600 # 每10分钟检查一次 # 邮件通知配置 email: enabled: true smtp_server: smtp.qq.com # 以QQ邮箱为例 smtp_port: 587 sender_email: your_email@qq.com sender_password: your_authorization_code # 注意:是授权码,不是邮箱密码 receiver_email: receiver@example.com重要提示:邮箱密码处请务必使用“授权码”。以QQ邮箱为例,需要在设置-账户中开启SMTP服务并生成专属授权码。直接使用登录密码通常会失败。
2. 状态存储与加载模块 (storage.py)
import json import os STATUS_FILE = 'up_status.json' def load_status(): """加载UP主状态""" if not os.path.exists(STATUS_FILE): return {} try: with open(STATUS_FILE, 'r', encoding='utf-8') as f: return json.load(f) except (json.JSONDecodeError, IOError): return {} def save_status(status): """保存UP主状态""" with open(STATUS_FILE, 'w', encoding='utf-8') as f: json.dump(status, f, ensure_ascii=False, indent=2) def update_up_status(uid, latest_bvid, latest_title, latest_pubdate): """更新单个UP主的状态""" status = load_status() status[str(uid)] = { 'latest_bvid': latest_bvid, 'latest_title': latest_title, 'latest_pubdate': latest_pubdate } save_status(status)3. B站数据获取模块 (bilibili_client.py)
import requests import time def get_up_latest_video(uid): """ 通过B站API获取UP主最新视频信息 参考API: https://api.bilibili.com/x/space/arc/search?mid={uid}&ps=1 """ url = f'https://api.bilibili.com/x/space/wbi/arc/search' # 注意:B站API时有更新,参数可能变化。当前需要mid(用户ID)和ps(每页数量) params = { 'mid': uid, 'ps': 1, # 只取最新一条 'pn': 1, # 第一页 'order': 'pubdate' # 按发布时间排序 } headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'Referer': f'https://space.bilibili.com/{uid}' } try: response = requests.get(url, params=params, headers=headers, timeout=10) response.raise_for_status() data = response.json() if data['code'] == 0 and data['data']['list']['vlist']: video = data['data']['list']['vlist'][0] return { 'bvid': video['bvid'], 'title': video['title'], 'pubdate': video['created'], # 时间戳 'author': video['author'] } else: print(f"获取UID {uid} 数据失败或暂无视频: {data.get('message')}") return None except requests.exceptions.RequestException as e: print(f"请求UID {uid} API时出错: {e}") return None except (KeyError, IndexError) as e: print(f"解析UID {uid} 返回数据时出错: {e}, 原始数据: {data}") return None4. 邮件通知模块 (notifier.py)
import smtplib from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart import yaml def load_config(): with open('config.yaml', 'r', encoding='utf-8') as f: return yaml.safe_load(f) def send_email_notification(up_name, video_info): """发送邮件通知""" config = load_config() email_cfg = config.get('email', {}) if not email_cfg.get('enabled', False): return sender = email_cfg['sender_email'] receiver = email_cfg['receiver_email'] password = email_cfg['sender_password'] subject = f'【B站追更】{up_name} 发布了新视频!' body = f""" <html> <body> <h2>{up_name} 更新啦!</h2> <p><strong>视频标题:</strong>{video_info['title']}</p> <p><strong>发布时间:</strong>{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(video_info['pubdate']))}</p> <p><strong>视频链接:</strong><a href="https://www.bilibili.com/video/{video_info['bvid']}">点击观看</a></p> <br> <p><small>此邮件由B站UP主更新追踪器自动发送。</small></p> </body> </html> """ msg = MIMEMultipart('alternative') msg['From'] = sender msg['To'] = receiver msg['Subject'] = subject msg.attach(MIMEText(body, 'html', 'utf-8')) try: server = smtplib.SMTP(email_cfg['smtp_server'], email_cfg['smtp_port']) server.starttls() # 启用TLS加密 server.login(sender, password) server.sendmail(sender, receiver, msg.as_string()) server.quit() print(f"邮件通知已发送给 {receiver}") except Exception as e: print(f"发送邮件失败: {e}")5. 主循环与调度 (main.py)
import schedule import time from datetime import datetime import yaml from bilibili_client import get_up_latest_video from storage import load_status, update_up_status from notifier import send_email_notification def check_updates(): """执行一次检查任务""" print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 开始检查更新...") with open('config.yaml', 'r', encoding='utf-8') as f: config = yaml.safe_load(f) up_list = config['up_list'] current_status = load_status() for uid in up_list: print(f" 检查 UID: {uid}") latest_video = get_up_latest_video(uid) if not latest_video: continue up_name = latest_video['author'] latest_bvid = latest_video['bvid'] # 检查是否有更新 if str(uid) in current_status: stored_bvid = current_status[str(uid)].get('latest_bvid') if stored_bvid == latest_bvid: print(f" {up_name} 暂无新视频") continue else: print(f" !! 发现更新: {up_name} - {latest_video['title']}") # 发送通知 send_email_notification(up_name, latest_video) else: # 首次运行,记录状态但不通知 print(f" 首次记录 {up_name} 的最新视频: {latest_video['title']}") # 更新状态 update_up_status(uid, latest_bvid, latest_video['title'], latest_video['pubdate']) print("本轮检查完成。\n") def main(): # 立即执行一次 check_updates() # 配置定时任务 interval = config.get('check_interval', 600) schedule.every(interval).seconds.do(check_updates) print(f"追踪器已启动,每 {interval} 秒检查一次。") while True: schedule.run_pending() time.sleep(1) if __name__ == '__main__': main()3.3 首次运行与测试
- 按照注释,填写
config.yaml中的邮箱配置(使用授权码)和你想追踪的UP主UID。 - 在项目根目录下,运行
python main.py。 - 程序会立即检查一次,并在控制台打印日志。如果发现新视频(或首次运行),会尝试发送邮件。
- 检查你的收件箱(包括垃圾邮件箱),查看是否收到通知。
至此,一个最基础的、功能完整的B站UP主更新追踪器就搭建完成了。它虽然简陋,但涵盖了核心逻辑。Artistkisa/bilibili-up-update-tracker项目的开源版本,其原理与此类似,但通常包含了更健壮的异常处理、更多的通知渠道(如Telegram Bot)、Web管理界面以及Docker部署支持。
4. 高级功能扩展与优化思路
基础版本跑通后,我们可以从稳定性、易用性和功能性上进行大量优化,这也是开源项目迭代的方向。
4.1 提升稳定性与健壮性
- 异常处理与重试机制:网络请求可能失败,API可能变更。需要在数据获取函数中加入更完善的异常捕获和重试逻辑。例如,使用
tenacity库为请求添加指数退避重试。from tenacity import retry, stop_after_attempt, wait_exponential @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10)) def get_up_latest_video_retry(uid): return get_up_latest_video(uid) - API失效应对:B站的API接口并非完全公开稳定,可能变更或增加鉴权。一个备选方案是使用“无头浏览器”如
playwright或selenium来模拟浏览器访问UP主主页,然后解析HTML。虽然更重,但通常更稳定。也可以准备多个API备用,一个失败则尝试另一个。 - 状态存储升级:将JSON文件换成SQLite数据库。可以轻松存储更长时间的历史记录,方便查询某个UP主过去一段时间的更新频率,甚至实现“标记已读”、“按标签分类UP主”等功能。
import sqlite3 conn = sqlite3.connect('tracker.db') c = conn.cursor() # 创建表 c.execute('''CREATE TABLE IF NOT EXISTS updates (uid TEXT, bvid TEXT, title TEXT, pubdate INTEGER, notified INTEGER DEFAULT 0)''') conn.commit()
4.2 丰富通知渠道与交互方式
集成Telegram Bot:
- 通过
@BotFather创建一个新的Telegram Bot,获取Token。 - 使用
python-telegram-bot库。用户可以向Bot发送/subscribe UID来订阅,Bot将UID存入数据库。 - 在
check_updates函数中,当发现更新时,除了发邮件,也遍历订阅了该UP主的用户,调用bot.send_message(chat_id=user_chat_id, text=update_msg)。 这种方式交互性极强,是很多开源项目的首选。
- 通过
支持Webhook:这提供了最大的灵活性。在配置文件中添加一个webhook URL。当有更新时,将视频信息组装成JSON格式,用
requests.post发送到该URL。这样,你可以用这个Webhook触发其他自动化流程,比如自动下载视频、转发到Discord/Slack、记录到Notion数据库等。def send_webhook_notification(webhook_url, video_info): payload = { "event": "bilibili_update", "data": video_info } requests.post(webhook_url, json=payload, timeout=5)消息模板化:允许用户自定义通知消息的格式。可以在配置文件中增加一个
message_template字段,使用类似Jinja2的语法,让用户自由定义标题和正文,支持变量如{{up_name}},{{video_title}},{{video_url}}。
4.3 构建用户友好的管理界面
对于非技术用户,命令行和配置文件不够友好。可以增加一个简单的Web管理界面。
- 使用轻量级Web框架:如
Flask或FastAPI。 - 设计几个核心接口:
GET /api/ups:获取当前订阅的UP主列表及其最新视频状态。POST /api/ups:添加一个新的UP主订阅(输入UID或主页链接)。DELETE /api/ups/<uid>:取消订阅。GET /api/history:获取更新历史记录。GET /:提供一个简单的HTML页面,展示上述信息,并提供添加/删除的表单。
- 部署:这个Web服务可以运行在本地局域网,也可以部署到云服务器,配合反向代理(如Nginx)和进程管理(如systemd或Supervisor)使其在后台稳定运行。
4.4 部署与运维考量
- 容器化部署:使用Docker和Docker Compose。创建一个
Dockerfile来定义Python环境、安装依赖、复制代码。再编写一个docker-compose.yml来定义服务、挂载配置文件和数据卷(用于持久化状态数据库)。这样,在任何支持Docker的机器上,一句docker-compose up -d就能启动服务,极大简化了部署。 - 日志记录:使用Python的
logging模块替代print,将日志输出到文件,并设置日志轮转,方便问题排查。 - 监控与告警:除了视频更新通知,还可以为追踪器本身设置监控。例如,如果连续多次检查失败,或者服务进程意外退出,可以通过另一个监控通道(如Server酱)发送告警给管理员。
5. 常见问题与实战排坑指南
在实际搭建和运行过程中,你几乎一定会遇到下面这些问题。这里是我踩过坑后总结的经验。
5.1 数据获取失败:API变更与反爬
问题现象:脚本运行一段时间后,突然无法获取到数据,返回空列表或错误码。
排查与解决:
- 手动验证API:首先在浏览器中直接访问脚本中使用的API URL(带上参数),查看返回的JSON数据是否正常。如果浏览器访问也出错,说明API可能已变更或需要额外的参数(如
wbi签名)。 - 检查请求头:B站对请求头有基本校验。确保你的
User-Agent是常见的浏览器标识,并正确设置Referer为UP主空间页。 - 处理WBI签名:B站部分API启用了WBI签名验证。如果你发现API返回
-403或提示签名错误,就需要实现签名算法。这需要从网页中提取img_key和sub_key,并对参数进行排序和MD5签名。这是一个相对复杂的步骤,通常需要参考最新的B站逆向工程资料。遇到此情况,一个暂时的规避方案是回退到HTML解析。 - 切换数据源:如果某个API不稳定,准备好备选方案。例如,主用
space.bilibili.com/ajax/member/getSubmitVideos,备用api.bilibili.com/x/space/arc/search,再备用HTML解析。在代码中实现降级逻辑。 - 控制请求频率:过于频繁的请求会导致IP被暂时限制。务必在配置中设置合理的检查间隔(如10-30分钟一次),对于大量UP主,可以考虑错峰检查。
5.2 通知发送失败:渠道配置问题
邮件发送失败:
- 错误:
smtplib.SMTPAuthenticationError:99%的情况是密码错误。请再次确认:- 使用的是授权码,不是邮箱登录密码。
- QQ邮箱/163邮箱等需要先在网页邮箱设置中开启SMTP服务并生成授权码。
- 发件邮箱地址填写正确。
- 错误:连接超时或被拒绝:检查
smtp_server和smtp_port是否正确。常见端口有465(SSL) 和587(TLS)。我们的示例使用了587和starttls()。如果使用465端口,则需要用SMTP_SSL。 - 邮件被收进垃圾箱:优化邮件内容。使用规范的
From/To头,正文不要过于简单,可以加入UP主名称、视频标题等个性化信息。如果可能,使用域名邮箱而非免费邮箱发送,成功率更高。
Telegram Bot 无响应:
- Bot没有收到消息?检查你是否已经向Bot发送过
/start命令来激活对话。 - Bot能收到命令但无法主动推送?确保你保存了用户的
chat_id。当用户向Bot发送任意消息时,在python-telegram-bot的更新对象update中,可以通过update.effective_chat.id获取到这个唯一的chat_id,并将其与用户的订阅关系一起存入数据库。推送消息时必须使用这个chat_id。
5.3 状态管理与误报
问题:有时UP主删除了最新视频,或者发布了“动态”而非视频,导致脚本误判为“新视频”。
解决方案:
- 更精确的比对:不要只比对最新的BVID。可以同时比对
BVID+发布时间戳。因为BVID可能重用(极罕见),但时间戳是递增的。如果发现本地记录的时间戳比获取到的“最新视频”时间戳还新,说明UP主可能删除了视频,此时应更新本地记录,但不发送通知。 - 区分内容类型:仔细分析API返回的数据结构。B站的“稿件”可能包含视频、专栏、音频等。确保你筛选的是
typeid或typename为视频类型的条目,避免将专栏更新误报为视频更新。 - 引入“确认窗口”:对于首次发现的“新视频”,不立即通知,而是等待下一次检查周期。如果连续两个周期都检测到同一个“新”视频,再发送通知。这可以过滤掉一些临时状态(如视频审核中、UP主立即删除等)。
5.4 性能与扩展性
问题:当订阅的UP主数量达到几百上千时,串行检查耗时很长,可能超过设定的间隔。
优化方案:
- 异步请求:使用
asyncio和aiohttp库,将所有的网络请求改为异步并发。这可以极大缩短单次检查的总耗时,尤其适合UP主数量多的情况。import aiohttp import asyncio async def fetch_up(session, uid): async with session.get(api_url, params={'mid': uid}) as resp: return await resp.json() async def check_all_ups(up_list): async with aiohttp.ClientSession() as session: tasks = [fetch_up(session, uid) for uid in up_list] results = await asyncio.gather(*tasks, return_exceptions=True) # 处理results - 分布式检查:如果UP主数量极其庞大,可以考虑将UP主列表分片,由多个工作进程或不同的服务器节点分别负责一部分UP主的检查,并通过一个中心服务汇总结果并发送通知。
搭建这样一个工具的过程,本身就是一次非常棒的全栈实践,涵盖了网络爬虫、数据存储、任务调度、消息推送、甚至简单的Web开发。当你成功运行起自己的追踪器,并准时收到心仪UP主的更新推送时,那种“自动化”带来的满足感,是单纯使用现成APP无法比拟的。更重要的是,你完全掌控了自己的订阅流,数据隐私和定制自由度都掌握在自己手中。