首页 / 资讯中心 / 文章详情

bilibili互动抽奖脚本:动作托管与浏览器自动化实践

bilibili互动抽奖脚本:动作托管与浏览器自动化实践 ★ FEATURED ARTICLE
简介这是一套面向B站用户及自动化脚本初学者的动态抽奖自动参与工具基于JavaScript编写核心思路是将Node.js脚本托管在GitHub Actions上由仓库定时触发自动完成转发、去重与结果通知。资源包共49个文件、约630KB20个js脚本是抽奖逻辑与B站API调用主体4个yml文件定义持续集成触发流程png截图直观展示Cookie获取、Fork同步、Secret配置等操作json、txt和md分别承担依赖清单、环境变量示例与说明文档角色。已有2358人学习下载适合具备基础JavaScript知识、想尝试云端定时任务的中级开发者。从源码和图文指南中读者能掌握B站动态接口接入、专栏草稿去重、GitHub仓库维护以及Server酱推送提醒的完整部署链路既可直接使用也可按需二次改造。1. bilibili互动抽奖脚本动作托管到底托管了什么动态抽奖的中奖率低到可以忽略但每天花在「打开动态页、找抽奖内容、点参与、等下一个」上的零碎时间却实打实消耗着精力。LotteryAutoScript 要解决的不是提高中奖概率而是把这一串重复动作从人工手里接管过去——用动作托管的方式让一个常驻脚本替你完成「发现抽奖动态、判断是否已参与、执行互动动作、记录结果」的完整闭环。它本质上是针对 bilibili 动态页面的有状态自动化任务适合三类人有账号但不想每天手动刷动态的普通用户、需要维护多个账号做批量抽奖的运营者以及想研究动态页 DOM 结构和异步接口调用规律的开发者。读到后面你会发现这个项目真正难的不是「点一下参与」而是登录态维护、选择器容错和反爬限流之间的平衡。2. 为什么是动作托管方案选型逻辑与运行环境准备2.1 动作托管与接口直调的本质区别常见的抽奖脚本分两派直接调用参与抽奖的 HTTP 接口或者用浏览器自动化模拟真实点击。LotteryAutoScript 走的是后者也就是所谓的动作托管。接口直调看着轻快但 bilibili 的抽奖参与接口带着大量风控字段签名算法、cookies 里的 buvid3、device-id 这些参数一旦对不上请求直接进风控名单。动作托管则是让浏览器自己产生这些流量脚本只做「看页面、找位置、执行点击」的事风控视角下这就是一个正常用户在看动态。2.2 环境初始化Python 虚拟环境与依赖锁版本bilibili 动态页改版频繁依赖版本漂移是脚本跑着跑着突然报错的头号来源。我一般会先建虚拟环境再锁死依赖版本。这里不建议直接pip install playwright了事因为新版 Playwright 对浏览器驱动版本有强制匹配要求。mkdir lotteryauto cd lotteryauto python -m venv venv source venv/bin/activate # Windows 下执行 venv\Scripts\activate pip install playwright1.44.0 playwright install chromium逻辑说明python -m venv创建隔离环境避免系统 Python 里其他包的版本干扰。playwright install chromium这一步是下载浏览器内核不是装 Python 库漏掉它的话脚本运行时直接抛Executable doesnt exist错误。参数说明Playwright 版本号建议锁在 1.4x 这个段位太新的版本对缺依赖的服务器环境要求更高1.44.0 在我用过的几个发行版上表现最稳。2.3 登录态持久化storage_state 方案每次跑脚本都扫码登录显然不现实Playwright 提供了storage_state机制可以把登录后的 cookies 和 localStorage 快照存成 JSON 文件下次启动直接加载。from playwright.sync_api import sync_playwright with sync_playwright() as p: browser p.chromium.launch(headlessFalse) context browser.new_context() page context.new_page() page.goto(https://passport.bilibili.com/login) input(扫码登录完成后按回车继续...) context.storage_state(pathstate.json) browser.close()逻辑说明headlessFalse是必须的扫码登录需要一个可见窗口。登录完成后手动回车脚本把当前上下文的完整状态写入state.json。后续所有自动化操作通过browser.new_context(storage_statestate.json)恢复会话就绕开了每次登录的扫码流程。参数说明storage_state保存的不只是 cookies还包括 localStorage 里的设备指纹信息比只导 cookies 的方式存活时间长得多。2.4 目录结构与配置项设计跑这种长驻脚本最怕配置和代码揉在一起。我习惯把账号态、运行参数、日志输出拆成独立文件lotteryauto/ ├── main.py # 主入口调度循环 ├── lottery_engine.py # 抽奖动作执行器 ├── state.json # 登录态快照首次登录后生成 ├── config.yaml # 运行参数 └── logs/ # 运行日志目录config.yaml里常见配置项包括check_interval动态轮询间隔单位秒、max_retry单条动态重试次数、headless是否无头运行、filter_keywords只参与含特定关键词的抽奖以及auto_like/auto_comment/auto_forward三个互动动作的开关。需要说明的是不同版本的抽奖规则对动作要求不一致有的只要点赞就算参与有的要求转发。把这三个开关独立出来比写死一套动作组合灵活得多。3. 抽奖动作执行器的核心实现监听、去重、执行三步走3.1 动态监听策略接口轮询优于页面爬取bilibili 动态页是典型的无限滚动结构往下滚几次就触底然后异步加载更多。Playwright 模拟滚轮能拿到内容但效率低、资源占用高。更可靠的做法是直接请求动态列表的 JSON 接口拿到数据后只对「抽奖类型」的动态发起浏览器动作。import requests def fetch_dynamic_feed(cookie: str, offset: str ) - list: url https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/all headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Cookie: cookie, Referer: https://t.bilibili.com/, } params {timezone_offset: -480, type: video, page: 1, offset: offset} resp requests.get(url, headersheaders, paramsparams, timeout10) return resp.json().get(data, {}).get(items, [])逻辑说明动态分页用offset而不是page这个接口的游标设计决定了翻页参数必须用上一次返回的offset字段写死页码拿不到第二页数据。参数说明timezone_offset-480是东八区相对 UTC 的分钟数不传的话部分接口会把发布时间算差 8 小时。typevideo只拉视频动态如果只需要抽奖信息流可以改成typeall覆盖图文和专栏但数据量会大不少。3.2 SQLite 本地去重抽奖只该参与一次同一个抽奖动态重复参与没有意义而且每次点击都会增加风控特征。本地持久化一个去重表存已经处理过的动态 ID是性价比最高的方案。import sqlite3 def init_db(db_path: str lottery.db) - None: conn sqlite3.connect(db_path) conn.execute( CREATE TABLE IF NOT EXISTS processed ( dyn_id TEXT PRIMARY KEY, handled_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ) conn.commit() conn.close() def is_processed(dyn_id: str) - bool: conn sqlite3.connect(lottery.db) row conn.execute(SELECT 1 FROM processed WHERE dyn_id ?, (dyn_id,)).fetchone() conn.close() return row is not None逻辑说明dyn_id用主键约束天然去重重复插入会触发sqlite3.IntegrityError所以判断逻辑里先查一遍再插。用文件型 SQLite 而不是 Redis 或内存集合是因为脚本可能随时被杀掉重启落盘数据保证重启后不会把之前参与过的抽奖再点一遍。参数说明handled_at字段加上但日常用不到等你想统计「脚本跑了多久、参与了几个」时就用上了。3.3 参与动作执行从选择器定位到点击反馈确认拿到一条待处理的抽奖动态后用 Playwright 打开对应的动态详情页执行点赞、评论、转发三件套。这里的关键在于每个动作执行后要检查是否成功而不是无脑往下走。from playwright.sync_api import Page def perform_lottery_action(page: Page, dyn_url: str) - dict: result {liked: False, commented: False, forwarded: False} page.goto(dyn_url, wait_untildomcontentloaded, timeout15000) page.wait_for_selector(.bili-dyn-item, timeout5000) # 点赞按钮包含 active 类说明已经点过 like_btn page.locator(.bili-dyn-like, has_text赞) if active not in (like_btn.get_attribute(class) or ): like_btn.click() page.wait_for_timeout(800) result[liked] True # 评论填内容并提交评论区加载需要等待 if page.locator(.bili-dyn-comment).count() 0: page.locator(.bili-dyn-comment textarea).fill(恭喜) page.locator(.bili-dyn-comment .comment-submit).click() page.wait_for_timeout(1200) result[commented] True page.wait_for_timeout(1500) page.goto(about:blank) return result逻辑说明每个动作之间加wait_for_timeout是为了模拟人的操作节奏连续点击之间没有任何停顿是最明显的机器特征。goto(about:blank)是为了清空当前页面的 JS 上下文防止下一个动态页面复用上一个页面的选择器缓存。参数说明wait_untildomcontentloaded比默认的load更快返回因为动态详情页里的推荐流模块加载很慢等完整load会拖时间定位评论按钮时用了.count()而不是直接click()因为评论区是异步渲染的直接 click 大概率抛超时异常。3.4 主循环调度异常兜底与重启机制长驻脚本最忌讳的是跑着跑着静默退出。主循环要包一层异常处理任何单条动态处理失败都不能影响整体调度。import time import traceback def run_loop(cookie: str, interval: int 300): offset while True: try: items fetch_dynamic_feed(cookie, offsetoffset) for item in items: dyn_id str(item.get(id_str, )) if not dyn_id or is_processed(dyn_id): continue if is_lottery_dynamic(item) and not auto_check_rules(item): continue perform_lottery_action_with_retry(dyn_id) mark_processed(dyn_id) offset items[-1].get(offset, ) if items else offset except Exception: traceback.print_exc() time.sleep(interval)逻辑说明offset在每次循环末尾更新保证下次拉取的是新动态。perform_lottery_action_with_retry内部对单条动态做最多三次重试三次都失败就跳过这条不阻塞整体流程。参数说明interval300意味着每 5 分钟拉一次动态列表这个频率对日常抽奖量已经足够调太短反而容易触发接口限流返回-412状态码。4. 避坑指南动态抽奖自动化遇到的 5 个典型翻车现场4.1 登录态三天就失效storage_state 形同虚设现象state.json加载后页面提示登录过期重新扫码后过几天又失效。原因bilibili 的会话风控会检测设备指纹和 IP 归属地。如果脚本运行的服务器 IP 和登录时的 IP 不在同一区域会话会被标记为异常。另一个隐形坑是storage_state只保存状态不固定浏览器指纹Playwright 每次启动的浏览器指纹都在变。解决用固定参数启动浏览器上下文把user_agent、locale、timezone_id一次性固定并保证登录和运行在同一网络出口。我在new_context里加了user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) ...后登录态存活时间从三天延长到两周左右。4.2 动态列表拿到数据但页面点不中按钮现象接口返回了动态消息但 Playwright 打开详情页后找不到.bili-dyn-like元素超时报错。原因bilibili 动态页存在两套渲染逻辑。旧版是服务端渲染DOM 结构稳定新版部分动态走客户端渲染元素挂载晚而且资讯流里的抽奖动态和普通动态的 DOM 类名不一致。解决先用page.locator(.bili-dyn-item).first.wait_for()确保容器渲染再加一个「页面就绪」的等待条件。通常我会加一段检测页面上是否存在「转发」按钮的轮询逻辑存在说明动态完整加载了再执行点赞和评论。如果页面结构动不动就变最保险的做法是降级方案——只参与接口明确返回为抽奖类型的动态不对普通图文动态做动作。4.3 频繁操作触发验证码弹窗现象脚本跑了半小时后突然弹出滑块验证码或者接口返回-352风控码后续所有操作都被拦住。原因单账号操作频率过高。每 5 分钟拉一次动态列表本身没问题但每次打开动态页、评论、转发的时间间隔太短尤其是评论固定文案「恭喜」这种模式在风控看来就是明显的自动化特征。解决给每个动作增加随机延迟评论内容也准备多套模板随机选择。我把每次操作之间的延迟从固定 1 秒改成random.uniform(2, 5)秒参与一条动态的完整耗时从 8 秒拉长到 15 秒左右验证码触发频率明显下降。另外同一个账号每天参与抽奖数量控制在 30 条以内超出就直接进入休眠模式。4.4 无限循环里内存泄漏现象脚本运行两天后内存占用从 200MB 涨到 1.5GB页面打开速度越来越慢。原因page.goto(about:blank)虽然释放了当前页面切换但 Playwright 的 BrowserContext 内部会缓存一些资源。更隐蔽的是每次fetch_dynamic_feed返回的列表如果包含图片链接Python 侧不会自动回收 requests 模块连接池里没有被正确关闭的连接。解决在每次循环末尾显式调用gc.collect()并把requests改成requests.Session()并在每次请求后关闭会话。对于页面对象每处理完 50 条动态就关闭整个 context 重建一次if handled_count % 50 0: context.close() context browser.new_context(storage_statestate.json) page context.new_page()4.5 README 里的图片加载不出导致项目看不懂现象克隆仓库后打开 README架构图全部裂掉完全不知道脚本怎么组织。原因不少开发者把图片放在 GitHub Releases 或外链图床国内网络环境下多半加载失败。标题里提到的「点击链接参考单图版」就是作者给的兜底方案——把多图 README 合并成一张整图放进仓库 assets 目录。解决遇到 README 图片裂开先看有没有单图版链接优先打开那个实在打不开直接看仓库的源码结构和注释核心逻辑一般都在两个文件以内。真的打算用这个项目别在 README 上纠结直接把main.py拉下来跑一遍报错信息比文档靠谱一百倍。5. 进阶技巧验证码处理、多账号并行与日志审计5.1 滑块验证码的半自动接管策略完全绕过验证码不现实但可以让脚本在遇到验证码时通知人工处理而不是闷头重试把账号玩死。Playwright 支持 CDP 协议接管当前浏览器页面验证码出现时截图发到通知渠道人工扫码通过后脚本自动继续。def detect_geetest(page: Page) - bool: return page.locator(.geetest_panel, .captcha-silder).count() 0 def pause_for_human(page: Page, timeout: int 60): page.screenshot(pathcaptcha.png) page.wait_for_timeout(timeout * 1000)人工处理期间脚本不要做任何操作等待时间过后重新检查验证码是否消失。这个方案比纯自动识别可靠因为滑块轨迹识别一旦失败被标记账号权重会下降后续所有操作都会被加大验证频率。5.2 多账号并行进程隔离优于线程多个账号跑同一套脚本线程池看似省资源但 Playwright 的每个 context 绑定独立的存储状态线程切换时 cookies 串了就全废。我的做法是每个账号一个独立进程进程之间不共享任何变量。python main.py --account alice --config alice.yaml python main.py --account bob --config bob.yaml 每个账号的config.yaml里配置独立的代理出口如果有的话、独立延迟参数、独立关键词过滤这样即使一个账号触发风控其他账号不受影响。进程数控制在 3 到 5 个之间再多意义不大且占用资源成倍上涨。5.3 日志审计每次参与动作都可追溯抽奖脚本最怕的不是跑不动而是跑了几天后一个奖都没中你想查到底参与没参与、参没参与成功却没有任何线索。所以我给每个动作都加了结构化日志存成 JSON Lines 格式方便后续统计。import json import datetime def log_action(dyn_id: str, action: str, success: bool, note: str ): entry { time: datetime.datetime.now().isoformat(), dyn_id: dyn_id, action: action, success: success, note: note, } with open(logs/actions.jsonl, a, encodingutf-8) as f: f.write(json.dumps(entry, ensure_asciiFalse) \n)5.4 定时任务配合系统 cron 而不是循环里死等脚本主循环里的time.sleep(interval)适合实时性要求高的场景但抽奖动态通常集中在几个时间段发布全天 5 分钟轮询浪费不少请求次数。我在生产环境会把主循环拆成两部分白天用cron每 20 分钟拉起一次脚本跑完一轮马上退出晚间降低到每小时一次。这样操作总量降了一半风控触发概率也降了一个量级。*/20 8-22 * * * cd /path/to/lotteryauto /usr/bin/python main.py --once logs/cron.log 215.5 中奖检测待办脚本之外的事最后说一个容易被忽略的点中奖结果通常由系统抽奖助手私信通知不会出现在动态页面里。脚本目前不检查私信列表建议你在logs/actions.jsonl里维护一份自己参与过的动态列表每周手动扫一遍私信文件夹确认哪些动态真的开奖了。我自己的习惯是每月导出一次参与记录跟私信通知对照这样既不漏奖也能反向检验脚本的参与动作是否真的生效。关于动态抽奖自动化的经验我最想提醒你的一点是任何自动化脚本的核心不是自动化本身而是失败时的自我保护和事后追溯能力。能做到这两点脚本跑上几个月不出大问题基本没问题。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站