用.NET寫「算命」程式
- 2019 年 10 月 3 日
- 筆記
用.NET寫「算命」程式
「算命」,是一種迷信,我父親那一輩卻執迷不悟,有時深陷其中,有時為求一「上上籤」,甚至不惜重金,向「天神」保佑。我曾看到過有些算命網站,可以根據人的生辰八字,來求得這個人一生的財運、桃花運,如果第一卦算得不好,還可以向「天神」「請願」(充錢),再算一卦,直到達到好運為止。
作為一個深信唯物辯證法的人來說,這些東西當然是不信。
但仔細口味,發現這些東西其中需要有些科學道理。我可以將算命
總結為以下「三要素」:
-
一致性
「命中注定」,因此「算」出來的東西,不管早算還是晚算,什麼時候算,結果應該都一樣。
-
無規律性
「天機不可泄露」,因此輸入相近的姓名等參數,輸出應該相差較遠。「每個人的命運各不相同」,比如狗二和狗三,相差只有一個字,但他們的命運並不一定會幾乎一樣。演算法應該也考慮這一點。
-
個性化
輸入參數應該盡量個性化,不要像
十二生肖
/十二星座
那樣,和性別
做排列組合,只有12x2=24
種結果。否則撞車的人太多,容易露餡?。因此輸入參數必須個性化,最好是姓名
、性別
再加上生辰八字
(出生時間)。 -
可操作性
孜孜不倦的求卦者,可能會「誠心誠意」想求個「上上籤」,因此在一致性的基礎上,必須要加一點點「可操作性」。這個可以當作一個單獨的輸入參數來表示。
如果將算命
當作一個函數,那它的輸入無疑是姓名
、其它個人資訊
和誠心
。,輸出就是一個分數(0
–100
),可以用下圖的程式碼表示:
int destinyScore = f(name, otherPersonalInformation, faith);
下面,我將用.NET
實現這個「算命」的功能。
最簡單的「算命」程式
最初想法
如果只以姓名
作為輸入,那麼這個函數可以簡化為:
int destinyScore = f(name);
這可能就好辦多了,如.NET
中的.GetHashCode()
,即可快速獲取一個字元串的哈希值,這個哈希值應該是固定的(嗎?),該值的取值範圍是int.MinValue
–int.MaxValue
。因此最簡單的辦法,可以先可以通過對100
求模,此時的取值範圍是-99~99
;然後再取絕對值+1即可,程式碼如下:
int GetForturn(string name) { return Math.Abs(name.GetHashCode() % 100) + 1; }
在.NET Framework 4.8
中運行,可以算出我(周傑)的得分固定為15分。
最簡單演算法的缺點-.NET Core
的不一致
在.NET Core
中,這個演算法每次重新運行,算出的結果都不同,因為.NET Core
為了確保安全性,在應用程式啟動時,會隨機生成一個字元串哈希值種子,因此每次exe
運行,哈希值都會變,文檔是這麼說的:
哈希程式碼本身不一定是穩定的。 對於單個版本的 .NET, 相同字元串的哈希程式碼可能跨 .net 實現、跨 .NET 版本和跨 .NET 平台 (如32位和64位) 不同。 在某些情況下, 它們甚至不同於應用程式域。 這意味著, 同一程式的兩次後續運行可能返回不同的哈希程式碼。(源自:https://docs.microsoft.com/zh-cn/dotnet/api/system.string.gethashcode?view=netframework-4.8 )
很顯然,這不符合「一致性」,看來想簡單地通過GetHashCode()
快速「算命」的想法落空了,只能使用標準的哈希演算法。
當然,使用如此簡單的演算法,客戶知道了,可能也不太情願消費更多的「誠意金」了。
哈希演算法
哈希演算法可以給任意長度的字元轉換為一串二進位數組,也就是哈希值。.NET
內置了許多不同的哈希演算法可供選擇:
- 有單純的哈希,如
MD5
、SHA1
之類; - 有「加鹽」的哈希,如
HMACSHA
、HMACSHA256
等; - 有可指定生成長度、可多次迭代、綜合性「加鹽」的哈希,如
Rfc2898DeriveBytes
。
我們要指定一點點「天機」(加鹽),但「天機不可泄露」,因此簡單地MD5
等單純哈希演算法排除;
我們要轉化為一個整數,最大的整數類型,long
/Int64
,為64
位,而最小的內置哈希演算法,MD5
,就已達128
位。因此也要排除HMACSHA
等「加鹽」哈希。當然這些哈希值也可以手動截取部分長度,但安全性是個問號(也受強迫症影響)。
搞過ASP.NET Identity
登錄的都知道裡面用到了Rfc2898DeriveBytes
,它默認為ASP.NET Core
做了10000
次迭代,用多次迭代的方式(而不是引入一個新哈希演算法的方式),確保了安全性。搞對稱加密的時候,有時也用這個類將客戶的密碼轉換為加密演算法的密鑰
(key
),非常有用。
所以最終我們選擇了Rfc2898DeriveBytes
,該演算法可以生成任意指定長度的哈希值。這個類的構造函數要求輸入一個鹽值和迭代次數,在這個示例中我們取一個別人不知道的值(程式碼中寫死了,你們假裝不知道,你們想用這個程式碼時可以改改?)。可以寫出如下程式碼:
int GetForturn(string name) { using (var h = new Rfc2898DeriveBytes(name, salt: new byte[8] { 44, 2, 3, 4, 5, 6, 7, 8}, iterations: 10086)) { return (int)(BitConverter.ToUInt64(h.GetBytes(8), 0) % 100) + 1; }; }
可見算出一卦80分以上的「上籤」,已經非常不容易了。我從網上自動生成了888個姓名,然後調用該函數,發現得分超過90
分「上上籤」標準的,只有83個,相同於十分之一,符合分布特點(詳情見Github
上的程式碼)。
通過以下程式碼,可以算出「狗二」是48分,「狗三」是96分,可見一字之差相差甚遠:
GetForturn("狗二").Dump(); // 48 GetForturn("狗三").Dump(); // 96
完整演算法
最後,依葫蘆畫瓢,加上個人資訊參數(生日)和「誠意金次數」,完成最後的演算法:
int GetForturn(string name, DateTime birthDay, int faithCount) { using (var h = new Rfc2898DeriveBytes(name + birthDay + faithCount, salt: new byte[8] { 44, 2, 3, 4, 5, 6, 7, 8 }, iterations: 10086)) { return (int)(BitConverter.ToUInt64(h.GetBytes(8), 0) % 100) + 1; }; }
然後又是「狗二」和「狗三」,加上他們的生日參數後,默認他們的得分是95分和3分:
GetForturn("狗二", new DateTime(1994, 5, 17), 0).Dump(); // 95 GetForturn("狗三", new DateTime(1996, 11, 3), 0).Dump(); // 3
但狗三經過1次「誠意金」後,也求得了高達99分以上的「上上籤」:
GetForturn("狗二", new DateTime(1994, 5, 17), 0).Dump(); // 98 GetForturn("狗三", new DateTime(1996, 11, 3), ).Dump(); // 99
最後的話
Rfc2898DeriveBytes
非常有用,本文說了Rfc2898DeriveBytes
的一種使用場景,相信各位在工作當時也經常會有機會去接觸它。
我將上述功能做了一個頁面,願博君一笑:https://destiny.starworks.cc/
本文所用程式碼下載地址:https://github.com/sdcb/blog-data/tree/master/2019/20190905-fortune-with-dotnet
請關注我的微信公眾號:【DotNet騷操作】,