 B站動態(tài)抽獎組件:從接口調(diào)用到隨機抽取全流程)
簡介BiliRaffle 是一款面向 C# 開發(fā)者與 B 站工具愛好者的動態(tài)抽獎組件基于 bilibili-api 實現(xiàn)可幫助用戶快速搭建 B 站動態(tài)抽獎的自動化流程適合具備一定 .NET 桌面開發(fā)基礎(chǔ)、希望研究抽獎邏輯或二次開發(fā)的技術(shù)人員。資源包共 32 個文件以 12 個 cs 源碼文件與 4 個 xaml 界面文件為核心輔以 config、xml、dll、sln、csproj 等工程配置與依賴文件另有 png、ico、resx 等資源文件整體約 60KB結(jié)構(gòu)緊湊、便于閱讀與編譯。項目采用 .NET Framework 4.5 及以上版本解壓后運行 BiliRaffle.exe 即可使用源碼涵蓋登錄窗口、主窗口、ViewModel、抽獎邏輯與 MVVM 輔助類等模塊目錄組織清晰。目前已有 697 人學(xué)習(xí)下載讀者可借此理解 B 站動態(tài)抽獎的接口調(diào)用、界面綁定與業(yè)務(wù)分層思路并在此基礎(chǔ)上進(jìn)行功能擴展或排錯調(diào)試。1. 抽獎工具還能這么寫一個 C# 組件把動態(tài)抽獎流程全包了刷到一條動態(tài)抽獎評論區(qū)幾千條手動翻不現(xiàn)實。BiliRaffle 就是沖著這個場景來的——它是一個用 C# 寫的 B站動態(tài)抽獎組件核心能力是拉取指定動態(tài)下的轉(zhuǎn)發(fā)/評論用戶列表按規(guī)則去重、過濾最后隨機抽出中獎?wù)?。適合兩類人一是想自己搭個抽獎工具但不想從零調(diào)接口的開發(fā)者二是拿它當(dāng) bilibili-api 的 C# 封裝參考來學(xué)。整個項目圍繞 bilibili-api 的接口調(diào)用展開把鑒權(quán)、分頁拉取、數(shù)據(jù)清洗、隨機抽取串成了一條完整鏈路。你拿到源碼后改幾行配置就能跑也能按自己的業(yè)務(wù)邏輯替換抽取策略。2. 項目結(jié)構(gòu)與技術(shù)選型C# 怎么跟 bilibili-api 對接2.1 目錄骨架與職責(zé)劃分拿到源碼先別急著跑花兩分鐘把目錄掃一遍后面改代碼能省不少時間。BiliRaffle 的結(jié)構(gòu)是典型的 C# 類庫 控制臺入口的組合常見做法是拆成三層接口層、業(yè)務(wù)層、入口層。接口層負(fù)責(zé)跟 bilibili-api 打交道封裝 HTTP 請求和 Cookie 鑒權(quán)業(yè)務(wù)層處理抽獎邏輯包括去重、過濾、隨機算法入口層就是一個 Program.cs讀配置、調(diào)業(yè)務(wù)、輸出結(jié)果。一個合理的目錄大概長這樣BiliRaffle/ ├── BiliRaffle.Core/ │ ├── Api/ │ │ ├── BiliClient.cs # HTTP 客戶端封裝 │ │ └── DynamicApi.cs # 動態(tài)相關(guān)接口 │ ├── Models/ │ │ ├── UserInfo.cs # 用戶數(shù)據(jù)模型 │ │ └── RaffleConfig.cs # 抽獎配置模型 │ ├── Services/ │ │ ├── RaffleService.cs # 抽獎核心邏輯 │ │ └── UserFilter.cs # 用戶過濾與去重 │ └── Utils/ │ └── RandomHelper.cs # 隨機抽取工具 ├── BiliRaffle.Console/ │ └── Program.cs # 控制臺入口 └── BiliRaffle.sln這個劃分的好處是你想換抽取算法只動 Services想換接口版本只動 Api互不干擾。我一般會把配置項單獨抽出來不硬編碼在代碼里后面會講怎么改。2.2 技術(shù)棧與依賴說明項目用的是 .NET 6 或以上語言是 C#。為什么選 C# 而不是 Python一個現(xiàn)實原因是 bilibili-api 在 C# 生態(tài)里有現(xiàn)成的 HttpClient 封裝方案而且 C# 的強類型模型在處理接口返回的嵌套 JSON 時更省心——字段對不上編譯期就報錯不用等到運行時才發(fā)現(xiàn) KeyError。核心依賴通常包括依賴用途備注System.Net.HttpHTTP 請求.NET 內(nèi)置System.Text.JsonJSON 反序列化.NET 內(nèi)置也可換 Newtonsoft.JsonMicrosoft.Extensions.Configuration讀配置文件可選簡單項目直接讀 JSON 也行如果你習(xí)慣用 Newtonsoft.Json把反序列化那幾行換掉就行模型類不用動。這里沒有引入太重的框架整個項目保持輕量編譯出來就是一個 exe扔到服務(wù)器上定時跑也沒問題。2.3 鑒權(quán)與請求封裝B站接口繞不開鑒權(quán)。BiliRaffle 的鑒權(quán)方式是在請求頭里帶 Cookie核心是 SESSDATA 和 bili_jct 這兩個值。SESSDATA 用來標(biāo)識登錄態(tài)bili_jct 用于部分寫操作抽獎本身是讀操作但有些接口會校驗。public class BiliClient { private readonly HttpClient _http; private readonly string _sessData; public BiliClient(string sessData) { _sessData sessData; _http new HttpClient(); // 常見做法統(tǒng)一設(shè)置 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(); } }這段代碼的關(guān)鍵點有三個一是 UA 和 Referer 必須帶不帶的話接口大概率返回 -412 風(fēng)控錯誤這是血淚經(jīng)驗二是 Cookie 拼接格式要對SESSDATA 后面直接跟值不要多加空格三是 EnsureSuccessStatusCode 會在 HTTP 層報錯時拋異常方便定位問題。參數(shù)方面sessData 從瀏覽器登錄后的 Cookie 里復(fù)制注意不要泄露給別人。3. 核心模塊拆解從拉取用戶到抽出中獎?wù)叩耐暾溌?.1 動態(tài)評論/轉(zhuǎn)發(fā)列表分頁拉取抽獎的數(shù)據(jù)來源是動態(tài)下的轉(zhuǎn)發(fā)和評論。B站接口是分頁返回的一頁最多 20 條左右所以必須循環(huán)拉取直到?jīng)]有下一頁。這里最容易翻車的地方是分頁參數(shù)和去重——接口返回的游標(biāo)next_offset要正確傳遞否則會重復(fù)拉同一頁。public async TaskListUserInfo FetchAllUsers(string dynamicId) { var users new ListUserInfo(); string offset ; bool hasMore true; while (hasMore) { // 構(gòu)造分頁請求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 }); } // 更新游標(biāo)判斷是否還有下一頁 offset result.Data.Cursor?.Next.ToString() ?? ; hasMore result.Data.Cursor?.IsEnd false !string.IsNullOrEmpty(offset); // 加延遲避免請求過快觸發(fā)風(fēng)控 await Task.Delay(800); } return users; }邏輯說明type11 表示動態(tài)類型oid 是動態(tài) IDmode3 是按時間排序。每次循環(huán)把當(dāng)前頁的用戶加進(jìn)列表然后從 Cursor 里取下一頁游標(biāo)。is_end 為 true 或者游標(biāo)為空就停止。參數(shù)方面dynamicId 從動態(tài)鏈接里取比如 t.bilibili.com/xxxxx 里的數(shù)字部分。那個 Task.Delay(800) 不是可有可無的請求太快會被限流800ms 是我試過比較穩(wěn)的間隔你可以根據(jù)實際情況調(diào)到 1000ms 以上。3.2 用戶去重與過濾規(guī)則拉回來的用戶列表里會有重復(fù)——同一個人可能轉(zhuǎn)發(fā)多次也可能既轉(zhuǎn)發(fā)又評論。去重是抽獎公平性的基礎(chǔ)不去重的話活躍用戶中獎概率會被放大。除了去重通常還要過濾掉一些賬號比如官方賬號、已注銷賬號、黑名單用戶。public class UserFilter { public ListUserInfo Deduplicate(ListUserInfo raw) { // 按 Uid 去重保留第一個出現(xiàn)的記錄 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時間復(fù)雜度 O(n)幾千條數(shù)據(jù)毫秒級完成。ApplyRules 里可以按需擴展比如加一個「關(guān)注時間早于某天」的過濾條件或者「等級大于 2」的門檻。config.ExcludeUids 是一個配置項把不想?yún)⑴c抽獎的 UID 填進(jìn)去就行。注意 Nickname 為空的賬號通常是已注銷或異常號直接過濾掉比較穩(wěn)妥。3.3 隨機抽取與結(jié)果輸出抽取環(huán)節(jié)看起來簡單但隨機數(shù)的質(zhì)量直接影響公平性。用 Random 類就夠了但要注意不要每次 new 一個 Random否則在快速連續(xù)調(diào)用時可能產(chǎn)生相同種子。正確做法是全局共享一個 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; // 人數(shù)不夠全部中獎 // 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 洗牌的好處是每個元素被選中的概率嚴(yán)格相等不會因為取前 N 個而偏向列表前面的用戶。參數(shù) count 是中獎人數(shù)從配置里讀。如果 pool 里的人數(shù)少于 count直接全部返回這個邊界情況要處理不然會拋異常。輸出結(jié)果時把 Uid 和昵稱一起打出來方便核對。4. 跑起來配置、編譯與運行步驟4.1 環(huán)境準(zhǔn)備與依賴還原跑這個項目之前確認(rèn)機器上裝了 .NET SDK 6.0 或更高版本。命令行敲dotnet --version能看到版本號就行。如果沒裝去微軟官方下載安裝包一路下一步即可。裝完之后進(jìn)入項目根目錄執(zhí)行依賴還原cd BiliRaffle dotnet restore這一步會根據(jù) .csproj 文件里的 PackageReference 拉取 NuGet 包。如果網(wǎng)絡(luò)慢可以配一個國內(nèi)鏡像源在NuGet.config里加一行就行。還原成功后dotnet build編譯一遍確認(rèn)沒有報錯。4.2 配置文件怎么寫項目通常用一個appsettings.json或config.json來管理參數(shù)不要把 SESSDATA 硬編碼在代碼里。一個典型的配置文件長這樣{ SessData: 你的SESSDATA值, DynamicId: 動態(tài)ID數(shù)字, WinnerCount: 3, ExcludeUids: [123456, 789012], RequestDelayMs: 800 }SessData 從瀏覽器開發(fā)者工具的 Application → Cookies 里復(fù)制注意不要帶多余空格。DynamicId 是動態(tài)鏈接里的數(shù)字。WinnerCount 是中獎人數(shù)。ExcludeUids 是排除列表沒有就留空數(shù)組。RequestDelayMs 控制請求間隔網(wǎng)絡(luò)好的話可以調(diào)到 500但別低于 300否則容易觸發(fā)風(fēng)控。4.3 運行與結(jié)果驗證配置填好后直接運行dotnet run --project BiliRaffle.Console控制臺會先打印拉取進(jìn)度比如「已拉取 200 條繼續(xù)…」拉完之后輸出去重后的總?cè)藬?shù)最后打印中獎名單。驗證結(jié)果是否合理可以看兩個地方一是總?cè)藬?shù)跟動態(tài)實際轉(zhuǎn)發(fā)評論數(shù)是否大致吻合差太多說明分頁沒拉全二是中獎名單里有沒有重復(fù) UID有的話說明去重邏輯沒生效。如果中途報 -412 或 -509基本是請求太快或者 Cookie 失效換一個 SESSDATA 再試。5. 避坑與常見問題排查5.1 接口返回 -412 風(fēng)控錯誤現(xiàn)象程序跑幾頁之后突然拋異常接口返回碼 -412。原因請求頻率過高或者 UA/Referer 沒帶對。解決把 RequestDelayMs 調(diào)到 1000 以上確認(rèn) HttpClient 的 DefaultRequestHeaders 里 UA 和 Referer 都設(shè)置了。如果還是不行換一個網(wǎng)絡(luò)環(huán)境試試有些 IP 段被風(fēng)控得更嚴(yán)。5.2 分頁拉取重復(fù)或遺漏現(xiàn)象拉回來的用戶列表里有大量重復(fù)或者總?cè)藬?shù)明顯偏少。原因游標(biāo)參數(shù)傳遞錯誤或者 is_end 判斷邏輯寫反了。解決打印每一頁的 next 游標(biāo)和 is_end 值確認(rèn)游標(biāo)在遞增、is_end 在最后一頁變?yōu)?true。另外注意有些接口的游標(biāo)字段名不叫 next 而叫 offset以實際返回為準(zhǔn)。5.3 SESSDATA 失效導(dǎo)致空結(jié)果現(xiàn)象程序不報錯但拉回來的用戶列表是空的。原因SESSDATA 過期了接口返回的是未登錄狀態(tài)的數(shù)據(jù)。解決重新登錄 B站從瀏覽器里復(fù)制新的 SESSDATA。注意 SESSDATA 里可能包含特殊字符放進(jìn) JSON 時確認(rèn)沒有被轉(zhuǎn)義。5.4 中獎名單出現(xiàn)重復(fù)用戶現(xiàn)象抽出來的中獎名單里同一個人出現(xiàn)兩次。原因去重步驟在抽取之后才執(zhí)行或者去重用的 HashSet 沒有正確比較 Uid。解決確保 Deduplicate 在 Draw 之前調(diào)用并且 UserInfo 的 Uid 類型是 long 而不是 string避免「123」和「123 」被當(dāng)成兩個不同的值。5.5 編譯報錯找不到 System.Text.Json現(xiàn)象dotnet build時報錯提示找不到 System.Text.Json 命名空間。原因項目目標(biāo)框架低于 .NET Core 3.0或者沒有引入對應(yīng)的 NuGet 包。解決把 .csproj 里的 TargetFramework 改成 net6.0 或更高或者手動dotnet add package System.Text.Json。6. 二次開發(fā)換抽取策略、加過濾條件和導(dǎo)出結(jié)果拿到源碼之后最常改的三個地方是抽取策略、過濾規(guī)則和結(jié)果輸出。先說抽取策略。默認(rèn)用的是 Fisher-Yates 洗牌公平但不夠靈活。如果你想加權(quán)重——比如給充電用戶更高的中獎概率——可以把 Draw 方法改成加權(quán)隨機。public ListUserInfo DrawWeighted(ListUserInfo pool, int count) { // 假設(shè) UserInfo 有一個 Weight 屬性默認(rèn) 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; }這段代碼的邏輯是每次按權(quán)重比例選一個選完從池子里移除并更新總權(quán)重。Weight 可以從配置文件里讀也可以根據(jù)用戶等級、是否充電等條件動態(tài)計算。注意 totalWeight 要同步減去已選中用戶的權(quán)重否則概率會偏。再說過濾條件。UserFilter 里的 ApplyRules 方法是一個擴展點你可以加各種規(guī)則。比如只保留關(guān)注了某個賬號的用戶或者排除最近 7 天新注冊的賬號。加規(guī)則的時候注意順序——先去重再過濾不然過濾完再去重可能把本該保留的用戶誤刪。最后是結(jié)果輸出。默認(rèn)是控制臺打印但實際用的時候往往需要導(dǎo)出成 CSV 或 Excel。加一個導(dǎo)出方法就行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); }導(dǎo)出之后用 Excel 打開核對比在控制臺里翻要方便得多。如果中獎人數(shù)多還可以加一個「備選名單」——多抽幾個作為候補萬一中獎?wù)呗?lián)系不上可以順延。我自己的習(xí)慣是每次改完抽取邏輯先用一個固定的隨機種子跑一遍確認(rèn)結(jié)果可復(fù)現(xiàn)再換成真隨機跑正式抽獎。這樣萬一結(jié)果有爭議還能用種子復(fù)現(xiàn)過程。從那以后我每次動隨機相關(guān)的代碼都強制走一遍「固定種子驗證 → 真隨機執(zhí)行」的流程省了不少扯皮。希望幫到你。本文還有配套的精品資源點擊獲取