故事要從八年前這篇漫談尾牙抽獎程式的公正性說起,我一向支持讓抽獎程式開源,以擺脫長久以來電腦抽獎常被人指責「黑箱」「作弊」卻百口莫辯的悲情。我認為「演算法及程式碼公開,以具公信力且無法操控的方式決定亂數種子,輸入亂數函數產生可預測的亂數決定抽獎結果,並確保演算結果能被反覆驗證」是面對不公質疑的終極解決方案。

這陣子周董演唱會黃牛搶票財政部雲端發票非隨機式抽獎屢屢佔據新聞版面,而我也趁亂四處力推「開源抽獎程式」,都快把自己搞成「抽獎程式開源策進會」的地下理事長。(笑)

我心中有個超簡單的完美解法:用台股收盤股價成交量資料算雜湊當成亂數種子,亂數演算法用 SHA256 迭代計算便能產生源源不斷的可預期亂數來決定中獎者,種子公開且難以被操縱,演算法也公開且不難理解(對於資訊背景朋友來說屬常識等級),幾番討論下來,幾乎沒發現什麼破綻。但,對於真想用它解決抽獎問題的朋友而言:講得天花亂墜,沒程式你說個雞霸?

程式這不就來了嗎?

先說程式怎麼使用好了。程式是用 PowerShell 寫的,不需編譯即可執行,用文字編譯器(如 Notepad 記事本/nano)能看到所有程式碼,程式碼及執行邏輯完全公開。Windows 作業系統已內建 PowerShell,OpenLuckDraw.ps1 檔案下載回去就可以直接跑。若是 Linux 或 macOS,可參考在 Windows、Linux 和 macOS 上安裝 PowerShell的說明下載安裝,PowerShell 不難學又可以做很多事,說不定你會愛上它。

抽獎名單請轉成 CSV,這裡有個範例:(請存成 UTF-8 編碼)

我去抓了 2023 台北馬半馬成績清單當範例,第一列是欄位名稱,第二列起是資料,欄位間以「,」分隔,理論上欄位不需多,一般只要編號及姓名,甚至改用系統內部序號(報名序號、收執聯序號)或用雜湊取代姓名 Email(避免洩漏個資),能取信於大眾可且能識別中獎者就行。

上面說的 RunnerList.csv 範例及 OpenLuckyDraw.ps1 程式可從 Github 下載

OpenLuckDraw.ps1 會自動去證交所網站抓最新一筆當日收盤結算的成交資料,由它來決定開獎結果,讓成千上萬的法人、自營商、外資跟散戶一起決定獎落誰家:

程式將日期、成交股數、成交金額、成交筆數、發行量加權股價指數、漲跌點數等欄位組合成字串,進行 SHA256 雜湊計算得到 32 位元組長度的數字當成亂數種子,用來為抽獎名單的每個參加者產生一個亂數作為排序優先依據,之後依這個優先值由大到小排序,看你要取幾位得獎者,就抓前幾筆。(預設抓十筆,可輸入參數調整)

因此,若抽獎名單己事先公佈,證交所公佈數字的一刻抽獎結果便已底定,任何人都能跑出相同的抽獎結果,連抽獎儀式都可省了。

如果要解釋抽獎原理給完全不懂技術的人聽,有個簡單說法:

這個中獎名單是用今天台股的成交統計依據一套固定規則去算出來的,不誰來算都是一樣的結果

應用上有個小問題:若用在尾牙抽獎,名單早早確定但晚上才抽獎,下午兩點半就知道誰中誰沒中多無趣?為了保有長官或嘉賓擔任抽獎人定生死這個意義非凡的橋段,程式有個 ExtraSeed 參數,可透過摸彩球、轉輪盤、抽樸克牌... 等大家公認隨機的方式再加入額外種子,由台股成交資料產生的主要種子(DrawSeed)跟額外種子共同決定最終名單。台股種子己確保公正的隨機性,其上可任意疊加傳統抽獎行為增加變化,既不破壞公正性又保有儀式感,二者兼顧。

但要注意若彩球有 100 顆,等同讓長官由 100 組結果中選擇一個,若名單己提早公佈,在證交所公告資料後,這 100 種結局便可被事先預測。要是有人算出來他在 100 種結局中沒有一個會中獎,豈不頓時陷入黑暗,連尾牙都沒心情吃了?

最簡單的解法是 CSV 名單不要事前公開,以免開獎結果被提前預測。亂數種子公佈前只提供 CSV 檔 SHA256 雜湊,抽獎程式都會顯示 CSV 檔雜湊可核對版本,事後如遇爭議,釋出 CSV 檔給大家驗證及重跑結果,便足以證明檔案未經變造。

剩下一個弱點,以抽 100 個彩球為例,有人可能會攻擊主辦方提前知道 100 種有限的可能結果,不肖人士仍有空間動手腳。若互不信任真嚴重至此,解法很多,請長官說幾句吉祥話用文字當種子、現場拍照存 .jpg 算雜湊... 有數不盡的做法,只要抽獎儀式能產生夠大的變化組合且能事後驗證就好,大家可自行發揮創意。

另一個常見情境是,要分別抽出三獎 5 名、二獎 2 名,頭獎 1 名,此時可利用前面介紹過的 ExtraSeed 參數,每次指定不同 -ExtraSeed 及 -Top 便可以陸續抽出不同獎項。至於慣例上己得獎者不能再抽其他獎,故有個 -RemoveWinners 參數,能將己中獎者自 CSV 移除(修改前原 CSV 會備份存查)。使用 -RemoveWinners 時由於己抽中者不會再出現,-ExtraSeed 可加可不加,直接用台股種子抽到底亦可。

相信以上這些設計應可滿足大部分尾牙之類的抽獎應用。

為防止爭議,程式執行的所有輸出訊息會全程側錄,寫入 Logs\LuckyDraw-yyyyMMddHHmmss.log,遇到爭議時,記錄檔中的最終亂數種子源[1]可用來重現抽獎結果(例如:./OpenLuckyDraw.ps1 RunnerList.csv -Top 5 -DrawSeed "113/11/06|8,013,368,499|409,279,637,442|2,608,905|23,217.38|110.58",而檔案 SHA256 檢核[2]則用來證明抽獎名單 CSV 沒被修改過,有了這些證明,即便面對最嚴格的放大檢驗也可安穩過關。

以下是程式指令及參數範例:

  1. 使用台股當日(或最新)成交統計作為亂數種子抽出前 10 名
    ./OpenLuckyDraw.ps1 RunnerList.csv 
    
  2. 使用指定亂數種子抽出前 5 名
    ./OpenLuckyDraw.ps1 RunnerList.csv -Top 5 -DrawSeed "113/11/06|8,013,368,499|409,279,637,442|2,608,905|23,217.38|110.58"
    
  3. 使用指定亂數種子及抽樣檢查種子抽出前 10 名 (驗證)
    ./OpenLuckyDraw.ps1 RunnerList.csv -SampleSeed "2021-11-06 12:34:56" -DrawSeed "113/11/06|8,013,368,499|409,279,637,442|2,608,905|23,217.38|110.58"
    
  4. 使用台股當日(或最新)成交統計作為亂數種子抽出前 10 名,並顯示指定欄位
    ./OpenLuckyDraw.ps1 RunnerList.csv -DisplayFields Name,Bibnr,Category
    
  5. 使用指定亂數種子抽出前 5 名,並將幸運兒自CSV名單移除 (原 CSV 檔案會備份後覆寫)
    ./OpenLuckyDraw.ps1 RunnerList.csv -Top 5 -DrawSeed "113/11/06|8,013,368,499|409,279,637,442|2,608,905|23,217.38|110.58" -RemoveWinners
    
  6. 使用主亂數種子源及附加亂數種子源分別抽出 5 名三獎, 3 名二獎, 1 名頭獎(允許重複中獎)
    ./OpenLuckyDraw.ps1 RunnerList.csv -Top 5 -DrawSeed "<主要種子>" -ExtraSeed "<三獎附加種子>"
    ./OpenLuckyDraw.ps1 RunnerList.csv -Top 3 -DrawSeed "<主要種子>" -ExtraSeed "<二獎附加種子>"
    ./OpenLuckyDraw.ps1 RunnerList.csv -Top 1 -DrawSeed "<主要種子>" -ExtraSeed "<頭獎附加種子>"
    
  7. 使用台股當日(或最新)成交統計亂數種子及附加亂數種子源分別抽出 5 名三獎, 3 名二獎, 1 名頭獎,得獎者不參加後續抽獎
    做法為每次抽獎後將中獎者自 CSV 名單移除 (原 CSV 檔案備份後覆寫)
    ./OpenLuckyDraw.ps1 RunnerList.csv -Top 5 -RemoveWinners
    ./OpenLuckyDraw.ps1 RunnerList.csv -Top 3 -RemoveWinners
    ./OpenLuckyDraw.ps1 RunnerList.csv -Top 1 -RemoveWinners
    

我把專案上傳到 Github 了,想動手試試的朋友,可以到 Release 區下載 OpenLuckyDraw.ps1 及 RunnerList.csv 範例回去玩。有問題或建議可留言或發 Issue。希望這個小專案能讓大家更接受「抽獎程式本來就該開源」這件事。

【程式原理簡介】

最後這段寫給技術人員看,簡單介紹程式運作原理。

程式邏輯不難,不需要什麼高級語言或開發工具,我挑了自己順手的 PowerShell,Windows 免安裝,Linux 跟 macOS 下載一下就有 (在 Windows、Linux 和 macOS 上安裝 PowerShell),跨平台使用沒什麼問題。(下圖為 Linux 測試結果)

亂數種子是從證交所的每日市場成交資訊抓每天盤後算出來的成交資料統計,將日期、成交股數、成交金額、成交筆數、發行量加權股價指數、漲跌點數組成字串計算 SHA256 雜湊($RandomSeed),取前 16 Bytes 得到 Unsigned Long 整數產生亂數($CurrentRandomNum),接著用 $RandomSeed + $CurrentRandomNum 計算 SHA256 取前 16 Bytes 得到下一個亂數並存入 $CurrentRandomNum,用這個演算法使可依種子產生源源不絕的亂數,且只要種子相同,產生的一連串亂數的順序與數值便完全相同。

抽獎時會先匯入 CSV 清單,使用上述亂數產生規則逐一為每筆資料加上 SortPriority 排序值,之後依 SortPriority 因大到小排序,取出前 N 名,就完成了抽獎。依此機制,只要記錄下抽奬時採用的亂數種子,便可無限次在不同機器上重現結果,將種子跟名單交給任何人都會跑出相同的結果,甚至可提交給檢調做為證據,這樣誰還能指控程式有問題、主辦方作弊?皆大歡喜。

另外,我在匯入 CSV 清單後加了一個用亂數抽樣幾筆顯示參加者資訊的環節,加減證明名單一致。(抽樣參加者屬表演性質,是給一般大眾看的,真正的檢核點程式顯示的 CSV 檔 SHA256 雜湊,它可以保證檔案沒有半個字元被動過)

中獎名單以表格方式呈現,顯示欄位預設會抓 CSV 的前兩欄(最常見的是員編、姓名),但可透過 -DisplayFields Name,Bibnr,Category 參數指定。

程式執行時顯示的所有文字將全程側錄寫入 Logs 目錄下的 LuckyDraw-yyyyMMddHHmmss.log 檔,請保存好此檔案及原始 CSV,未來如遭任何人質疑,輸入當時種子重跑一次程式即可自證清白。

This post discusses the importance of transparency in lottery programs and introduces a PowerShell script, OpenLuckyDraw.ps1, that uses stock market closing data as a random seed for fair and verifiable draws. The program ensures reproducibility and integrity by making the algorithm and code open-source. Detailed usage instructions and examples are provided for various scenarios, including additional seed parameters for extra randomness.


Comments

# by 小雞

沒有加權不符規範

# by yoyo

證交所資料也許可以用OpenAPI取得 https://openapi.twse.com.tw/v1/exchangeReport/FMTQIK

# by danny50610

亂數或許可以考慮 drand,分散式亂數產生,也有簽章 指定未來時間來抽

# by 小黑

這就對了

# by Jim

抓不到技術上問題 抓到個錯字“下國為 Linux 測試結果”

# by Jeffrey

to yoyo, 好主意,OpenAPI 更可靠,不易因網頁改版異動規格,晚上來改版。感謝分享!

# by Jeffrey

to danny50610,感謝分享,好東西,學到了。但如果要面對大眾,我覺得還是「抽獎結果是用今天台股的交易統計數字算出來的」更容易被理解。

# by Jeffrey

to Jim, 呼~ 幸好是錯字 (XD) 感謝指正。

# by Jeffrey

to 小雞,有加權哦 - SortPriority 欄位就是加權,所有台股投資人一起決定的加權,覺得不好可以找他們去理論。(誤)

# by

對於某事件來說, 主辦單位現在在思考的應該不是怎麼抽會更公正, 而是下一次怎麼樣內定不會被抓 XD

# by 野比小熊

你已經不是綠色夥伴,是敵人(X

# by Jeffrey

to yoyo, 試改了一個版本,但馬上發現問題 - OpenAPI 版沒有當日資料。 https://github.com/darkthread/OpenLuckyDraw/tree/OpenAPI

# by yoyo

證交所 OpenAPI 資料行為竟然跟網頁不一致嗎? 感覺不合理XD

# by 永遠恥笑powershell

等待複核中,留言將在稍後顯示 / The comment is awaiting review.

# by Jeffrey

to 永遠恥笑powershell, YES! Python 版出現了,感謝分享。(比我預期的晚一些) (不常接觸 Windows 或到哪都能隨身攜帶兵器的同學,比較難體會 PowerShell 的美妙,有過流落荒島靠地上樹枝擊退猛獸的經驗,應很快會改觀,哈)

# by 永遠恥笑powershell

不該吐糟是新版本的excel,己用上python接替vba,屬輕屬重自行領會.該吐糟則是C#和powershell作為VB(包含VBA和VBS)的官配替代,是否標準庫得先滿足VB的所有功能呢?離C#和powershell去python,也屬無耐之舉吧

# by Jeffrey

to 永遠恥笑powershell,Python 在資料科學領域已強大到近乎無敵,與其窮盡公司之力去追趕全球社群累積的成果,不如站在它的肩膀上。我覺得 Excel 支援 Python 是聰明之舉,使用者不必重學一套新語言,用現成資料處理及繪圖技能,搭配 Excel 技巧一起做出瀨尿牛丸,有什麼不好? 只有 PowerShell 可用的場合就用 PowerShell,能用 Python 又佔優勢時就用 Python,應該不用弄到漢賊不兩立?哈

# by Eddie

用真隨機數(硬體亂數)不是更方便,誰來搖獎次次都不同 https://pkg.go.dev/crypto/rand

# by Jeffrey

to Eddie,真隨機數方便歸方便,缺點是主辦方難以自證清白。 可以試想一下,當被不講理的人質疑程式動手腳(給的原始碼跟跑的程式不是同一支、呼叫的 API 網址被人攔截換掉... ),你如何拿出證明讓對方啞口無言?(證明有外星人比較簡單,證明沒外星人難上十萬倍) 這套做法最美妙的地方在於,面對質疑時:程式跟清單在這裡,不信你自已跑看看,不會跑請你相信的人跑給你,看結果是不是一模一樣 ?這不是簡單多了。

# by 永遠恥笑powershell

主要是作為程式實現,powershell並不如C#那麼直觀,效能更加是一個慘字,現時powershell調度.net(可理解成一個C#),是powershell>>調度C#的實現>>再從C#>調度C的實現,這很難快得了(其實Python調度.net如是...),至於Python ,它的Python shell功能有多慘我想大家也知的,所以個人而言powershell老實當回一個shell就好,Python則是做程式的做去做程式,總想一魚兩吃只會落個空.

# by yoyo

to 永遠恥笑powershell, 再從C#>調度C的實現 沒聽說過C#會call C,請問是哪邊的資訊? 另外,不是所有情境都需要效能,開發方便好維護重要的多 需要極致效能,請找C/C++ XD 每個語言有各自擅長的情境

# by 永遠恥笑powershell

to yoyo C# 的執行環境 CLR 本身是用 C++ 寫成,所以會提起C#>調度C python我可用cython轉成C來跑,何必死磨在C#/Powershell呢? 同意,每個語言有各自擅長的情境,所以說勉強無幸福

# by Clayblockunova

其他證交所(如紐約,倫敦,東京,大陸滬深)可以嗎?

# by Clayblockunova

還有,什麼是說個陰勁?

# by Woody

Github release 那邊的 說明文件 連結錯了 /blogs/ X /blog/ O

# by Jeffrey

to Woody, 感謝,已修正。

Post a comment