URP教务系统一键评教

每次查成绩之前都需要一门门课程评价,觉得有些麻烦。

写了一个RESTful风格API,用来登录URP教务系统和自动评价教学(一键评教)。

其实最开始打算用JavaFX打造一个C/S架构的程序,然而最近一直都在写B/S架构的,所以改变了一下需求。

导致全部写好又之后重写了一遍。

概述

主要使用的工具:

  • Jersey :构建RESTful风格API
  • Jsoup :解析HTML(主要是选择器太优秀XD)
  • HTMLUnit :模拟浏览器行为
  • JSON-lib :处理json对象

实现思路:

整体流程很简单,一切按照用户操作过程来实现。即:

graph LR
A(登录) -->B(获取需评教课程)
B --> C(填写表单)
C --> D(发送表单)
D --> F(评教结束)

登录

在处理登录部分还花了挺久时间的(毕竟自己太菜),之前是想着直接把账户、密码、验证码然后发送POST请求给指定URL来登录。验证码部分提前下载好然后手动输入。

但是一直提示该资源无法访问。嗨呀!好气啊。想着肯定登录没这么简单。

经过一番分析,发现了2个地方。

一个是验证码是通过向指定URL发送GET请求获取的,每次都会带上一个random参数,而且这个random参数是通过登录页的js随机生成的。

二是登录状态的判断是通过浏览器中的cookies来的。

那么就修改了一波方案:

graph LR
A(访问登录页) --> B(解析HTML获取random值)
B --> C(带上random值获取验证码)
C --> D(填写信息)
D --> E(带上cookies登录)
E --> F(...)

这对于一个C/S架构程序来说并没有什么问题,反正我可以线程一直运行着等待用户输入验证码(毕竟程序一开始需要先做出操作获取验证码)。

可是改成API之后就行不通了(大概是姿势不够,不会框架)。

于是继续思考:

研究了一下,发现一个重大的….BUG?那个什么random值根本没有用…..我累个去

只需要带着cookies….就好了….

所以最终变成了:

graph LR
F(开始) –>G(前端请求获取验证码)
G –> A
A(访问验证码URL) –> B(获取cookies和保存验证码)
B –> H(返回前端)
H –> C(填写表单)
C –> I(提交到服务器)
I –> D(带上cookies登录)
D –> E(…)

获取验证码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void getCaptcha() throws Exception {
WebRequest request = new WebRequest(new URL(CAPTCHA_URL));
request.setHttpMethod(HttpMethod.GET);
// 这个是带着cookie获取的单独的验证码页面
Page page = webClient.getPage(request);
WebResponse res = page.getWebResponse();
// 通过res创建输入流
InputStream is = res.getContentAsStream();
// 通过输入流写入文件并保存
captchaImgFileName = RandomUtil.getRandomFileName("img_", ".jpeg");
saveImg(is, captchaImgFileName);
// 获取cookies
cookies = webClient.getCookieManager().getCookies();
webClient.close();
}

执行登录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public boolean doLogin() {
WebClient webClient = new WebClient(BrowserVersion.CHROME);
webClient.getOptions().setJavaScriptEnabled(true); // 启用JS
webClient.getOptions().setThrowExceptionOnScriptError(false); // JS出错抛出异常
webClient.getOptions().setThrowExceptionOnScriptError(false);
webClient.getOptions().setThrowExceptionOnFailingStatusCode(false); // 当HTTP的状态非200时是否抛出异常
webClient.getOptions().setActiveXNative(false);
webClient.getOptions().setDoNotTrackEnabled(false);
webClient.getOptions().setCssEnabled(false); // 是否启用CSS
webClient.getOptions().setTimeout(5000); // 连接超时时间
webClient.setAjaxController(new NicelyResynchronizingAjaxController()); // 设置支持AJAX
webClient.getCookieManager().setCookiesEnabled(true); // 设置cookies
for (Cookie cookie : cookies) {
webClient.getCookieManager().addCookie(cookie);
}
HtmlPage htmlPage = null;
boolean loginRes = false;
try {
htmlPage = webClient.getPage(LOGIN_URL); // 获取dom树
fillForm(htmlPage); // 填充表单
HtmlImageInput btn = (HtmlImageInput) htmlPage.getElementById("btnSure");
HtmlPage htmlPageAfterLogin = (HtmlPage) btn.click(); // 提交表单
webClient.waitForBackgroundJavaScript(5000);
loginRes = checkLoginStatus(htmlPageAfterLogin);
} catch (IOException e) {
e.printStackTrace();
} finally {
webClient.close();
}
return loginRes;
}

获取待评教课程

使用HTMLUnit模拟浏览器进行获取待评教页面的HTML(不过这里我估计大部分人一学期课程数量不会超过20门,全部在第一页就能显示,所以没有特殊设置),然后用Jsoup解析。

下面Questionnaire是个待评教课程的实体类

1
2
3
4
WebRequest request = new WebRequest(new URL(URL_COURSE_LIST));
request.setHttpMethod(HttpMethod.GET);
HtmlPage evaluateListPage = webClient.getPage(request);
List<Questionnaire> list = parseHtml(evaluateListPage);

从HTML中解析待评教课程列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private List<Questionnaire> parseHtml(HtmlPage htmlPage) {
List<Questionnaire> list = new ArrayList<>();
Document document = Jsoup.parse(htmlPage.asXml());
Elements courseElements = document.select("tr.odd");
if (courseElements != null) {
for (Element courseElement : courseElements) {
Elements detailsElements = courseElement.select("td>img");
String[] infos = detailsElements.get(0).attr("name").split("#@");
Questionnaire questionnaire = new Questionnaire();
questionnaire.setWjbm(infos[0]);
questionnaire.setBpr(infos[1]);
questionnaire.setCharacter(infos[2]);
questionnaire.setName(infos[3]);
questionnaire.setContent(infos[4]);
questionnaire.setPgnr(infos[5]);
questionnaire.setEvaluated(courseElement.select("td").get(3).text());
System.out.println(questionnaire.toString());
list.add(questionnaire);
}
}
return list;
}

评教

模拟客户真实操作,点击一个课程,评价一个课程。

循环处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int numAll = 0; // 列表中一共有的课程数量
int numTot = 0; // 未评教的课程数量
int numOK = 0; // 评教成功的课程数量
for (Questionnaire questionnaire : list) {
if (questionnaire.isEvaluated()) continue; // 如果已经被评教则跳过
DomElement domElement = evaluateListPage.getElementByName(questionnaire.getString());
HtmlPage evaluatePage = domElement.click();
System.out.println("课程[" + questionnaire.getContent() + "] " + "开始处理");
// System.out.println(evaluatePage.asXml());
// 填写表单
fillForm(evaluatePage);
System.out.println("课程[" + questionnaire.getContent() + "] " + "评价填写完成");
// 提交表单
evaluateListPage = submitForm(evaluatePage);
System.out.println("课程[" + questionnaire.getContent() + "] " + "评价提交完成");
// 获取评教结果
boolean evaluateResult = isEvaluateSuccess(alertInfo[0]);
System.out.println("课程[" + questionnaire.getContent() + "] 评教结果:" + evaluateResult);
numTot += 1;
numOK += (evaluateResult ? 1 : 0);
}

返回结果

注意POST提交跨域问题,response中需要设置header:

1
2
3
4
return Response.status(200).entity(returnJSON.toString())
.header("Access-Control-Allow-Origin", "*")
.header("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT")
.allow("OPTIONS").build();

最后

源码查看请移步 GitHub - Ningxxxl / JWPT_API