#前言
不说话,先猛戳 Ranklist 看我排名。
这是用 node 自动刷题大概半天的 “战绩”,本文就来为大家简单讲解下如何用 node 做一个 “自动AC机”。
#过程
先来扯扯 oj(online judge)。计算机学院的同学应该对 ACM 都不会陌生,ACM 竞赛是拼算法以及数据结构的比赛,而 oj 正是练习 ACM 的 “场地”。国内比较有名的 oj 有 poj、zoj 以及 hdoj 等等,这里我选了 hdoj (完全是因为本地上 hdoj 网速快)。
在 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)。
用 node 来模拟用户的这个过程,其实就是一个 模拟登录+模拟提交 的过程,根据经验,模拟提交这个 post 过程肯定会带有 cookie。提交的 code 哪里来呢?直接爬取搜索引擎就好了。
整个思路非常清晰:
- 模拟登录(post)
- 从搜索引擎爬取 code(get)
- 模拟提交(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 正确率比较低下的原因。
我选择了百度来爬取答案。以 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。
其中 singleSubmit.js 为单一题目提交,实例代码为 hdu1004 的提交,而 allSubmit.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,有兴趣的可以用我的账号继续刷题。
少年,作为苦练ACM,通宵刷题的你 是不是想着有一天能够荣登各大OJ榜首,俯瞰芸芸众生,唔....要做到这件事情可是需要一定天赋的哦!
博主本身也搞过一段时间的acm,对刷题深有感触,不信可以去看我博客的acm题解(哈哈)。
不过,先给各位辛苦刷题的ACMer赔个不是,毕竟这是很投机的一种方式,仅供娱乐,还请各位见谅!
受学长的启蒙,打算自己做一个使用C++语言完成的自动刷题神器,也可以叫自动AC机(什么?ac自动机...吓尿),先来看一下成果:
(注:这是第一次刷完后的排名,后来对代码进行了很大的优化,但是由于时间关系,没有再刷一遍,否则肯定进入前十!)
第17名是我,AC率还算不错吧,但是我优化之后的肯定比这个高!
好了,扯淡完毕,下面进入正文,先来说一下整体思路:
1)使用socket编程模拟HTTP协议GET请求向服务器发送页面请求
2)借助搜索引擎找到相关题目的代码(一般csdn的居多)
3)使用正则表达式解析HTML代码获取博客连接,紧接着从博客中解析出 题目的代码
4)对代码进行编码转换的处理,模仿HTTP协议的POST请求向服务器提交代码
5)解析提交后返回的State页面,提取最终的结果(是否Accepted)、耗时和空间占用。
6)将刷题过程存储至SQL Server数据库,供以后的数据分析。
是不是感觉很简单的样子,让我们一步一步来!
(一)使用socket编程模拟HTTP协议GET请求向服务器发送页面请求
我们在baidu中搜索关键字,点击按钮,服务器会返回我们一个页面,这件事情使用程序该如何实现呢?
答案就是我们使用Socket编程通过bind(),connect(),send(),recv()这些函数建立与服务器的连接。这些知识就不再这里展开了,读者可以自行baidu或者参考我的Linux网络编程专栏。 接下来我们想:点击按钮的过程发生了什么,我们使用send()需要将什么信息发送至服务器,这里就要涉及到HTTP协议的GET请求。
我们只需要实现GET的请求头即可(可以通过chrome按F12来查看),注意和正文之间有一个空行,即/r/n
(二)借助搜索引擎获取csdn博客链接
一开始的时候我想利用百度搜索引擎,但是发现返回到HTML页面中根本无法找出csdn博客的链接特征,后来发现,原来是baidu为了避免爬虫爬取进行了加密处理,如下图:
注意左下角是第一个csdn博客的连接....坑了我一段时间。
后来发现360搜索没有加密,所以那就用360吧。。提取出来放入vector保存,然后使用C++11的正则表达式解析出csdn博客的地址。
(三)从HTML中解析出代码
注意代码一般由#include开始,结束的位置是</textarea>或者</pre>,我们利用这个特征进行提取。
(四)对代码进行编码转换的处理,模仿HTTP协议的POST请求向服务器提交代码
我们可以看到上面的代码并不是解析出来就能用的,还包含有<,>等HTML的元素,并且还有汉字转码的问题需要我们需要处理。POST的时候,还需要考虑HTTP编码,
将空格回车等转换为十六进制发送提交,不说了,直接看代码:
POST:注意头信息要全,并且Cookie要写你自己的,一旦浏览器关闭就会失效,要重置:
(五)解析提交后返回的State页面,提取最终的结果(是否Accepted)、耗时和空间占用
这个就比较简单了,数据分析,可能实现都不一样,我是先定位的题号:
(六)将刷题过程存储至SQL Server数据库,供以后的数据分析
要点就是C++使用ado连接SQL Server数据库
注意将上面的server名字,uid和pwd改为你自己的。
下图的Queuing请无视,因为我为了速度并没有Sleep(),页面还没有显示出结果,对,我比较懒==
好啦,到这里就大功告成啦!别刷太快哦,貌似被hdu封了一次IP。。。。。(囧)
最后项目的完整源码在我的Github,欢迎大家fork!
有疑问或者优化请留言或者 通过邮箱联系我,联系方式在博客的左上角,希望给大家学习网络编程带来帮助!
#其他
其实 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 才百来行就能搞定,实在是太强大了!