简介:BiliRaffle 是一款面向 C# 开发者的 B 站动态抽奖组件,基于 bilibili-api 与 bilibili-tool 生态构建,适合希望快速实现动态抽奖自动化、研究 B 站接口调用与 WPF 桌面应用开发的初中级程序员。资源包共 32 个文件,约 60KB,以 12 个 cs 源码文件与 4 个 xaml 界面文件为核心,辅以 config、xml、dll、sln、csproj 等工程配置与依赖文件,以及 png、ico、resx 等界面资源,整体结构完整,可直接用 Visual Studio 打开编译。项目采用 .NET Framework 4.5 及以上版本,解压后运行 BiliRaffle.exe 即可使用,涵盖登录窗口、主窗口、ViewModel、抽奖逻辑与 MVVM 辅助类等模块,便于读者理解桌面端抽奖工具的架构设计与实现思路。目前已有 697 人学习下载,适合作为 C# 桌面开发与 B 站接口实践的参考案例。
1. 抽奖工具还能这么写:一个把B站动态抽奖自动化的C#组件
刷到一条B站动态抽奖,要求关注、转发、@好友,你手动点完一轮,发现还有几十条类似的。更麻烦的是开奖后要一个个核对谁转发了、谁关注了,手动统计到眼花。BiliRaffle 就是冲着这个场景来的——一个用 C# 写的 B站动态抽奖组件,把「识别抽奖动态 → 记录参与者 → 随机开奖 → 输出结果」这条链路自动化。它基于 bilibili-api 这类接口封装思路,适合想拿现成组件做二次开发的后端或桌面端开发者,也适合想学 C# 调用第三方 API 的新手。项目本身不复杂,但麻雀虽小,接口鉴权、分页拉取、去重、随机算法这些坑一个不少,拿来练手或者直接嵌进自己的工具链都行。
2. 项目定位与技术选型:为什么用 C# 写一个抽奖组件
2.1 它到底解决什么问题
B站动态抽奖的痛点不在「抽」这个动作,而在「数据从哪来」和「结果怎么算」。一条抽奖动态下面可能有几千条转发评论,官方客户端不会给你导出参与者列表,手动翻页复制根本不现实。BiliRaffle 的核心价值是把这件事拆成三步:通过 bilibili-api 拉取指定动态的转发/评论数据,按规则过滤出有效参与者,再用随机算法抽出中奖者。
它不是一个带界面的成品软件,而是一个组件——你可以理解为一组类库加一个可执行的入口。开发者拿到源码后,可以把它塞进自己的控制台工具、桌面应用,甚至挂到定时任务里。关键词里的 bilibili-tool、bilibili-raffle 说的就是这类工具的统称,而 BiliRaffle 是其中用 C# 实现的一个具体版本。
适合谁?如果你写过 C#,想找一个真实调用 REST API、处理 JSON、做分页和去重的练手项目,它比那些纯 CRUD 的 Demo 有意思得多。如果你已经在做 B站相关的工具,需要一块抽奖逻辑,可以直接把核心模块拆出来用。
2.2 技术栈拆解
项目主体是 C#,大概率跑在 .NET 6 或 .NET 8 上(具体看 csproj 里的 TargetFramework)。为什么选 C# 而不是 Python?一个现实原因是:如果你要做 Windows 桌面端的抽奖工具,C# 加 WPF 或 WinForms 的打包和分发比 Python 省心,不用让用户装解释器。另一个原因是 C# 的 async/await 写异步 HTTP 请求很顺手,拉取大量分页数据时不会阻塞。
依赖方面,核心是 HTTP 客户端和 JSON 解析。常见做法是用HttpClient发请求,用System.Text.Json或Newtonsoft.Json反序列化响应。bilibili-api 本身有 Python 版本,但 C# 这边通常是直接调 B站的 Web API 端点,自己封装请求头和参数。项目里应该有一个BiliApiClient之类的类,负责统一处理 cookie、User-Agent、Referer 这些鉴权相关的头。
提示:B站接口对请求头比较敏感,尤其是
Referer和User-Agent,缺了容易被拒。这是后面避坑章节会细说的点。
2.3 目录结构长什么样
虽然没有拿到完整源码树,但按这类项目的惯例,结构大致如下:
BiliRaffle/ ├── BiliRaffle.Core/ # 核心逻辑 │ ├── Api/ # API 封装 │ │ ├── BiliApiClient.cs # HTTP 请求客户端 │ │ └── Models/ # 响应数据模型 │ ├── Raffle/ # 抽奖逻辑 │ │ ├── Participant.cs # 参与者实体 │ │ └── RaffleEngine.cs # 开奖引擎 │ └── Utils/ # 工具类 │ └── RandomHelper.cs # 随机数封装 ├── BiliRaffle.Cli/ # 命令行入口 │ └── Program.cs └── BiliRaffle.Tests/ # 单元测试这个结构的好处是 Core 和 Cli 分离,抽奖逻辑不依赖具体入口,你换成 WPF 界面或者 Web API 都不用动核心代码。如果你拿到的源码结构不同,重点找Api和Raffle这两个目录,改造时优先看它们。
3. 核心模块拆解:从拉取动态到开出中奖名单
3.1 接口鉴权与请求封装
B站大部分接口需要登录态,也就是 cookie 里的SESSDATA。BiliRaffle 要能读到抽奖动态的转发列表,就必须带上有效的 cookie。常见做法是把 cookie 放在配置文件或环境变量里,代码里统一注入到 HttpClient 的默认请求头。
public class BiliApiClient { private readonly HttpClient _http; public BiliApiClient(string sessData) { _http = new HttpClient(); // B站接口对 Referer 和 UA 有校验,必须带上 _http.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"); _http.DefaultRequestHeaders.Add("Referer", "https://www.bilibili.com"); // SESSDATA 是登录凭证,从 cookie 中提取 _http.DefaultRequestHeaders.Add("Cookie", $"SESSDATA={sessData}"); } public async Task<string> GetAsync(string url) { var resp = await _http.GetAsync(url); resp.EnsureSuccessStatusCode(); return await resp.Content.ReadAsStringAsync(); } }这段代码的关键点有三个:User-Agent伪装成浏览器、Referer指向 B站主站、Cookie里带SESSDATA。参数sessData需要你从浏览器登录后的 cookie 里手动复制,或者通过扫码登录接口获取。少了任何一个,接口大概率返回 -403 或空数据。
3.2 分页拉取转发列表
一条动态的转发数据是分页返回的,每页通常 20 条左右。要拿全所有参与者,必须循环请求直到没有下一页。这里有个容易翻车的地方:B站的分页接口用offset或pn参数,不同接口不一样,而且返回的has_more字段要正确判断。
public async Task<List<Participant>> FetchAllReposts(long dynamicId) { var all = new List<Participant>(); string offset = ""; bool hasMore = true; while (hasMore) { // 动态转发列表接口,offset 为空表示第一页 var url = $"https://api.bilibili.com/x/polymer/web-dynamic/v1/detail/repost" + $"?dynamic_id={dynamicId}&offset={offset}"; var json = await _client.GetAsync(url); var result = JsonSerializer.Deserialize<RepostResponse>(json); if (result?.Data?.Items == null) break; foreach (var item in result.Data.Items) { all.Add(new Participant { Uid = item.User.Mid, Name = item.User.Name, RepostTime = item.RepostTime }); } // 更新 offset 和 has_more,准备下一页 offset = result.Data.Offset; hasMore = result.Data.HasMore; // 加延迟,避免请求过快被限流 await Task.Delay(500); } return all; }逻辑说明:offset是游标,第一页传空字符串,后续用上一页返回的值。has_more为 false 时停止循环。Task.Delay(500)是血泪经验——不加延迟连续请求,很容易触发风控,返回 412 或者直接封 IP 一段时间。参数dynamicId是动态的 ID,从动态链接里能拿到,比如t.bilibili.com/xxxxx里的数字部分。
3.3 参与者去重与过滤
拉回来的列表里会有重复——同一个人可能转发了多次,或者既转发了又评论了。抽奖规则通常要求「每个用户只算一次」,所以要去重。另外还要过滤掉不符合条件的,比如抽奖发起者本人、机器人账号、转发内容为空的。
public List<Participant> Deduplicate(List<Participant> raw) { // 按 Uid 分组,每组取最早一次转发时间 var unique = raw .GroupBy(p => p.Uid) .Select(g => g.OrderBy(p => p.RepostTime).First()) .ToList(); // 过滤掉发起者自己(假设发起者 Uid 已知) unique = unique.Where(p => p.Uid != _hostUid).ToList(); return unique; }GroupBy按用户 ID 分组,OrderBy取最早时间,保证结果稳定。_hostUid是抽奖发起者的 UID,需要提前配置。这一步看起来简单,但如果不去重,开奖时同一个人可能中两次,结果就失去公信力了。
3.4 随机开奖算法
开奖的核心是「公平随机」。C# 里用Random类就能做,但要注意两点:一是不要每次 new 一个 Random,否则短时间内可能产生相同种子;二是如果需要可复现的结果,可以用固定种子。
public List<Participant> Draw(List<Participant> pool, int winnerCount) { if (pool.Count < winnerCount) throw new ArgumentException("参与者数量不足"); // 使用 Fisher-Yates 洗牌算法,保证每个位置等概率 var shuffled = new List<Participant>(pool); var rng = Random.Shared; // .NET 6+ 的线程安全随机源 for (int i = shuffled.Count - 1; i > 0; i--) { int j = rng.Next(i + 1); (shuffled[i], shuffled[j]) = (shuffled[j], shuffled[i]); } return shuffled.Take(winnerCount).ToList(); }Fisher-Yates 洗牌是标准做法,比「随机取一个然后移除」效率高,也不会出现概率偏差。Random.Shared是 .NET 6 引入的静态实例,线程安全,省去了自己管理种子的麻烦。参数winnerCount是中奖人数,pool是去重后的参与者列表。开奖结果建议同时输出中奖者的 UID 和昵称,方便核对。
4. 跑起来:环境准备与命令行执行
4.1 环境依赖清单
在跑之前,确认本机有这些:
| 依赖项 | 版本要求 | 说明 |
|---|---|---|
| .NET SDK | 6.0 或以上 | 编译和运行 C# 项目 |
| Git | 任意 | 克隆源码 |
| 文本编辑器 | VS / VS Code / Rider | 改配置和看代码 |
| B站账号 | 已登录 | 获取 SESSDATA |
如果只是跑起来看效果,装 .NET SDK 和 Git 就够了。想改代码,建议用 Visual Studio 或者 Rider,调试体验比 VS Code 好一些。
4.2 获取 SESSDATA 并配置
SESSDATA 是 B站的登录凭证,存在浏览器的 cookie 里。操作步骤:登录 B站网页版,按 F12 打开开发者工具,切到 Application 或 Storage 面板,找到 Cookies 下的bilibili.com,复制SESSDATA的值。这个值很长,类似xxxxxx%2Cxxxxx%2Cxxxxx的格式。
拿到后,在项目根目录创建一个appsettings.json或者直接改Program.cs里的配置:
{ "Bili": { "SessData": "你的SESSDATA值", "HostUid": 12345678 }, "Raffle": { "DynamicId": 987654321, "WinnerCount": 3 } }HostUid是抽奖发起者的 UID,DynamicId是抽奖动态的 ID,WinnerCount是中奖人数。这三个参数是跑通流程的最小配置。
4.3 编译与运行
打开终端,进入项目目录,执行:
# 还原依赖 dotnet restore # 编译 dotnet build -c Release # 运行命令行入口 dotnet run --project BiliRaffle.Cli如果一切正常,你会看到控制台先输出「正在拉取转发列表…」,然后显示拉取到的参与者数量,最后打印中奖名单。第一次跑建议把WinnerCount设小一点,比如 1,方便验证。
注意:如果运行时报 403 或返回空列表,先检查 SESSDATA 是否过期。B站的登录态有时效,过期后需要重新获取。
4.4 验证结果是否正确
跑完之后怎么确认结果没毛病?三个检查点:第一,参与者数量是否合理,一条几千转发的动态,去重后应该接近实际人数;第二,中奖者是否在参与者列表里,可以手动搜一下 UID;第三,重复跑几次,中奖名单应该不同(除非用了固定种子)。如果每次结果都一样,检查随机数是不是被写死了。
5. 避坑与常见问题:那些让我熬夜的报错
5.1 请求返回 -403 或 -412
现象:接口调用后返回错误码 -403,或者 HTTP 状态码 412。原因:请求头不完整,缺少Referer、User-Agent,或者 SESSDATA 过期。解决:补齐请求头,重新获取 SESSDATA。如果还是不行,检查是不是请求频率太高被风控了,加长Task.Delay的时间。
5.2 分页拉取中途断掉
现象:拉到一半报错,或者has_more一直是 true 导致死循环。原因:B站接口在数据量大时可能返回异常结构,或者 offset 没有正确更新。解决:在循环里加最大页数限制,比如最多拉 200 页;对返回的 JSON 做空值判断,result?.Data?.Items这种写法能避免空引用。
5.3 去重后人数对不上
现象:去重后参与者数量比预期少很多。原因:可能把不同用户误判为同一人,或者 UID 字段解析错了。解决:打印几个原始数据的 UID 看看,确认字段映射正确。B站用户唯一标识是mid,不是昵称,昵称可以重复。
5.4 中奖名单被质疑不公平
现象:开奖后有人觉得结果有猫腻。原因:随机过程不透明,没有留下可验证的记录。解决:开奖前把参与者列表的哈希值公布出来,开奖后用固定种子复现结果。虽然 BiliRaffle 本身不一定带这个功能,但二次开发时可以加上。
5.5 程序跑完就退出,看不到结果
现象:命令行一闪而过,或者输出完就关了。原因:控制台程序执行完 Main 方法就结束了。解决:在最后加一句Console.ReadLine()暂停,或者把结果写到文件里。我一般会把中奖名单同时输出到result.txt,方便后续核对。
6. 二次开发:把抽奖组件改成你自己的工具
拿到源码后,最直接的改造方向是换入口。BiliRaffle.Cli 是命令行,你可以新建一个 WPF 项目引用 BiliRaffle.Core,做一个带界面的抽奖工具。界面部分只需要三个输入框(SESSDATA、动态 ID、中奖人数)和一个按钮,点击后调用RaffleEngine.Draw,把结果展示在列表里。这样非技术用户也能用。
第二个方向是加定时任务。抽奖动态往往有截止时间,你可以用System.Threading.Timer或者 Quartz.NET,在指定时间自动拉取数据并开奖。核心代码不用改,只是把FetchAllReposts和Draw串起来,挂到定时器上。
第三个方向是扩展数据源。现在只拉了转发,其实评论也可以作为参与方式。B站的评论接口和转发接口类似,你可以照着FetchAllReposts写一个FetchAllComments,然后把两个列表合并去重。注意评论接口的分页参数可能不一样,需要单独调试。
改造时有个技巧:先把 Core 里的接口抽象出来。比如定义一个IParticipantSource接口,转发和评论各实现一个类,这样以后加新的参与方式(比如弹幕)就不用动开奖逻辑了。
public interface IParticipantSource { Task<List<Participant>> FetchAsync(long dynamicId); } public class RepostSource : IParticipantSource { /* ... */ } public class CommentSource : IParticipantSource { /* ... */ }这样RaffleEngine只依赖接口,符合开闭原则。我当初就是没做这层抽象,后来加评论功能时把开奖逻辑改得一团糟,后悔药都没得吃。
最后一个建议:不管怎么改,开奖前一定先跑一遍「干跑」——只拉数据不开奖,确认参与者数量和名单没问题再执行随机。从那以后我每次改完抽奖逻辑,都强制走一遍干跑验证,再也没出现过开奖后才发现数据拉错的情况。希望帮到你。
本文还有配套的精品资源,点击获取