不說話,先猛戳 Ranklist 看我排名。php
這是用 node 自動刷題大概半天的 "戰績",本文就來爲你們簡單講解下如何用 node 作一個 "自動AC機"。html
先來扯扯 oj(online judge)。計算機學院的同窗應該對 ACM 都不會陌生,ACM 競賽是拼算法以及數據結構的比賽,而 oj 正是練習 ACM 的 "場地"。國內比較有名的 oj 有 poj、zoj 以及 hdoj 等等,這裏我選了 hdoj (徹底是由於本地上 hdoj 網速快)。node
在 oj 作題很是簡單。以 hdoj 爲例,先註冊個帳號(http://bestcoder.hdu.edu.cn/register.php),而後隨便打開一道題(http://acm.hdu.edu.cn/showproblem.php?pid=1000),點擊最下面的 Submit 按鈕(http://acm.hdu.edu.cn/submit.php?pid=1000),選擇提交語言(Language),將答案複製進去,最後再點擊 Submit 按鈕提交,以後就能夠去查看是否 AC(Accepted) 了(http://acm.hdu.edu.cn/status.php)。git
用 node 來模擬用戶的這個過程,其實就是一個 模擬登陸+模擬提交 的過程,根據經驗,模擬提交這個 post 過程確定會帶有 cookie。提交的 code 哪裏來呢?直接爬取搜索引擎就行了。github
整個思路很是清晰:算法
首先來看模擬登陸,根據經驗,這大概是一個 post 過程,會將用戶名以及密碼以 post 的方式傳給服務器。打開 chrome,F12,抓下這個包,有必要時能夠將 Preserve log
這個選項勾上。chrome
請求頭竟然還帶有 Cookie,經測試,key 爲 PHPSESSID
的這個 Cookie 是請求所必須的,這個 Cookie 哪來的呢?其實你只要一打開 http://acm.hdu.edu.cn/
域名下的任意地址,服務端便會把這個 Cookie "種" 在瀏覽器中。通常你登陸總得先打開登陸頁面吧?打開後天然就有這個 Cookie 了,而登陸請求便會攜帶這個 Cookie。一旦請求成功,服務器便會和客戶端創建一個 session,服務端表示這個 cookie 我認識了,每次帶着這個 cookie 請求的我均可以經過了。一旦用戶退出,那麼該 session 停止,服務端把該 cookie 從認識名單中刪除,即便再次帶着該 cookie 提交,服務端也會表示 "不認識你了"。瀏覽器
因此模擬登陸能夠分爲兩個過程,首先請求 http://acm.hdu.edu.cn/
域名下的任意一個地址,而且將返回頭中 key 爲 PHPSESSID
的 Cookie 取出來保存(key=value 形式),而後攜帶 Cookie 進行 post 請求進行登陸。服務器
// 模擬登陸 function login() { superagent // get 請求任意 acm.hdu.edu.cn 域名下的一個 url // 獲取 key 爲 PHPSESSID 這個 Cookie .get('http://acm.hdu.edu.cn/status.php') .end(function(err, sres) { // 提取 Cookie var str = sres.header['set-cookie'][0]; // 過濾 path var pos = str.indexOf(';'); // 全局變量存儲 Cookie,登陸 以及 post 代碼時候用 globalCookie = str.substr(0, pos); // 模擬登陸 superagent // 登陸 url .post('http://acm.hdu.edu.cn/userloginex.php?action=login') // post 用戶名 & 密碼 .send({"username": "hanzichi"}) .send({"userpass": "hanzichi"}) // 這個請求頭是必須的 .set("Content-Type", "application/x-www-form-urlencoded") // 請求攜帶 Cookie .set("Cookie", globalCookie) .end(function(err, sres) { // 登陸完成後,啓動程序 start(); }); }); }
模擬 HTTP 請求的時候,有些請求頭是必須的,有些則是能夠忽略。好比模擬登陸 post 時,Content-Type
這個請求頭是必須攜帶的,找了我很久,若是程序一直啓動不了,能夠試試把全部請求頭都帶上,逐個進行排查。cookie
這一部分我作的比較粗糙,這也是個人爬蟲 AC 正確率比較低下的緣由。
我選擇了百度來爬取答案。以 hdu1004 這道題爲例,若是要搜索該題的 AC 代碼,咱們通常會在百度搜索框中輸入 hdu1004,而結果展示的頁面 url 爲 https://www.baidu.com/s?ie=UTF-8&wd=hdu1004。這個 url 仍是很是有規律的,https://www.baidu.com/s?ie=UTF-8&wd= 加上 keyword。
百度的一個頁面會展示 10 個搜索結果,代碼裏我選擇了 ACMer 在 csdn 裏的題解,由於 csdn 裏的代碼塊真是太好找了,不信請看。
csdn 把代碼徹底放在了一個 class 爲 cpp 的 dom 元素中,簡直是太友好了有沒有!相比之下,博客園等其餘地方還要字符串過濾,爲了簡單省事,就直接選取了 csdn 的題解代碼。
一開始我覺得,一個搜索結果頁有十條結果,每條結果很顯然都有一個詳情頁的 url,判斷一下 url 中有沒有 csdn 的字樣,若是有,則進入詳情頁去抓 code。可是百度竟然給這個 url 加密了!
我注意到每一個搜索結果還帶有一個小字樣的 url,沒有加密,見下圖。
因而我決定分析這個 url,若是帶有 csdn 字樣,則跳轉到該搜索結果的詳情頁進行代碼抓取。事實上,帶有 csdn 的也不必定能抓到 code( csdn 的其餘二級域名,好比下載頻道 http://download.csdn.net/),因此在 getCode() 函數中寫了個 try{}..catch(){} 以避免代碼出錯。
// 模擬百度搜索題解 function bdSearch(problemId) { var searchUrl = 'https://www.baidu.com/s?ie=UTF-8&wd=hdu' + problemId; // 模擬百度搜索 superagent .get(searchUrl) // 必帶的請求頭 .set("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36") .end(function(err, sres) { var $ = cheerio.load(sres.text); var lis = $('.t a'); for (var i = 0; i < 10; i++) { var node = lis.eq(i); // 獲取那個小的 url 地址 var text = node.parent().next().next().children("a").text(); // 若是 url 不帶有 csdn 字樣,則返回 if (text.toLowerCase().indexOf("csdn") === -1) continue; // 題解詳情頁 url var solutionUrl = node.attr('href'); getCode(solutionUrl, problemId); } }); }
bdSearch() 函數傳入一個參數,爲 hdoj 題目編號。而後去爬取百度獲取題解詳情頁的 url,通過測試 爬取百度必須帶有 UA!其餘的就很是簡單了,代碼裏的註釋很清楚。
// 從 csdn 題解詳情頁獲取代碼 function getCode(solutionUrl, problemId) { superagent.get(solutionUrl, function(err, sres) { // 爲防止該 solutionUrl 可能不是題解詳情頁 // 沒有 class 爲 cpp 的 dom 元素 try { var $ = cheerio.load(sres.text); var code = $('.cpp').eq(0).text(); if (!code) return; post(code, problemId); } catch(e) { } }); }
getCode() 函數根據題解詳情頁獲取代碼。前面說了,csdn 的代碼塊很是直接,都在一個類名爲 cpp 的 dom 元素中。
最後一步來看模擬提交。咱們能夠抓一下這個 post 包看看長啥樣。
很顯然,Cookie 是必須的,咱們在第一步模擬登陸的時候已經獲得這個 Cookie 了。由於這是一個 form 表單的提交,因此 Content-Type 這個請求 key 也須要攜帶。其餘的話,就在請求數據中了,problemid 很顯然是題號,code 很顯然就是上面求得的代碼。
// 模擬代碼提交 function post(code, problemId) { superagent .post('http://acm.hdu.edu.cn/submit.php?action=submit') .set('Content-Type', 'application/x-www-form-urlencoded') .set("Cookie", globalCookie) .send({"problemid": problemId}) .send({"usercode": code}) .end(function (err, sres) { }); }
完整代碼能夠參考 Github。
其中 one.js 爲單一題目提交,實例代碼爲 hdu1004 的提交,而 all.js 爲全部代碼的提交,代碼中我設置了一個 10s 的延遲,即每 10s 去百度搜索一次題解,由於要爬取 baidu、csdn 以及 hdoj 三個網站,任意一個網站 ip 被封都會中止整個灌水機的運做,因此壓力仍是很大的,設置個 10s 的延遲後應該木有什麼問題了。
學習 node 主要就是由於對爬蟲有興趣,也陸陸續續完成了幾回簡單的爬取,能夠移步個人博客中的 Node.js 系列。這以前我把代碼都隨手扔在了 Github 中,竟然有人 star 和 fork,讓我受寵若驚,決定給個人爬蟲項目單獨建個新的目錄,記錄學習 node 的過程,項目地址 https://github.com/hanzichi/funny-node。我會把個人 node 爬蟲代碼都同步在這裏,同時會記錄每次爬蟲的實現過程,保存爲每一個小目錄的 README.md 文件。
仔細看,其實個人爬蟲很是 "智弱",正確率十分低下,甚至不能 AC hdu1001!我認爲能夠從如下幾個方面進行後續改進:
爬取 csdn 題解詳情頁時進行 title 過濾。好比爬取 hdu5300 的題解 https://www.baidu.com/s?ie=UTF-8&wd=hdu5300,搜索結果中有 HDU4389,程序顯然沒有預料到這一點,而會將之代碼提交,顯然會 WA 掉。而若是在詳情頁中進行 title 過濾的話,能有效避免這一點,由於 ACMer 寫題解時,title 通常都會帶 hdu5300 或者 hdoj5300 字樣。
爬取具體網站。爬取百度顯然不是明智之舉,個人實際 AC 正確率在 50% 左右,我尼瑪,難道題解上的代碼一半都是錯誤的嗎?可能某些提交選錯了語言(post 時有個 language 參數,默認爲 0 爲 G++提交,程序都是以 G++ 進行提交),其實咱們並不能判斷百度搜索獲得的題解代碼是否真的正確。如何提升正確率?咱們能夠定向爬取一些題解網站,好比 http://accepted.com.cn/ 或者 http://www.acmerblog.com/,甚至能夠爬取 http://acm.hust.edu.cn/vjudge/problem/status.action 中 AC 的代碼!
實時獲取提交結果。個人代碼寫的比較粗糙,爬取百度搜索第一頁的 csdn 題解代碼,若是有 10 個就提交 10 個,若是沒有那就不提交。一個更好的策略是實時獲取提交結果,好比先提交第一個,獲取返回結果,若是 WA 了則繼續提交,若是 AC 了那就 break 掉。獲取提交結果的話,暫時沒有找到這個返回接口,能夠從 http://acm.hdu.edu.cn/status.php 中進行判斷,也能夠抓取 user 詳情頁 http://acm.hdu.edu.cn/userstatus.php?user=hanzichi。
PS:我在 hdoj 的帳號用戶名和密碼均爲 hanzichi,有興趣的能夠用個人帳號繼續刷題。
其實 hdoj 排行榜第一頁有很多都是機器人刷的。
好比 NKspider 這位仁兄,他是用 C++ 寫的,具體能夠參考 http://blog.csdn.net/nk_test/article/details/49497017。
再好比 beautifulzzzz 這位哥們,是用 C# 刷的,具體能夠參考 http://www.cnblogs.com/zjutlitao/p/4337775.html。
一樣用 C# 刷的還有 CSUSTrobot,過程能夠參考 http://blog.csdn.net/qwb492859377/article/details/47448599。
思路都是相似的,它們的代碼都不短,node 才百來行就能搞定,實在是太強大了!