木匣子

Web/Game/Programming/Life etc.

图书馆入馆考试自动答题(2012)

今年课业很闲,在新生群里认识了很多学弟学妹,其中不乏对技术很有兴趣的同学。于是打算找几个志同道合的一起做些小项目。 正好他们军训过后要参加图书馆入馆培训,培训之后需要参加一个在线测试,分数高于80分的才可以从图书馆借阅书籍。于是我把去年写的代码翻了出来,重新运行了一下,发现很多题目已经过期了,正确率已经不足40%。这是一个不错的信号——就拿这个小项目动手吧。 这次的目标是制作一个可以自动完成入馆考试的脚本,并提供一个网页供他人使用。

技术分析

图书馆通识测验地址 - http://210.34.218.48/exam/ (公网访问出奇的慢) 这是一个基于 asp.net 的在线测试系统,题目类型有:单选题、多选题以及判断题。最坑爹的地方在于——完全是用 .net 那套 updatePanel (Ajax) 来响应操作。每次点击一个选项,都要 ajax 通讯一次,把整个题目信息更新一遍。对于公网用户,平均响应速度几乎超过1s~幸好我是忠实的教育网用户。 对付网页版的在线测试,直接用 greasemonkey 脚本最方便了(但只支持firefox和chrome)。考虑到跨浏览器支持,还是决定采用比较小众的 js 注入法——通过浏览器书签(或地址栏)将外部脚本注入到当前页面。其余的所有操作就由 js 控制页面元素来完成。

iframe 的妙用

还有一个问题就是在线测试的不同题型分布在不同页面。当完成一种题型后切换到另一个页面,之前注入的 js 脚本就没了,需要重新注入一次。这大大加重了用户的操作负担,而且在使用教程上也不容易解释。后来想到一个取巧的方法,就是将被注入的页面自嵌到 iframe 里,通过控制 iframe 到实现自动页面切换:

/* 把网页放到浮动框架中 */
document.write('<iframe id="iframe" name="content" src="' + location.href + '" style="width:100%;"></iframe>');

/* 响应页面载入事件 */
frame.onload = function() {
    /* do_something(); */
};

/* 跳转页面 */
function redirect(url) {
    frame.src = url;
}

/* 得到页面信息 */
content.location.href; 

注意到这里使用了 id="frame"name="content"。经过测试发现,通过 id 获取到的是 iframe 元素本身,可以响应各种事件,如 onload ;而通过 name 获取到的是 iframe 的内容——一个 window 对象,包含了被嵌入网页的所有信息。这样一来,就可以通过 iframe 做任何事情了。不过由于安全沙箱的限制,通常只允许对相同域名下的网页进行操作。但本例是通过 js 注入,所以安全沙箱也无法阻档,hah。 其实只要两行代码,就可以防止网页被嵌到框架里:可以参考阮一峰老师的这篇博客《防止网页被嵌入框架的代码》,如果管理员加了这个脚本的话,我就只能老老实实地分次 js 注入了。

updated 2012-09-26:经过测试确认 IE系列的浏览器不能响应通过这种形式创建的iframe的各种事件。

可行性分析得到的结果令我很满意,接下来的工作就是收集题库了。由于这个测试系统允许无限次答题,并且每次答题结束后都会公布答案信息(其实只要直接访问测试结果的页面就行了)。所以只要多试几次,就能拼凑出大部分题目了。在这里感谢12级物联网专业的林智同学帮我整理了题库。 晚上花了两个多小时和林同学结对编程,终于把基本的功能实现了,录了一段视频让大家偿偿鲜,接下来将近一步完善并修复一些 bug,最后制作并发布一个公测页面。敬请期待 :)

updated 2012-09-26:

以上是使用定时器控制的答题情况。在实际使用中会出现很多问题,尤其是在宽带访问出现 ajax 更新延时,正确率严重下滑!于是我考虑能否通过响应 ajax 回调事件来进行答题控制。

updatePanel 之迷

由于对 Asp.Net 比较不熟悉,于是直接在 chrome 开发人员工具下跟踪调试了一个下午才找到用于响应事件的代码——Sys.WebForms.PageRequestManager 类,其中有两个方法 add_endRequest() 和 remove_endRequest() 可以自定义ajax请求结束时的回调函数。

Sys.WebForms.PageRequestManager.getInstance().add_endRequest(callback);

但是通过 iframe 操作 add_endRequest() 时不知何时种原因 chrome 出现了异常:

iframe.Sys.WebForms.PageRequestManager.getInstance().add_endRequest(callback);
> RangeError: Maximum call stack size exceeded

为了解决这个问题,我改用直接操作事件列表的方式来处理这个方法:

iframe.Sys.WebForms.PageRequestManager.getInstance()
    ._get_eventHandlerList()
    ._getEvent("endRequest", true)
    .push(handler);

这样一来就可以知道答题操作后结果返回的确切时间了,在回调函数里决定接下来做什么就可以了。以下是效果预览:

跨浏览器支持

在 chrome 中,直接使用对象的 name 或者 id 即可以获取到对象;而在 firefox 中则建议使用 w3c 标准的 document.getElementById() 来获取对象,甚至没有提供使用 name 来获取对象的api,只有 document.getElementsByName() 获得对象数组。 为了快速移植脚本使其适应其它浏览器,我写了一个简单的 function 来获取对象。需要引用对象的话,将 iframe[“name_or_id_for_element”] 统一改成 getFrameElement(iframe, “name_or_id_for_element”); 即可:

function getFrameElement(frame, name_or_id){
    var element = frame.document.getElementById(name_or_id);
    if(!element) element = frame.document.getElementsByName(name_or_id)[0];
    return element;
}

现在脚本已经能很顺利地在 chrome/firefox/opera/safari 执行了。而 IE 则不能很好地处理 iframe 对象,所以暂时不考虑 IE 的自动答题脚本。这里提供了一个半自动版的演示,需要多次 js 注入:

另外,IE6 甚至没有提供 Array.indexOf() 方法,需要自己实现:

if(!Array.prototype.indexOf){
    Array.prototype.indexOf = function(val){
        var value = this;
        for(var i =0; i < value.length; i++){
            if(value[i] == val) return i;
        }
        return -1; 
    };
}

接下来将着手开始制作公测页面,我考虑在网页上加入 kill ie6 代码:

Let’s Kill IE6 
https://code.google.com/p/letskillie6/

敬请期待~

updated 2012-09-27:

厦图答题神器公测地址已发布:http://xujc.sinaapp.com/library/ 欢迎同学进行测试 :)