C# 实现 B站动态抽奖组件:从接口调用到随机抽取全流程

发布时间:2026/10/23 15:05:37
C# 实现 B站动态抽奖组件:从接口调用到随机抽取全流程 简介BiliRaffle 是一款面向 C# 开发者与 B 站工具爱好者的动态抽奖组件基于 bilibili-api 实现可帮助用户快速搭建 B 站动态抽奖的自动化流程适合具备一定 .NET 桌面开发基础、希望研究抽奖逻辑或二次开发的技术人员。资源包共 32 个文件以 12 个 cs 源码文件与 4 个 xaml 界面文件为核心辅以 config、xml、dll、sln、csproj 等工程配置与依赖文件另有 png、ico、resx 等资源文件整体约 60KB结构紧凑、便于阅读与编译。项目采用 .NET Framework 4.5 及以上版本解压后运行 BiliRaffle.exe 即可使用源码涵盖登录窗口、主窗口、ViewModel、抽奖逻辑与 MVVM 辅助类等模块目录组织清晰。目前已有 697 人学习下载读者可借此理解 B 站动态抽奖的接口调用、界面绑定与业务分层思路并在此基础上进行功能扩展或排错调试。1. 抽奖工具还能这么写一个 C# 组件把动态抽奖流程全包了刷到一条动态抽奖评论区几千条手动翻不现实。BiliRaffle 就是冲着这个场景来的——它是一个用 C# 写的 B站动态抽奖组件核心能力是拉取指定动态下的转发/评论用户列表按规则去重、过滤最后随机抽出中奖者。适合两类人一是想自己搭个抽奖工具但不想从零调接口的开发者二是拿它当 bilibili-api 的 C# 封装参考来学。整个项目围绕 bilibili-api 的接口调用展开把鉴权、分页拉取、数据清洗、随机抽取串成了一条完整链路。你拿到源码后改几行配置就能跑也能按自己的业务逻辑替换抽取策略。2. 项目结构与技术选型C# 怎么跟 bilibili-api 对接2.1 目录骨架与职责划分拿到源码先别急着跑花两分钟把目录扫一遍后面改代码能省不少时间。BiliRaffle 的结构是典型的 C# 类库 控制台入口的组合常见做法是拆成三层接口层、业务层、入口层。接口层负责跟 bilibili-api 打交道封装 HTTP 请求和 Cookie 鉴权业务层处理抽奖逻辑包括去重、过滤、随机算法入口层就是一个 Program.cs读配置、调业务、输出结果。一个合理的目录大概长这样BiliRaffle/ ├── BiliRaffle.Core/ │ ├── Api/ │ │ ├── BiliClient.cs # HTTP 客户端封装 │ │ └── DynamicApi.cs # 动态相关接口 │ ├── Models/ │ │ ├── UserInfo.cs # 用户数据模型 │ │ └── RaffleConfig.cs # 抽奖配置模型 │ ├── Services/ │ │ ├── RaffleService.cs # 抽奖核心逻辑 │ │ └── UserFilter.cs # 用户过滤与去重 │ └── Utils/ │ └── RandomHelper.cs # 随机抽取工具 ├── BiliRaffle.Console/ │ └── Program.cs # 控制台入口 └── BiliRaffle.sln这个划分的好处是你想换抽取算法只动 Services想换接口版本只动 Api互不干扰。我一般会把配置项单独抽出来不硬编码在代码里后面会讲怎么改。2.2 技术栈与依赖说明项目用的是 .NET 6 或以上语言是 C#。为什么选 C# 而不是 Python一个现实原因是 bilibili-api 在 C# 生态里有现成的 HttpClient 封装方案而且 C# 的强类型模型在处理接口返回的嵌套 JSON 时更省心——字段对不上编译期就报错不用等到运行时才发现 KeyError。核心依赖通常包括依赖用途备注System.Net.HttpHTTP 请求.NET 内置System.Text.JsonJSON 反序列化.NET 内置也可换 Newtonsoft.JsonMicrosoft.Extensions.Configuration读配置文件可选简单项目直接读 JSON 也行如果你习惯用 Newtonsoft.Json把反序列化那几行换掉就行模型类不用动。这里没有引入太重的框架整个项目保持轻量编译出来就是一个 exe扔到服务器上定时跑也没问题。2.3 鉴权与请求封装B站接口绕不开鉴权。BiliRaffle 的鉴权方式是在请求头里带 Cookie核心是 SESSDATA 和 bili_jct 这两个值。SESSDATA 用来标识登录态bili_jct 用于部分写操作抽奖本身是读操作但有些接口会校验。public class BiliClient { private readonly HttpClient _http; private readonly string _sessData; public BiliClient(string sessData) { _sessData sessData; _http new HttpClient(); // 常见做法统一设置 UA 和 Referer否则接口可能返回 -412 _http.DefaultRequestHeaders.Add(User-Agent, Mozilla/5.0 (Windows NT 10.0; Win64; x64)); _http.DefaultRequestHeaders.Add(Referer, https://www.bilibili.com); _http.DefaultRequestHeaders.Add(Cookie, $SESSDATA{_sessData}); } public async Taskstring GetAsync(string url) { var resp await _http.GetAsync(url); resp.EnsureSuccessStatusCode(); return await resp.Content.ReadAsStringAsync(); } }这段代码的关键点有三个一是 UA 和 Referer 必须带不带的话接口大概率返回 -412 风控错误这是血泪经验二是 Cookie 拼接格式要对SESSDATA 后面直接跟值不要多加空格三是 EnsureSuccessStatusCode 会在 HTTP 层报错时抛异常方便定位问题。参数方面sessData 从浏览器登录后的 Cookie 里复制注意不要泄露给别人。3. 核心模块拆解从拉取用户到抽出中奖者的完整链路3.1 动态评论/转发列表分页拉取抽奖的数据来源是动态下的转发和评论。B站接口是分页返回的一页最多 20 条左右所以必须循环拉取直到没有下一页。这里最容易翻车的地方是分页参数和去重——接口返回的游标next_offset要正确传递否则会重复拉同一页。public async TaskListUserInfo FetchAllUsers(string dynamicId) { var users new ListUserInfo(); string offset ; bool hasMore true; while (hasMore) { // 构造分页请求offset 为空表示第一页 var url $https://api.bilibili.com/x/v2/reply/main $?type11oid{dynamicId}mode3next{offset}; var json await _client.GetAsync(url); var result JsonSerializer.DeserializeApiResponseReplyData(json); if (result?.Data?.Replies null) break; foreach (var reply in result.Data.Replies) { users.Add(new UserInfo { Uid reply.Member.Mid, Nickname reply.Member.Uname }); } // 更新游标判断是否还有下一页 offset result.Data.Cursor?.Next.ToString() ?? ; hasMore result.Data.Cursor?.IsEnd false !string.IsNullOrEmpty(offset); // 加延迟避免请求过快触发风控 await Task.Delay(800); } return users; }逻辑说明type11 表示动态类型oid 是动态 IDmode3 是按时间排序。每次循环把当前页的用户加进列表然后从 Cursor 里取下一页游标。is_end 为 true 或者游标为空就停止。参数方面dynamicId 从动态链接里取比如 t.bilibili.com/xxxxx 里的数字部分。那个 Task.Delay(800) 不是可有可无的请求太快会被限流800ms 是我试过比较稳的间隔你可以根据实际情况调到 1000ms 以上。3.2 用户去重与过滤规则拉回来的用户列表里会有重复——同一个人可能转发多次也可能既转发又评论。去重是抽奖公平性的基础不去重的话活跃用户中奖概率会被放大。除了去重通常还要过滤掉一些账号比如官方账号、已注销账号、黑名单用户。public class UserFilter { public ListUserInfo Deduplicate(ListUserInfo raw) { // 按 Uid 去重保留第一个出现的记录 var seen new HashSetlong(); var result new ListUserInfo(); foreach (var u in raw) { if (seen.Add(u.Uid)) result.Add(u); } return result; } public ListUserInfo ApplyRules(ListUserInfo users, RaffleConfig config) { return users.Where(u !config.ExcludeUids.Contains(u.Uid) // 排除指定用户 !string.IsNullOrEmpty(u.Nickname) // 排除异常账号 ).ToList(); } }去重用的是 HashSet时间复杂度 O(n)几千条数据毫秒级完成。ApplyRules 里可以按需扩展比如加一个「关注时间早于某天」的过滤条件或者「等级大于 2」的门槛。config.ExcludeUids 是一个配置项把不想参与抽奖的 UID 填进去就行。注意 Nickname 为空的账号通常是已注销或异常号直接过滤掉比较稳妥。3.3 随机抽取与结果输出抽取环节看起来简单但随机数的质量直接影响公平性。用 Random 类就够了但要注意不要每次 new 一个 Random否则在快速连续调用时可能产生相同种子。正确做法是全局共享一个 Random 实例或者用 RandomNumberGenerator。public class RaffleService { private static readonly Random _rng new Random(); public ListUserInfo Draw(ListUserInfo pool, int count) { if (count pool.Count) return pool; // 人数不够全部中奖 // Fisher-Yates 洗牌取前 count 个 var shuffled new ListUserInfo(pool); 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(count).ToList(); } }Fisher-Yates 洗牌的好处是每个元素被选中的概率严格相等不会因为取前 N 个而偏向列表前面的用户。参数 count 是中奖人数从配置里读。如果 pool 里的人数少于 count直接全部返回这个边界情况要处理不然会抛异常。输出结果时把 Uid 和昵称一起打出来方便核对。4. 跑起来配置、编译与运行步骤4.1 环境准备与依赖还原跑这个项目之前确认机器上装了 .NET SDK 6.0 或更高版本。命令行敲dotnet --version能看到版本号就行。如果没装去微软官方下载安装包一路下一步即可。装完之后进入项目根目录执行依赖还原cd BiliRaffle dotnet restore这一步会根据 .csproj 文件里的 PackageReference 拉取 NuGet 包。如果网络慢可以配一个国内镜像源在NuGet.config里加一行就行。还原成功后dotnet build编译一遍确认没有报错。4.2 配置文件怎么写项目通常用一个appsettings.json或config.json来管理参数不要把 SESSDATA 硬编码在代码里。一个典型的配置文件长这样{ SessData: 你的SESSDATA值, DynamicId: 动态ID数字, WinnerCount: 3, ExcludeUids: [123456, 789012], RequestDelayMs: 800 }SessData 从浏览器开发者工具的 Application → Cookies 里复制注意不要带多余空格。DynamicId 是动态链接里的数字。WinnerCount 是中奖人数。ExcludeUids 是排除列表没有就留空数组。RequestDelayMs 控制请求间隔网络好的话可以调到 500但别低于 300否则容易触发风控。4.3 运行与结果验证配置填好后直接运行dotnet run --project BiliRaffle.Console控制台会先打印拉取进度比如「已拉取 200 条继续…」拉完之后输出去重后的总人数最后打印中奖名单。验证结果是否合理可以看两个地方一是总人数跟动态实际转发评论数是否大致吻合差太多说明分页没拉全二是中奖名单里有没有重复 UID有的话说明去重逻辑没生效。如果中途报 -412 或 -509基本是请求太快或者 Cookie 失效换一个 SESSDATA 再试。5. 避坑与常见问题排查5.1 接口返回 -412 风控错误现象程序跑几页之后突然抛异常接口返回码 -412。原因请求频率过高或者 UA/Referer 没带对。解决把 RequestDelayMs 调到 1000 以上确认 HttpClient 的 DefaultRequestHeaders 里 UA 和 Referer 都设置了。如果还是不行换一个网络环境试试有些 IP 段被风控得更严。5.2 分页拉取重复或遗漏现象拉回来的用户列表里有大量重复或者总人数明显偏少。原因游标参数传递错误或者 is_end 判断逻辑写反了。解决打印每一页的 next 游标和 is_end 值确认游标在递增、is_end 在最后一页变为 true。另外注意有些接口的游标字段名不叫 next 而叫 offset以实际返回为准。5.3 SESSDATA 失效导致空结果现象程序不报错但拉回来的用户列表是空的。原因SESSDATA 过期了接口返回的是未登录状态的数据。解决重新登录 B站从浏览器里复制新的 SESSDATA。注意 SESSDATA 里可能包含特殊字符放进 JSON 时确认没有被转义。5.4 中奖名单出现重复用户现象抽出来的中奖名单里同一个人出现两次。原因去重步骤在抽取之后才执行或者去重用的 HashSet 没有正确比较 Uid。解决确保 Deduplicate 在 Draw 之前调用并且 UserInfo 的 Uid 类型是 long 而不是 string避免「123」和「123 」被当成两个不同的值。5.5 编译报错找不到 System.Text.Json现象dotnet build时报错提示找不到 System.Text.Json 命名空间。原因项目目标框架低于 .NET Core 3.0或者没有引入对应的 NuGet 包。解决把 .csproj 里的 TargetFramework 改成 net6.0 或更高或者手动dotnet add package System.Text.Json。6. 二次开发换抽取策略、加过滤条件和导出结果拿到源码之后最常改的三个地方是抽取策略、过滤规则和结果输出。先说抽取策略。默认用的是 Fisher-Yates 洗牌公平但不够灵活。如果你想加权重——比如给充电用户更高的中奖概率——可以把 Draw 方法改成加权随机。public ListUserInfo DrawWeighted(ListUserInfo pool, int count) { // 假设 UserInfo 有一个 Weight 属性默认 1 var totalWeight pool.Sum(u u.Weight); var selected new ListUserInfo(); var remaining new ListUserInfo(pool); for (int n 0; n count remaining.Count 0; n) { var r _rng.NextDouble() * totalWeight; double cumulative 0; for (int i 0; i remaining.Count; i) { cumulative remaining[i].Weight; if (cumulative r) { selected.Add(remaining[i]); totalWeight - remaining[i].Weight; remaining.RemoveAt(i); break; } } } return selected; }这段代码的逻辑是每次按权重比例选一个选完从池子里移除并更新总权重。Weight 可以从配置文件里读也可以根据用户等级、是否充电等条件动态计算。注意 totalWeight 要同步减去已选中用户的权重否则概率会偏。再说过滤条件。UserFilter 里的 ApplyRules 方法是一个扩展点你可以加各种规则。比如只保留关注了某个账号的用户或者排除最近 7 天新注册的账号。加规则的时候注意顺序——先去重再过滤不然过滤完再去重可能把本该保留的用户误删。最后是结果输出。默认是控制台打印但实际用的时候往往需要导出成 CSV 或 Excel。加一个导出方法就行public void ExportCsv(ListUserInfo winners, string path) { var lines new Liststring { Uid,Nickname }; lines.AddRange(winners.Select(w ${w.Uid},{w.Nickname})); File.WriteAllLines(path, lines); }导出之后用 Excel 打开核对比在控制台里翻要方便得多。如果中奖人数多还可以加一个「备选名单」——多抽几个作为候补万一中奖者联系不上可以顺延。我自己的习惯是每次改完抽取逻辑先用一个固定的随机种子跑一遍确认结果可复现再换成真随机跑正式抽奖。这样万一结果有争议还能用种子复现过程。从那以后我每次动随机相关的代码都强制走一遍「固定种子验证 → 真随机执行」的流程省了不少扯皮。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询