M
M
文章目录
  1. 目标
  2. 背景
  3. Java.net 工具介绍
    1. 建立网页连接
    2. 设置请求头
      1. 如何设置请求头
      2. 设置cookie
      3. 设置浏览器
      4. 设置输入输出权限
      5. 设置时间限制
      6. 设置请求方法
  4. 登录
    1. 登录确认机制
    2. 登录中的trick
      1. trick.1
      2. trick.2
  5. 爬课程
  6. 验证码图片抓取
  7. 多课程刷课
  8. 图片demo

点歪技能树:用Java.net制作安卓端北大刷课机

写在最前面

本文仅供技术交流和参考,绝无鼓励大家利用刷课机进行刷课的意思。为了防止大面积的刷课机使用导致其它同学选不上课,并不会直接提供全部信息。

本文主要介绍WebConnect部分,不包括验证码识别和安卓界面的设计。

project地址:https://github.com/MemphiSqrt/Elective-Master

目标

  • 实现在线登录

  • 实现验证码自动识别

  • 实现多课程自动刷课

  • 实现课程信息查询

其中验证码的识别和安卓界面的制作分别交由@warshallrho@mrmrfan完成了

背景

在北大选课大致分为两轮:

  • 第一轮预选,大家通过投“意愿点”表明意愿,而后加权随机选出选上课的人。

  • 第二轮补选,也就是“抢课”,一旦有空位,先到先得。这也就是为何会出现“刷课机”,即一直刷新界面,空位出现时就可以立刻选上。

现刷课方式大概有三种:

  • 直接在chrome等浏览器上编写插件,让使用者直接打开补退选界面进行刷课。

  • 借用浏览器调用工具(如selenium库)进行全自动刷课,但是无法移植到安卓端上。

  • 第三种方法,也是本文介绍的方法,即用Java.net等直接进行网络连接。

Java.net 工具介绍

建立网页连接

1
2
URL url = new URL(WebPage);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();

设置请求头

如何设置请求头

除了cookie以外,设置浏览器之类的请求头只需要模仿浏览器访问时请求头即可。

以下给出一些例子。

设置cookie

1
2
3
if (cookie != "") {
connection.setRequestProperty("Cookie", cookie);
}

设置浏览器

1
2
connection.setRequestProperty("User-Agent",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36");

1

设置输入输出权限

1
2
connection.setDoInput(true);
connection.setDoOutput(true);

设置时间限制

超出时间就会抛出异常

1
2
connection.setReadTimeout(CONNECT_LIMIT_TIME);
connection.setConnectTimeout(CONNECT_LIMIT_TIME);

设置请求方法

请求方法一般只有POST和GET

1
connection.setRequestMethod(method);

登录

登录确认机制

登录其实是一件很麻烦的事情(北大选课网登录更麻烦,坑实在太多了。。TAT。。这个后面会讲),首先要理解一下登录的机制。

网页链接并非像浏览器浏览时那样看上去是连续的,事实上这次发送的请求,接收端并不知道你是谁,必须要有确认“你就是上次输入正确的用户名和密码的那个人”的方法。这个方法就是验证了你的身份以后,给你一个“令牌”,这个“令牌”就是cookie

得到cookie以后,以后每次登录网站只需要带着这个cookie就可以成功登录网页了。

先输入参数(即用户名、密码以及隐藏的动态密码、验证码,这些东西虽然看不到,会在你连续输入错误的密码的时候跳出来,但是你看不到的时候还是要给接收端默认参数的!

1
2
3
4
5
6
7
String outputStr = "appid=syllabus"+"&userName="+userName+"&password="+passWord+"&randCode=验证码"+"&smsCode=短信验证码"+"&otpCode=动态口令"+"&redirUrl=http://elective.pku.edu.cn:80/elective2008/agent4Iaaa.jsp/../ssoLogin.do";
HttpURLConnection conn = GetConn(LoginPage, "", "POST", false);
conn.connect();
OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream(),"utf-8");
out.write(outputStr);
out.flush();
out.close();

输完参数以后,网页便会返回一个json message告诉你是否成功登陆,尝试以后就知道他的json格式,如果json中包括false就是登录失败,那么只需要判断一下

1
2
3
4
5
BufferedReader buffR = new BufferedReader(new InputStreamReader(conn.getInputStream(),"utf-8"));
String line = buffR.readLine();
if (line.contains("false")) {
return false;
}

然后就可以得到cookie了

1
String cookieGet = conn.getHeaderField("Set-Cookie");

登录中的trick

trick.1

北大网站程序员:你以为这样就好了?我就是要难为难为你!

光用之前的方法,依旧是无法登录的。。

经过一通研究发现,登录以后网站会经过一个跳转网站,这个网站需要登录界面返回的json中的token作为参数去登录!而且这个网站还有一个参数是一个随机数(这什么骚操作)。然后这个网站会马上跳转到登录成功的选课主页面helper control界面!而这个跳转的中间网站会返回一个cookie,这个cookie才是真正的cookie!

网站制作者,你牛逼。

没办法,那我们改了一下

1
2
3
4
5
6
String tokenGet = line.substring(line.indexOf("token")+8, line.length()-2);
String reDirPageFull = RedirPage+"?rand="+String.valueOf(Math.random())+"&token="+tokenGet;
conn = GetConn(reDirPageFull, cookieGet, "POST", false);
conn.connect();
cookieGet = conn.getHeaderField("Set-Cookie");
conn.disconnect();

trick.2

现在我们可以成功登陆helper control了!

正当我大出一口气的时候,结果发现进入补选supply界面后,它告诉我

“请同意选课协议!”

woc??这是啥?

回想起当时我们第一次登录选课网的时候,是有一个选课协议会跳出来,然后点了同意。

可以任何账号成功登录选课网站同意选课协议一次以后都不会再遇到选课协议了啊。。

尝试print helper control界面,也没有任何可以点击同意的地方。。

经过痛苦的各种尝试以后,突然发现,当我readLine一遍helper control界面,就可以了。。

???

还有这么骚的操作吗?

网站制作者,你牛逼。

1
2
3
4
conn = GetConn(HelperControlPage, cookieGet, "POST", true);
conn.connect();
displayConnNoPrint(conn);
conn.disconnect();

至此我们就完成了登录。

爬课程

完成了登录以后,接下来的爬课程工作就类似于网络爬虫了,相比登录而言,这就没有什么令人头大的trick了,北大选课网也没有什么反爬虫机制,一般就是伪装成浏览器在网站上爬网页下来就行了。

贴一下爬专业课的代码:

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
// ----------------------   crawl education_plan_bk

System.out.println("education_plan_bk crawling");
conn = GetConn(CurriculmFormPage, cookieKey, "GET", false);
conn.setRequestProperty("Referer", "http://elective.pku.edu.cn/elective2008/edu/pku/stu/elective/controller/help/HelpController.jpf");
conn.setRequestProperty("Host", "http://elective.pku.edu.cn");
conn.setRequestProperty("Accept-Encoding","gzip, deflate");
conn.setRequestProperty("Upgrade-Insecure-Requests","1");
conn.connect();
OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream(),"utf-8");
String requestStr = "wlw-radio_button_group_key%3A%7BactionForm.courseSettingType%7D=education_plan_bk"
+"&%7BactionForm.courseID%7D="
+"&%7BactionForm.courseName%7D="
+"&wlw-select_key%3A%7BactionForm.deptID%7DOldValue=true"
//+"&wlw-select_key%3A%7BactionForm.deptID%7D=048"
+"&wlw-select_key%3A%7BactionForm.courseDay%7DOldValue=true"
+"&wlw-select_key%3A%7BactionForm.courseDay%7D="
+"&wlw-select_key%3A%7BactionForm.courseTime%7DOldValue=true"
+"&wlw-select_key%3A%7BactionForm.courseTime%7D="
+"&wlw-checkbox_key%3A%7BactionForm.queryDateFlag%7DOldValue=false"
//+"&deptIdHide=048"
;
out.write(requestStr);
out.flush();
out.close();
String nextPage = "";
String prePage = CurriculmFormPage;
while((nextPage = websiteScanning(conn)) != "") {
conn.disconnect();
nextPage = "http://elective.pku.edu.cn/"+nextPage;
nextPage = nextPage.replace("amp;", "");
conn = WebIterator(prePage, nextPage);
prePage = nextPage;
}
conn.disconnect();

验证码图片抓取

个人认为这个是除了登录之外第二坑的部分了,原因就在于,验证码图片这个东西每次读取都是不一样的,假如我拽一张图出来,虽然图变了,但是似乎这时候老验证码会失效,新的验证码输进去才有用。。

归根结底应该还是cookie的问题,事实上这种问题在用浏览器实现刷课的方法时会非常令人困惑,因为浏览器会自动加载验证码图片,当你尝试去获得验证码的图片的时候其实是第二次获得了验证码图片,这个时候验证码就会改变了。

于是我们利用Java.net,事实上可以很好的避免这个问题,我们爬下了网页源代码之后,不主动去加载图片,自然就不会改变验证码!

另外顺带一提的,验证码的网址也有一个参数是随机数,而且这个随机数和之前登录跳转页面的随机数还不一样,之前的那个随机数是[0,1]之间的一个小数,现在这个随机数要*10000。。表示不懂网站制作人的脑回路。。。

code

1
2
3
4
5
6
7
8
9
10
11
12
13
String srcPic = "http://elective.pku.edu.cn/elective2008/DrawServlet?Rand="+String.valueOf(Math.random()*10000);
HttpURLConnection conn = GetConn(srcPic, cookieKey, "GET", false);
conn.setRequestProperty("Referer", SupplyCancelPage);
conn.setRequestProperty("Host", "elective.pku.edu.cn");
conn.setRequestProperty("Accept-Encoding","gzip, deflate");
conn.setRequestProperty("Upgrade-Insecure-Requests","1");
conn.connect();
String strCookie = conn.getHeaderField("Set-Cookie");
BufferedImage picR = ImageIO.read(conn.getInputStream());
File picOut = new File("src/captcha/captcha.jpg");
ImageIO.write(picR, "jpg", picOut);

conn.disconnect();

TIP: 如果ImageIO在安卓上无法使用可以尝试其他文件读取方法。

多课程刷课

相比而言,这就是一个纯粹的代码实现问题了,大概步骤为:

  • 接受要补选的课程列表作为参数

  • 进入补退选界面

  • 枚举需要补选的课程,一一查看课程状态

    • 若课程状态已满,刷新之
    • 若课程状态未满,补选之
  • 枚举完毕,重新枚举,直到刷上课为止

需要注意的一点是,刷新课程的间隔下限是3s,低于这个返回的json不会返回任何东西!

代码太长,有兴趣的同学可以自行去project里查看。

图片demo

2

这是仅后端的展示,我们还有安卓app的视频demo,在课程pre展出后放出。