天天看點

“秒殺”心得

  本文記錄對某網站a的秒殺活動編寫秒殺器的經曆和技術重點。

故事回顧

    某日早上,朋友給我說最近a網站在開展秒殺活動,有ipad、iphone,讓大家一起去秒殺。結果我們四個人一起秒,都沒有别人快,沒有一個人秒到。然後下午我就開始嘗試分析它網站的秒殺流程,并嘗試使用自動送出資料的方案來進行秒殺。結果,在晚上的時候,成功做出了第一個版本的秒殺器,然後我們一起秒殺了幾個ipad(大家都想要ipad,而對iphone沒興趣,汗)。

    當時就用網銀付了帳,等待它發貨。接下來我們每個人一個接一個地接到了a網站打來的電話,确定我們是不是作弊了,哈哈,我們當然打死不會承認了~

    過了半個來月,該網站又發起了新一輪的秒殺活動,但是由于之前發現有許多人作弊,是以這次全面更改了網站的流程,随機出現各種題目讓會員回答,回答成功才能繼續秒殺。這樣,難度就大了些,一開始以為它們是題庫,後來發現原來所有的題目都是自動生成的。元旦也沒閑着,花了幾天時間,改出了第二個版本的秒殺器,智能解題。經測試,目前沒有失敗過。

第一版本

    以下簡明扼要地描述所有的分析流程:

    分析網站秒殺流程,得出“入口頁面”的位址。但是嘗試登入此頁面失敗,傳回活動等待頁面,并提示:“活動未開始”。

    寫了一個簡單的控制台程式,在活動開始時立刻運作此程式,快速地打開了20-40個入口頁面。此時,發現有一半左右的頁面進入成功,到達“送出頁面”。送出頁面中需要填寫一些必要的個人資訊,最下面是一個送出按鈕。估計這是a網站秒殺的最後一道關。

    把送出頁面的用戶端源代碼全部儲存下來,嘗試進行分析。發現表單中需要填寫的是:一些固定資訊、一些隐藏域(hiddenfield)、圖檔驗證碼。

    隐藏域中需要送出一些如:當時秒殺活動id、使用者id等的資訊。這些資訊隻要在網站中多分析一下就能得出。

    驗證碼:這個目前并沒有什麼好的辦法能自動識别驗證碼,網上雖然有此類程式,不過我懶得去下載下傳了,直接把驗證碼的圖檔顯示在程式中,人工錄入就好了。這樣做的原因是,以我的經驗,他們的驗證碼十有八九存儲在服務端session中,也有可能是用戶端cookie中,也就是說,驗證碼是可以提前擷取的,并不一定需要等等活動開始後再擷取。是以隻要在臨近活動開始的前2分鐘擷取并錄入驗證碼就行了。

    這樣,所有的資料都準備好了,接下來就是如何讓程式自動填寫資料并送出到網站上。這是重點,也是難點。如果純粹使用背景代碼模拟送出的話,就需要保證背景代碼擁有已經被網站驗證通過後的cookie。之前我做過類似的送出程式,但是準備假cookie的工作一直沒有成功過,也比較麻煩。由于這次時間比較緊,沒法再試驗這種純正的方案。是以靜下心來想别的方案。後來靈機一動決定使用控制浏覽器的方案來試試:在秒殺程式中嵌入一個浏覽器,在浏覽器中執行登入操作。這樣,登入成功後的cookie,就由浏覽器自己來維護,而我要做的就是控制浏覽器中頁面的運作,讓它以我的方式加載頁面、填寫資料、送出資料。在送出資料時,浏覽器也會自動把cookie一并送出。這樣就可以在登入的狀态下,把前面準備好的資料直接自動送出給伺服器。

    最後一個問題,讓浏覽器先通路a網站的頁面,登入并拿到登入成功的憑證後,如何讓浏覽器運作我的代碼來送出資料呢?我試了一下在wpf應用程式中直接使用wpf自帶的浏覽器控件,并研究它的api。在webbrowser類的api清單中,我發現以下方法:

1

2

<code>public</code> <code>void navigatetostring(string text);</code>

<code>public</code> <code>object invokescript(string scriptname);</code>

這正是我想要的啊,先構造一個模拟的頁面,使用navigatetostring到這個頁面上,然後使用invokescript方法來調用javascript送出表單到表單上指定的網站的位址就行了!

    ok,至此,全部設計完成。由于驗證碼已經在活動前就準備好了,是以整個過程基本上是完全自動化的,速度當然比人快多了,ipad自然也就手到擒來!

第二版本

    上面已經說過,網站改版後的秒殺活動,已經使用随機出現的題目來防止作弊。是以這次我的主要任務就是如何自動答題!其次,分析網站的送出頁面中的表單,發現有很多的隐藏域是一連串随機的數字,沒有任何規律,估計這些資料是每次活動都不一樣的,是以再使用第一版中靜态的模拟頁面送出資料的方法不行了,必須使用動态的頁面,把這些随機的資料都保留下來,并一起送出,或者直接在a網站發回來的頁面中填寫資料再送出。

    首先,第一直覺就是擷取大量的送出頁面,提取出擷取到的所有題目,存儲在題目庫中。答題時,直接在題庫中進行比對,如果找到相同的題目,則直接使用題目庫中的答案進行回答。

    當時,簡單地畫了一個流程的設計:

“秒殺”心得

    後來在該次活動的最後一輪秒殺時,程式開發完成,并開始使用。結果,發現沒有一題比對成功,都找不到答案,全部都顯示到了右邊的視窗中人為回答,結果我還答錯了!!!活動結束!!!

    很氣人啊,這樣的方案根本不行。後來分析了半天,發現原來所有的題目都是程式自動生成的,隻是模式固定而已。是以改了設計方案,遵循設計模式寫了一些類來自動回答題目,類結構如下:

“秒殺”心得

這裡,隻貼一個子類的代碼,展示一下解答的模式。這個子類的邏輯也是所有題目中最複雜的一個:

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

<code>class talleralgorithm : selectionquestionalgorithm</code>

<code>{</code>

<code>    </code><code>internal override void trycomplete(selectionquestion question)</code>

<code>    </code><code>{</code>

<code>        </code><code>var match = regex.match(question.content, @</code><code>"小(?&lt;a&gt;.)比小(?&lt;b&gt;.)(?&lt;abunit&gt;.),小(?&lt;c&gt;.)比小(?&lt;d&gt;.)(?&lt;cdunit&gt;.),請問他們當中誰最(?&lt;finalunit&gt;.)?"</code><code>);</code>

<code>        </code><code>if (match.success)</code>

<code>        </code><code>{</code>

<code>            </code><code>var a = match.groups[</code><code>"a"</code><code>].value;</code>

<code>            </code><code>var b = match.groups[</code><code>"b"</code><code>].value;</code>

<code>            </code><code>var c = match.groups[</code><code>"c"</code><code>].value;</code>

<code>            </code><code>var d = match.groups[</code><code>"d"</code><code>].value;</code>

<code>            </code><code>var abunit = match.groups[</code><code>"abunit"</code><code>].value;</code>

<code>            </code><code>var cdunit = match.groups[</code><code>"cdunit"</code><code>].value;</code>

<code>            </code><code>var finalunit = match.groups[</code><code>"finalunit"</code><code>].value;</code>

<code>            </code><code>var winner1 = abunit == finalunit ? a : b;</code>

<code>            </code><code>var loster1 = abunit == finalunit ? b : a;</code>

<code>            </code><code>var winner2 = cdunit == finalunit ? c : d;</code>

<code>            </code><code>var loster2 = abunit == finalunit ? d : c;</code>

<code>            </code><code>string champion =</code><code>null</code><code>;</code>

<code>            </code><code>if (winner1 == winner2) champion = winner1;</code>

<code>            </code><code>else</code> <code>if (winner1 == loster2) champion = winner2;</code>

<code>            </code><code>else</code> <code>if (winner2 == loster1) champion = winner1;</code>

<code>            </code><code>if (!string.isnullorwhitespace(champion))</code>

<code>            </code><code>{</code>

<code>                </code><code>question.rightanswer = question.answers</code>

<code>                    </code><code>.singleordefault(an =&gt; an.text.</code><code>contains</code><code>(champion));</code>

<code>            </code><code>}</code>

<code>        </code><code>}</code>

<code>    </code><code>}</code>

<code>}</code>

    其次,是動态的頁面的整理:把裡面的題目都提取出來,自動答題之後,填寫答案,插入一些送出的javascript代碼。然後繼續使用navigatetostring、invokescript來送出資料。過程中,有兩點心得:

1. 在一開始控制浏覽器導向送出頁面後,發現無法擷取html源代碼,花了些時間研究,沒搞出來。查了半天網頁,最後使用winform中的webbrowser來解決了這個問題。winform中webbrowser不象wpf中的webbrowser,它擁有着強大的api,documenttext屬性就取到了源代碼。

2. 這次我使用了linqtoxml來維護html dom中的所有内容,發現xlinq的api實在是太友善了,查找某個元素,更改某個屬性。如果沒有xlinq,相同的功能,我可能需要3-5倍的時間來完成。

總結

    這次秒殺器編寫的過程,讓我的一個心結給解了。一直以來,就想完全控制網頁用戶端程式的運作:大四在電信的時候,老總讓我給上司刷票;再後來有要人給我給論壇自動送出資料。這些需求都需要持有用戶端的使用者憑證,然後用這個身份給服務端自動發送一些請求。一直使用純背景代碼的方式送出,沒有成功過。這次,使用控制浏覽器的方案,使得真正做到了一直想做到的:“完全控制用戶端”。

繼續閱讀