1. 程式人生 > >繼續Node爬蟲 — 百行程式碼自制自動AC機器人日解千題攻佔HDOJ

繼續Node爬蟲 — 百行程式碼自制自動AC機器人日解千題攻佔HDOJ

前言

不說話,先猛戳 Ranklist 看我排名。

這是用 node 自動刷題大概半天的 "戰績",本文就來為大家簡單講解下如何用 node 做一個 "自動AC機"。

過程

先來扯扯 oj(online judge)。計算機學院的同學應該對 ACM 都不會陌生,ACM 競賽是拼演算法以及資料結構的比賽,而 oj 正是練習 ACM 的 "場地"。國內比較有名的 oj 有 poj、zoj 以及 hdoj 等等,這裡我選了 hdoj (完全是因為本地上 hdoj 網速快)。

用 node 來模擬使用者的這個過程,其實就是一個 模擬登入+模擬提交 的過程,根據經驗,模擬提交這個 post 過程肯定會帶有 cookie。提交的 code 哪裡來呢?直接爬取搜尋引擎就好了。

整個思路非常清晰:

  1. 模擬登入(post)
  2. 從搜尋引擎爬取 code(get)
  3. 模擬提交(post)

模擬登入

首先來看模擬登入,根據經驗,這大概是一個 post 過程,會將使用者名稱以及密碼以 post 的方式傳給伺服器。開啟 chrome,F12,抓下這個包,有必要時可以將Preserve log 這個選項勾上。

請求頭居然還帶有 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 這個請求頭是必須攜帶的,找了我好久,如果程式一直啟動不了,可以試試把所有請求頭都帶上,逐個進行排查。

搜尋引擎爬取 Code

這一部分我做的比較粗糙,這也是我的爬蟲 AC 正確率比較低下的原因。

百度的一個頁面會展現 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 。

其中 singleSubmit.js 為單一題目提交,例項程式碼為 hdu1004 的提交,而allSubmit.js 為所有程式碼的提交,程式碼中我設定了一個 10s 的延遲,即每 10s 去百度搜索一次題解,因為要爬取 baidu、csdn 以及 hdoj 三個網站,任意一個網站 ip 被封都會停止整個灌水機的運作,所以壓力還是很大的,設定個 10s 的延遲後應該木有什麼問題了。

學習 node 主要就是因為對爬蟲有興趣,也陸陸續續完成了幾次簡單的爬取,可以移步我的部落格中的Node.js 系列。這之前我把程式碼都隨手扔在了 Github 中,居然有人 star 和 fork,讓我受寵若驚,決定給我的爬蟲專案單獨建個新的目錄,記錄學習 node 的過程,專案地址 https://github.com/1335661317/funny-node/tree/master/auto-AC-machine 。我會把我的 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:可是我試了好多次,Node環境還是沒有搭建成功,總是缺少一個東西……