接上文:

王競先:用WebTools包製作刷課機?

zhuanlan.zhihu.com
圖標

這一次主要和驗證碼打交道,從如何獲取驗證碼到如何識別驗證碼。(當然這個驗證碼是「十分簡單」的那種……)

我想著呢這個部分我就換個方式寫,上一個部分裡面主要是常規的介紹WebTools裏基本功能的使用,按部就班講過來就好。而這個部分則是更「探索性」的,所有的內容都得我自己找到方法來解決,方案也不是唯一的。那麼我就不直截了當的講我的程序怎麼寫的(其實很短),而是講故事一樣說說我在處理過程中是怎麼想的,希望這樣能讓讀者看起來更加有意思些。(假如覺得沒意思一定要告訴我哦~)

北大採用的是圖形驗證碼,給些個樣例呢就是大概這樣:

嗯,看起來很脆弱的樣子……而且這也是一個一兩天內胡搞的內容,也不是我的飯碗,自然不想大動干戈的用什麼機器學習,簡單粗暴各種簡單的圖像處理揉一起解決掉就行了……

Section 1: 對付驗證碼

第一步肯定是先看看這驗證碼有啥特點:

  1. 傻(廢話……)
  2. 大致結構上是四個正常字元然後背景上加隨機噪點的結構
  3. 所有驗證碼中字元都是一樣的大小,一樣的字體,但是位置有不同。
  4. 不能更傻(你給我閉嘴!)

那麼怎麼對症下藥呢?首先去掉噪點:Binarize之後DeleteSmallComponents就可以了。 然後怎麼處理字元識別呢?既然字元字體、大小都一樣,也就是我們可以把所有可能出現的字元都提取出來然後用個方法一個一個匹配就行啦~

首先搞點測試集下來,測試集麼,也就是有效無效的,只要是驗證碼就都行。那就很簡單了,也是扒一下HTML,發現:誒,點一下刷新那個按鈕,它就會訪問一下這個地址:elective.pku.edu.cn/ele ,然後瀏覽器一看,這玩意直接登進去就能出一個隨機的驗證碼,於是就很簡單了:

Import@URLRead@HTTPRequest@"http://elective.pku.edu.cn/elective2008/DrawServlet"

果真,就搞下來了張圖片,再多來幾次就獲得了一個好用的數據集。

然後稍微處理下:

Binarize@DeleteSmallComponents[ColorNegate@Binarize[orig, .15], 3,CornerNeighbors -> False]

emmm,這個裡面其實還有點細節,例如為啥只消掉3個像素及以下的,還有為啥要把CornerNeighbors變成False。其實就是因為i和j上面那個點有四個像素啦……而且一些幹擾點會有那種藕斷絲連的一點點,自然是去掉最好,所以就設置成False。

這樣其實字就相當清晰且銳利了!上面那些處理完就成了這樣:

看起來就很棒了!

然後再看看如何簡單的把字元提取出來。

首先看看單字,總共最多A~Z+a~z+0~9這麼點,直接上手,畫圖大法好,一下就把所有字元都給摳出來:

emmm,看起來很不錯。

下一件事情就是如何識別這個二維碼,簡單的想,我可以比較驗證碼的一部分和我的這些字元都像不像,假如有一部分像極了,那就極有可能是匹配上了。這個想法就是Correlate或者Convolve的想法,實現起來也很簡單:

ImageCorrelate[image, kernel, ManhattanDistance[Flatten@#1, Flatten@#2] &, PerformanceGoal -> "Quality"]

這個效果是啥樣子呢,例如我拿第一張驗證碼和D這個字元去做一下這個操作就會得到這樣一張照片(ImageAdjust過):

可以看到中間一個點超級黑的,旁邊的要麼就是白不拉幾要麼就是灰不拉幾。黑的啥意思呢,值小唄,或者說在這個位置上的圖和D差別小唄,對著一看,誒,真的就是一個D在這個位置上誒。事實上,這種操作被廣泛運用於各種要匹配一些東西的場合,超級好用!所以大致思路就是對每個字元都這麼做一遍,然後挑特別黑的點唄。

這裡多廢話一句,這個方法在找多個較為固定的特徵的時候更方便,在數數、定位(例如有一坨機器人分佈在地上,需要追蹤機器人的位置)等工作中這個套路都是很好用的。做字元圖的時候也可以用這種方法來找可以填充新字元的空位,後面假如寫如何做字元圖的時候也可以講一下。

再一考慮,不對,不同的字元大小不同,大的的話也就會囊括更多噪點,所以,合理的方案是比較匹配差別除去字元圖片的像素數,嗯,看起來就很有道理了。再考慮下i、j這種毒瘤,假如是個j的話,i可以完全的匹配上,這就很坑爹了,那解決方案是啥呢?就是在所有字元旁邊加一小圈黑色的框,認為一個字元附近一點點除了幹擾該是沒有別的字元的,這樣的話,i在加了一個黑圈之後會認為i底下一小塊是黑色,而j下面還有個勾,自然就出現了匹配誤差,有這麼點匹配誤差,加上i本來面積很小,一除,大部分情況下j就會勝出了,也就實現了相對正確的匹配。

稍微經過一會兒調試就能找到誤差/像素數的一個界限,取所有足夠黑的像素點位置和對應的字元就能得到驗證碼結果大概的猜測了。

但是還有個問題,還是用i和j的識別舉例,雖然i成功的被j擠下去了,但是i的誤差還是很小,怎麼辦呢?想想這裡只有四個字元,顯然字元的中心位置是要有點間距的,所以就是從最小誤差開始往上算,假如新的字元中心位置離前有的字元的中心位置差距都挺大就加進去,否則就不管,直到有四個字元,假如所有合格(像素點位置夠黑)的匹配都用完了就匹配失敗了……

按照這個思想寫出來的代碼:

(*黑邊大小*)$TextImagePadding = {{1, 1}, {3, 3}};(*導入處理好的字元文件*)$IMGAsso = With[{s = ImagePad[Binarize@Import[NotebookDirectory[] <> "characters" <> #], $TextImagePadding]}, {StringTake[#, 1], s, Times @@ (ImageDimensions@s)}] & /@ Import[NotebookDirectory[] <> "characters"];(*Pre-processing image*)IMGPreProc[orig_] := ImagePad[ImageCrop@ With[{testfig = Binarize@DeleteSmallComponents[ColorNegate@Binarize[orig, .15], 3, CornerNeighbors -> False]}, With[{l =Accumulate[UnitStep[Total /@ ImageData@Binarize@testfig - 3]]}, ImageTake[testfig, {Count[l, 0] + 1, Length@l - Count[l, Last@l] + 1}]]], $TextImagePadding](*Identify Image*)IMGGetResult[orig_] := With[{o = IMGPreProc@orig}, If[Length[#] == 4, StringJoin@#, ""] &@SortBy[Take[Gather[SortBy[Catenate[ With[{s = ImageCorrelate[o, #2, ManhattanDistance[Flatten@#1, Flatten@#2] &, PerformanceGoal -> "Quality"]}, With[{p = PixelValuePositions[Binarize[s/#3, .07], 0]}, Thread[{#1, p, PixelValue[s/#3, p]}]]] & @@@ $IMGAsso] , Last], Norm[#1[[2]] - #2[[2]]] < 4 &][[;; , 1]], UpTo[4]], #[[2, 1]] &][[;; , 1]]];

這裡還多加了點自動處理圖片,把頂上和底下的空白刪掉點,減少處理量也減少失誤率。

用前面搞下來的數據集測試一波,識別率90%+接近95%,剩下的部分中,大部分都是報告無法識別而不是給出錯誤答案(因為假如識別不了我可以很快的換張驗證碼,比錯誤識別選不上課要好……),感覺效果不錯!

Section 2:獲取驗證碼

這有啥難的……我一開始也是這麼想的,畢竟扒圖片的事情也沒少幹……可是我發現很難通過WebTools,或者說JS從已經渲染好的網頁上搞下來圖,新開一張圖、右鍵點擊下載的話搞下來的都不是現在放在驗證碼區的圖了,這就讓我很尷尬……怎麼辦咧?

嘗試1:狂搜索有沒有能在已經渲染的網頁上搞下來圖的方法……失敗

嘗試2:猜想可以通過調整網址得到不同的圖,只要門路對了就行……失敗

後來偶然發現,假如我拽一張圖出來,雖然圖變了,但是似乎這時候老驗證碼會失效,新的驗證碼輸進去纔有用誒!emmmm,這就十分intriguing了……(大家可以自己看下我第一部分裡面的視頻,機器輸入驗證碼的時候數值和顯示的可不一樣哦!)

嘗試3:猜想是通過什麼奇技淫巧把生成的碼和時間結合了,那豈不是GG!

後來和 @葉林楠 聚佬談笑風生的時候他提了句估計是SessionId的問題,一開始想URL上沒顯示出來啊,後來查了下發包的信息,果真有個Cookie在裡面,刪刪減減最後發現裡面有用的就一句話: SessionId=**** 額……

遂嘗試用Mathematica寫Request……這裡吐槽一波Mathematica的HTTPRequest函數的用法,真是超級噁心,我根本不知道它自動給我發了多少辣雞信息,而且有些奇奇怪怪的選項放在不同的地方還不一樣……有些還會不識別……中間一波三折,吐槽吐槽而已,不贅述了。最後是用Wireshark慢慢試,可算把Mathematica發的請求的信息和Web上的信息對上了……測試下,果真對的:

SetElectiveId[] := ($ElectiveId = StringSplit[wtJavascriptExecute["return document.cookie;"], "="][[2]])GetVerificationIMG[] := (SetElectiveId[]; Import@URLRead[HTTPRequest["http://elective.pku.edu.cn/elective2008/DrawServlet?Rand=1109. 12659122104", <|Method -> "GET", "Cookies" -> {<|"Domain" -> "elective.pku.edu.cn", "Path" -> "/", "Name" -> "JSESSIONID", "Content" -> $ElectiveId, "ExpirationDate" -> Automatic, "AllowSubdomains" -> False, "ConnectionType" -> All, "ScriptAccessible" -> True|>}|>]])

嗯,這段代碼就能獲取Cookie,得到SessionId,然後得到驗證碼啦!再識別後就是驗證碼的結果。如何把一些文字用WebTools輸入到瀏覽器裏前一個部分已經說過了,直接給出代碼:

ByPassVerification[] := Module[{s = ""}, While[s === "", s = IMGGetResult@GetVerificationIMG[]]; wtTypeElement[wtId["validCode"], s]; ]

這個函數結合了所有驗證碼從獲取到識別的功能,只需要輸入ByPassVerification[]就能自動將可以通過驗證的結果輸入到對應的框,別的地方該幹嘛幹嘛就行啦!

哇,吐槽一句,這個Section 2寫的好輕描淡寫啊!這個是我全過程中最煩的一段誒…… 嘗試了好多方法最後才行……

嗯,然後就是Po上來完整的選課過程的代碼:

這裡為了思路明晰我拆成了兩部分,最原始的是根據課程在整個課表中的絕對位置來選課,也就是選「網頁中第幾頁的第幾個課」,第二個則是根據課的名稱選課。

首先,選課提示啥的是很惱人的東西,關掉!

DiableAlerts[] := wtJavascriptExecute["window.alert=function(){return 1}; window.confirm=function(){return 1};"]

選個課呢,就是先切換到對應的頁面,破掉驗證碼,然後點下選課就行……當然還需要注意別選了錯誤的課,那就很尷尬了,所以我還特意加了個名字驗證:

SelectCourse[{p_Integer, n_Integer}, name_: Automatic] := Block[{name1 = name}, ToPage[p, GetPageText[]]; If[name1 === Automatic, name1 = GetPageContent[GetPageText[n]][[1]]]; DiableAlerts[]; ByPassVerification[]; wtClickElement[ wtXPath["/html/body/table[2]/tbody/tr[" <> ToString[7 + CheckInfo[]] <> "]/td/table/tbody/tr[" <> ToString[n + 1] <> "]/td[11]/a"]]; [email protected]; ! FreeQ[GetSelected[][[;; , ;; 2]], name1[[;; 2]]] ]

下一步就是直接用名字選課,也就是先把所有頁面掃一遍,找到要選的名字在什麼位置,調用上面的函數就好啦……看起來超級簡單……當然我加了很多奇奇怪怪的模式,這裡就不贅述了,大家自己看源碼去吧。大致說一下各種模式的功能:

  1. 給一坨課名,狂刷,有空就佔。
  2. 有些同學希望某個課有空位的時候就退掉現在的某個課,這樣纔有空位去選新的課,我也提供了重載,可以讓用戶指定一個課有空位的時候退什麼課。過程大致來說就是:檢查空位->找到有空的課->假如需要退課,先退->選課->檢查選沒選上->選上了皆大歡喜,沒選上的話呢,也不能棄療啊!趕緊把退掉的課嘗試選回來,若是沒選回來咧……就加到刷課列表裡一起刷……#捂臉#我能怎麼辦,我也很無奈啊~
  3. 自由模式:自己設計,直接敲代碼!(還不如你直接改源代碼方便咧……BTW,我設置了一個進入該模式的問題,以防小白進去了之後使用不當把自己的課都退光光了……)

話說其實最後一個模式還是有很多值得開發的東西,例如可以用Mathematica造一個通過拖拽進行編程的語言,感覺就很棒誒!(但是我懶#攤手#)


推薦閱讀:
相关文章